mirror of
https://github.com/supabase/supabase.git
synced 2026-10-11 04:15:04 +03:00
simplify all the things
This commit is contained in:
1 parent
82747fbe8a
commit
a7e63f2ad0
35 files changed
+465
-1239
No files matched your search
@@ -12,6 +12,15 @@ export type DatabaseConnectionType =
|
||||
| 'python'
|
||||
| 'sqlalchemy'
|
||||
|
||||
export const INSTALL_COMMANDS: Record<string, string> = {
|
||||
supabasejs: 'npm install @supabase/supabase-js',
|
||||
supabasepy: 'pip install supabase',
|
||||
supabaseflutter: 'flutter pub add supabase_flutter',
|
||||
supabaseswift:
|
||||
'swift package add-dependency https://github.com/supabase-community/supabase-swift',
|
||||
supabasekt: 'implementation("io.github.jan-tennert.supabase:supabase-kt:VERSION")',
|
||||
}
|
||||
|
||||
export const DATABASE_CONNECTION_TYPES: {
|
||||
id: DatabaseConnectionType
|
||||
label: string
|
||||
|
||||
@@ -24,28 +24,6 @@ export interface ConnectionStringPooler {
|
||||
direct?: string
|
||||
}
|
||||
|
||||
/** @deprecated Use ConnectionStringPooler instead */
|
||||
export type ConnectionStrings = ConnectionStringPooler
|
||||
|
||||
/** @deprecated Use StepContentProps instead */
|
||||
export interface ContentFileProps {
|
||||
projectKeys: {
|
||||
apiUrl: string
|
||||
anonKey?: string
|
||||
publishableKey?: string
|
||||
}
|
||||
connectionStringPooler: {
|
||||
transactionShared: string
|
||||
sessionShared: string
|
||||
transactionDedicated?: string
|
||||
sessionDedicated?: string
|
||||
ipv4SupportedForDedicatedPooler: boolean
|
||||
direct?: string
|
||||
}
|
||||
connectionTab: 'App Frameworks' | 'Mobile Frameworks' | 'ORMs'
|
||||
onCopy?: () => void
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Schema Types - Conditional Resolution
|
||||
// ============================================================================
|
||||
@@ -76,15 +54,7 @@ export type ConditionalValue<T> =
|
||||
// Schema Types - Modes
|
||||
// ============================================================================
|
||||
|
||||
export type ConnectMode = 'framework' | 'direct' | 'orm' | 'mcp'
|
||||
|
||||
export interface ModeDefinition {
|
||||
id: ConnectMode
|
||||
label: string
|
||||
description: string
|
||||
icon?: string
|
||||
fields: string[] // References to field IDs
|
||||
}
|
||||
export type ConnectMode = string
|
||||
|
||||
// ============================================================================
|
||||
// Schema Types - Fields
|
||||
@@ -99,13 +69,15 @@ export interface FieldOption {
|
||||
description?: string
|
||||
}
|
||||
|
||||
export type FieldOptionsResolver = (state: ConnectState) => FieldOption[]
|
||||
|
||||
export interface FieldDefinition {
|
||||
id: string
|
||||
type: FieldType
|
||||
label: string
|
||||
description?: string
|
||||
// Options can be static, or reference a data source, or be conditional
|
||||
options?: FieldOption[] | { source: string } | ConditionalValue<FieldOption[]>
|
||||
// Options can be static, conditional, or resolved from state
|
||||
options?: FieldOption[] | ConditionalValue<FieldOption[]> | FieldOptionsResolver
|
||||
// Only show this field when these state conditions are met
|
||||
dependsOn?: Record<string, string[]>
|
||||
// Default value for this field
|
||||
@@ -140,7 +112,6 @@ export type StepFieldValueMap = {
|
||||
// ============================================================================
|
||||
|
||||
export interface ConnectSchema {
|
||||
modes: ModeDefinition[]
|
||||
fields: Record<string, FieldDefinition>
|
||||
// Steps are fully conditional based on state
|
||||
steps: StepTree
|
||||
@@ -179,9 +150,3 @@ export interface StepContentProps {
|
||||
projectKeys: ProjectKeys
|
||||
connectionStringPooler: ConnectionStringPooler
|
||||
}
|
||||
|
||||
/** @deprecated Use StepContentProps instead */
|
||||
export interface StepComponentProps {
|
||||
state: ConnectState
|
||||
projectKeys: ProjectKeys
|
||||
}
|
||||
@@ -1,223 +0,0 @@
|
||||
import { describe, test, expect } from 'vitest'
|
||||
import { resolveFrameworkLibraryKey } from './Connect.utils'
|
||||
|
||||
describe('Connect.utils:resolveFrameworkLibraryKey', () => {
|
||||
test('should return null if no framework provided', () => {
|
||||
const result = resolveFrameworkLibraryKey({
|
||||
framework: undefined,
|
||||
frameworkVariant: undefined,
|
||||
library: undefined,
|
||||
})
|
||||
expect(result).toBeNull()
|
||||
})
|
||||
|
||||
test('should return null for empty framework string', () => {
|
||||
const result = resolveFrameworkLibraryKey({
|
||||
framework: '',
|
||||
frameworkVariant: undefined,
|
||||
library: undefined,
|
||||
})
|
||||
expect(result).toBeNull()
|
||||
})
|
||||
|
||||
test('should return explicit library if provided', () => {
|
||||
const result = resolveFrameworkLibraryKey({
|
||||
framework: 'nextjs',
|
||||
frameworkVariant: 'app',
|
||||
library: 'custom-library',
|
||||
})
|
||||
expect(result).toBe('custom-library')
|
||||
})
|
||||
|
||||
test('should resolve library for Next.js App Router', () => {
|
||||
const result = resolveFrameworkLibraryKey({
|
||||
framework: 'nextjs',
|
||||
frameworkVariant: 'app',
|
||||
library: undefined,
|
||||
})
|
||||
// Next.js App Router has supabasejs as its library
|
||||
expect(result).toBe('supabasejs')
|
||||
})
|
||||
|
||||
test('should resolve library for Next.js Pages Router', () => {
|
||||
const result = resolveFrameworkLibraryKey({
|
||||
framework: 'nextjs',
|
||||
frameworkVariant: 'pages',
|
||||
library: undefined,
|
||||
})
|
||||
expect(result).toBe('supabasejs')
|
||||
})
|
||||
|
||||
test('should resolve library for React with Vite variant', () => {
|
||||
const result = resolveFrameworkLibraryKey({
|
||||
framework: 'react',
|
||||
frameworkVariant: 'vite',
|
||||
library: undefined,
|
||||
})
|
||||
expect(result).toBe('supabasejs')
|
||||
})
|
||||
|
||||
test('should resolve library for React with Create React App variant', () => {
|
||||
const result = resolveFrameworkLibraryKey({
|
||||
framework: 'react',
|
||||
frameworkVariant: 'create-react-app',
|
||||
library: undefined,
|
||||
})
|
||||
expect(result).toBe('supabasejs')
|
||||
})
|
||||
|
||||
test('should resolve library for framework without variants (Remix)', () => {
|
||||
const result = resolveFrameworkLibraryKey({
|
||||
framework: 'remix',
|
||||
frameworkVariant: undefined,
|
||||
library: undefined,
|
||||
})
|
||||
// Remix has single child which is supabasejs
|
||||
expect(result).toBe('supabasejs')
|
||||
})
|
||||
|
||||
test('should resolve library for Flutter', () => {
|
||||
const result = resolveFrameworkLibraryKey({
|
||||
framework: 'flutter',
|
||||
frameworkVariant: undefined,
|
||||
library: undefined,
|
||||
})
|
||||
expect(result).toBe('supabaseflutter')
|
||||
})
|
||||
|
||||
test('should resolve library for Swift', () => {
|
||||
const result = resolveFrameworkLibraryKey({
|
||||
framework: 'swift',
|
||||
frameworkVariant: undefined,
|
||||
library: undefined,
|
||||
})
|
||||
expect(result).toBe('supabaseswift')
|
||||
})
|
||||
|
||||
test('should resolve library for Android Kotlin', () => {
|
||||
const result = resolveFrameworkLibraryKey({
|
||||
framework: 'androidkotlin',
|
||||
frameworkVariant: undefined,
|
||||
library: undefined,
|
||||
})
|
||||
expect(result).toBe('supabasekt')
|
||||
})
|
||||
|
||||
test('should resolve library for Flask (Python)', () => {
|
||||
const result = resolveFrameworkLibraryKey({
|
||||
framework: 'flask',
|
||||
frameworkVariant: undefined,
|
||||
library: undefined,
|
||||
})
|
||||
expect(result).toBe('supabasepy')
|
||||
})
|
||||
|
||||
test('should fallback to first variant library when variant not specified for multi-variant framework', () => {
|
||||
const result = resolveFrameworkLibraryKey({
|
||||
framework: 'nextjs',
|
||||
frameworkVariant: undefined,
|
||||
library: undefined,
|
||||
})
|
||||
// Should get library from first variant (app router)
|
||||
expect(result).toBe('supabasejs')
|
||||
})
|
||||
|
||||
test('should return null for unknown framework', () => {
|
||||
const result = resolveFrameworkLibraryKey({
|
||||
framework: 'unknown-framework',
|
||||
frameworkVariant: undefined,
|
||||
library: undefined,
|
||||
})
|
||||
expect(result).toBeNull()
|
||||
})
|
||||
|
||||
test('should handle SvelteKit framework', () => {
|
||||
const result = resolveFrameworkLibraryKey({
|
||||
framework: 'sveltekit',
|
||||
frameworkVariant: undefined,
|
||||
library: undefined,
|
||||
})
|
||||
expect(result).toBe('supabasejs')
|
||||
})
|
||||
|
||||
test('should handle Nuxt framework', () => {
|
||||
const result = resolveFrameworkLibraryKey({
|
||||
framework: 'nuxt',
|
||||
frameworkVariant: undefined,
|
||||
library: undefined,
|
||||
})
|
||||
expect(result).toBe('supabasejs')
|
||||
})
|
||||
|
||||
test('should handle Vue.js framework', () => {
|
||||
const result = resolveFrameworkLibraryKey({
|
||||
framework: 'vuejs',
|
||||
frameworkVariant: undefined,
|
||||
library: undefined,
|
||||
})
|
||||
expect(result).toBe('supabasejs')
|
||||
})
|
||||
|
||||
test('should handle Solid.js framework', () => {
|
||||
const result = resolveFrameworkLibraryKey({
|
||||
framework: 'solidjs',
|
||||
frameworkVariant: undefined,
|
||||
library: undefined,
|
||||
})
|
||||
expect(result).toBe('supabasejs')
|
||||
})
|
||||
|
||||
test('should handle Astro framework', () => {
|
||||
const result = resolveFrameworkLibraryKey({
|
||||
framework: 'astro',
|
||||
frameworkVariant: undefined,
|
||||
library: undefined,
|
||||
})
|
||||
expect(result).toBe('supabasejs')
|
||||
})
|
||||
|
||||
test('should handle Expo React Native', () => {
|
||||
const result = resolveFrameworkLibraryKey({
|
||||
framework: 'exporeactnative',
|
||||
frameworkVariant: undefined,
|
||||
library: undefined,
|
||||
})
|
||||
expect(result).toBe('supabasejs')
|
||||
})
|
||||
|
||||
test('should handle Ionic React', () => {
|
||||
const result = resolveFrameworkLibraryKey({
|
||||
framework: 'ionicreact',
|
||||
frameworkVariant: undefined,
|
||||
library: undefined,
|
||||
})
|
||||
expect(result).toBe('supabasejs')
|
||||
})
|
||||
|
||||
test('should handle Ionic Angular', () => {
|
||||
const result = resolveFrameworkLibraryKey({
|
||||
framework: 'ionicangular',
|
||||
frameworkVariant: undefined,
|
||||
library: undefined,
|
||||
})
|
||||
expect(result).toBe('supabasejs')
|
||||
})
|
||||
|
||||
test('should handle Refine framework', () => {
|
||||
const result = resolveFrameworkLibraryKey({
|
||||
framework: 'refine',
|
||||
frameworkVariant: undefined,
|
||||
library: undefined,
|
||||
})
|
||||
expect(result).toBe('supabasejs')
|
||||
})
|
||||
|
||||
test('should handle TanStack Start framework', () => {
|
||||
const result = resolveFrameworkLibraryKey({
|
||||
framework: 'tanstack',
|
||||
frameworkVariant: undefined,
|
||||
library: undefined,
|
||||
})
|
||||
expect(result).toBe('supabasejs')
|
||||
})
|
||||
})
|
||||
@@ -1,37 +0,0 @@
|
||||
import { FRAMEWORKS, MOBILES } from './Connect.constants'
|
||||
|
||||
type FieldValue = string | boolean | string[] | undefined
|
||||
|
||||
interface FrameworkLibraryInput {
|
||||
framework?: FieldValue
|
||||
frameworkVariant?: FieldValue
|
||||
library?: FieldValue
|
||||
[key: string]: FieldValue
|
||||
}
|
||||
|
||||
export function resolveFrameworkLibraryKey(state: FrameworkLibraryInput): string | null {
|
||||
const { framework, frameworkVariant, library } = state
|
||||
|
||||
if (!framework) return null
|
||||
|
||||
if (library) return String(library)
|
||||
|
||||
const allFrameworks = [...FRAMEWORKS, ...MOBILES]
|
||||
const selectedFramework = allFrameworks.find((f) => f.key === framework)
|
||||
|
||||
if (!selectedFramework?.children?.length) return null
|
||||
|
||||
if (frameworkVariant) {
|
||||
const variant = selectedFramework.children.find((c) => c.key === frameworkVariant)
|
||||
if (variant?.children?.length) {
|
||||
return variant.children[0].key
|
||||
}
|
||||
}
|
||||
|
||||
const firstChild = selectedFramework.children[0]
|
||||
if (firstChild?.children?.length) {
|
||||
return firstChild.children[0].key
|
||||
}
|
||||
|
||||
return firstChild?.key ?? null
|
||||
}
|
||||
@@ -1,5 +1,3 @@
|
||||
import { BASE_PATH } from 'lib/constants'
|
||||
import { Box, Cable, Database, Sparkles } from 'lucide-react'
|
||||
import { useTheme } from 'next-themes'
|
||||
import {
|
||||
RadioGroupStacked,
|
||||
@@ -10,9 +8,7 @@ import {
|
||||
SelectValue_Shadcn_,
|
||||
Select_Shadcn_,
|
||||
Switch,
|
||||
cn,
|
||||
} from 'ui'
|
||||
import { ClientSelectDropdown, MCP_CLIENTS } from 'ui-patterns/McpUrlBuilder'
|
||||
import { FormLayout } from 'ui-patterns/form/Layout/FormLayout'
|
||||
import {
|
||||
MultiSelector,
|
||||
@@ -22,18 +18,8 @@ import {
|
||||
MultiSelectorTrigger,
|
||||
} from 'ui-patterns/multi-select'
|
||||
|
||||
import { FRAMEWORKS, MOBILES } from './Connect.constants'
|
||||
import type { ConnectMode, FieldOption, ResolvedField } from './Connect.types'
|
||||
import type { FieldOption, ResolvedField } from './Connect.types'
|
||||
import { ConnectionIcon } from './ConnectionIcon'
|
||||
import { FrameworkSelector } from './FrameworkSelector'
|
||||
|
||||
// Icon mapping for modes
|
||||
const MODE_ICONS: Record<string, React.ReactNode> = {
|
||||
framework: <Box size={16} strokeWidth={1.5} />,
|
||||
direct: <Database size={16} strokeWidth={1.5} />,
|
||||
orm: <Cable size={16} strokeWidth={1.5} />,
|
||||
mcp: <Sparkles size={16} strokeWidth={1.5} />,
|
||||
}
|
||||
|
||||
interface ConnectConfigSectionProps {
|
||||
activeFields: ResolvedField[]
|
||||
@@ -54,9 +40,6 @@ export function ConnectConfigSection({
|
||||
|
||||
if (activeFields.length === 0) return null
|
||||
|
||||
// Get all frameworks for the combobox
|
||||
const allFrameworks = [...FRAMEWORKS, ...MOBILES]
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{activeFields.map((field) => {
|
||||
@@ -72,28 +55,6 @@ export function ConnectConfigSection({
|
||||
|
||||
switch (field.type) {
|
||||
case 'radio-grid':
|
||||
// For framework field, use the combobox selector
|
||||
if (field.id === 'framework') {
|
||||
return (
|
||||
<FormLayout
|
||||
key={field.id}
|
||||
layout="flex-row-reverse"
|
||||
label={field.label}
|
||||
description="What is your preferred coding agent"
|
||||
className={formLayoutClassName}
|
||||
>
|
||||
<FrameworkSelector
|
||||
value={String(value ?? '')}
|
||||
onChange={(v) => onFieldChange(field.id, v)}
|
||||
items={allFrameworks}
|
||||
className="w-full"
|
||||
size="small"
|
||||
/>
|
||||
</FormLayout>
|
||||
)
|
||||
}
|
||||
|
||||
// Default radio-grid behavior for other fields
|
||||
return (
|
||||
<FormLayout
|
||||
key={field.id}
|
||||
@@ -162,30 +123,6 @@ export function ConnectConfigSection({
|
||||
)
|
||||
|
||||
case 'select':
|
||||
// Special case for MCP client - use the custom dropdown
|
||||
if (field.id === 'mcpClient') {
|
||||
const selectedClient = MCP_CLIENTS.find((c) => c.key === value) ?? MCP_CLIENTS[0]
|
||||
return (
|
||||
<FormLayout
|
||||
key={field.id}
|
||||
layout="flex-row-reverse"
|
||||
label={field.label}
|
||||
description={field.description}
|
||||
className={formLayoutClassName}
|
||||
>
|
||||
<ClientSelectDropdown
|
||||
basePath={BASE_PATH}
|
||||
theme={(resolvedTheme ?? 'dark') as 'light' | 'dark'}
|
||||
label=""
|
||||
className="w-full"
|
||||
clients={MCP_CLIENTS}
|
||||
selectedClient={selectedClient}
|
||||
onClientChange={(v) => onFieldChange(field.id, v)}
|
||||
/>
|
||||
</FormLayout>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<FormLayout
|
||||
key={field.id}
|
||||
@@ -275,39 +212,3 @@ export function ConnectConfigSection({
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Mode Selector
|
||||
// ============================================================================
|
||||
|
||||
interface ModeSelectorProps {
|
||||
modes: Array<{ id: ConnectMode; label: string; description: string }>
|
||||
selected: ConnectMode
|
||||
onChange: (mode: ConnectMode) => void
|
||||
}
|
||||
|
||||
export function ModeSelector({ modes, selected, onChange }: ModeSelectorProps) {
|
||||
return (
|
||||
<div className="grid grid-cols-4 rounded-lg border">
|
||||
{modes.map((mode) => (
|
||||
<button
|
||||
key={mode.id}
|
||||
type="button"
|
||||
onClick={() => onChange(mode.id)}
|
||||
className={cn(
|
||||
'flex flex-col items-center gap-2 p-4 transition-colors border-r last:border-r-0',
|
||||
selected === mode.id
|
||||
? 'bg-surface-200'
|
||||
: 'border-default hover:border-strong hover:bg-surface-100 '
|
||||
)}
|
||||
>
|
||||
<span className="text-foreground-light">{MODE_ICONS[mode.id]}</span>
|
||||
<div>
|
||||
<p className="heading-default text-center">{mode.label}</p>
|
||||
<p className="text-sm text-foreground-lighter text-center">{mode.description}</p>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -3,13 +3,12 @@ import { useParams } from 'common'
|
||||
import { getKeys, useAPIKeysQuery } from 'data/api-keys/api-keys-query'
|
||||
import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query'
|
||||
import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions'
|
||||
import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled'
|
||||
import { parseAsBoolean, parseAsString, useQueryState } from 'nuqs'
|
||||
import { useMemo } from 'react'
|
||||
import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, cn } from 'ui'
|
||||
|
||||
import type { ProjectKeys } from './Connect.types'
|
||||
import { ConnectConfigSection, ModeSelector } from './ConnectConfigSection'
|
||||
import { ConnectConfigSection } from './ConnectConfigSection'
|
||||
import { ConnectStepsSection } from './ConnectStepsSection'
|
||||
import { useConnectState } from './useConnectState'
|
||||
|
||||
@@ -27,34 +26,7 @@ export const ConnectSheet = () => {
|
||||
setShowConnect(sheetOpen)
|
||||
}
|
||||
|
||||
const {
|
||||
projectConnectionShowAppFrameworks: showAppFrameworks,
|
||||
projectConnectionShowMobileFrameworks: showMobileFrameworks,
|
||||
} = useIsFeatureEnabled([
|
||||
'project_connection:show_app_frameworks',
|
||||
'project_connection:show_mobile_frameworks',
|
||||
])
|
||||
|
||||
const handleSourceChange = (databaseId: string) => {
|
||||
// Database selection is handled by the DatabaseSelector's internal state
|
||||
// We just need to trigger a re-render of connection strings
|
||||
}
|
||||
|
||||
// Filter available modes based on feature flags
|
||||
const availableModeIds = useMemo(() => {
|
||||
const showFrameworks = showAppFrameworks || showMobileFrameworks
|
||||
|
||||
return showFrameworks ? ['framework'] : []
|
||||
}, [showAppFrameworks, showMobileFrameworks])
|
||||
|
||||
const { state, updateField, setMode, activeFields, resolvedSteps, getFieldOptions, schema } =
|
||||
useConnectState()
|
||||
|
||||
// Filter modes based on feature flags
|
||||
const availableModes = useMemo(
|
||||
() => schema.modes.filter((m) => availableModeIds.includes(m.id)),
|
||||
[schema.modes, availableModeIds]
|
||||
)
|
||||
const { state, updateField, activeFields, resolvedSteps, getFieldOptions } = useConnectState()
|
||||
|
||||
// Project keys for step components
|
||||
const { ref: projectRef } = useParams()
|
||||
@@ -90,21 +62,14 @@ export const ConnectSheet = () => {
|
||||
<Sheet open={showConnect} onOpenChange={handleOpenChange}>
|
||||
<SheetContent size="lg" className="flex flex-col gap-0 p-0 space-y-0" tabIndex={undefined}>
|
||||
<SheetHeader className={cn('text-left border-b shrink-0 py-6 px-8')}>
|
||||
<div className="flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between">
|
||||
<div>
|
||||
<SheetTitle>Connect to your project</SheetTitle>
|
||||
<SheetDescription>Choose how you want to use Supabase</SheetDescription>
|
||||
</div>
|
||||
</div>
|
||||
<SheetTitle>Connect to your project</SheetTitle>
|
||||
<SheetDescription>Choose how you want to use Supabase</SheetDescription>
|
||||
</SheetHeader>
|
||||
|
||||
<div className="flex flex-1 flex-col overflow-y-auto">
|
||||
{/* Configuration Section */}
|
||||
<div className="space-y-6 border-b p-8 shrink-0">
|
||||
{availableModes.length > 1 && (
|
||||
<ModeSelector modes={availableModes} selected={state.mode} onChange={setMode} />
|
||||
)}
|
||||
<div className="border-t pt-8">
|
||||
<div>
|
||||
<ConnectConfigSection
|
||||
activeFields={activeFields}
|
||||
state={state}
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
import dynamic from 'next/dynamic'
|
||||
import { useMemo, useRef } from 'react'
|
||||
|
||||
import { useParams } from 'common'
|
||||
import { getAddons } from 'components/interfaces/Billing/Subscription/Subscription.utils'
|
||||
import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query'
|
||||
@@ -9,18 +6,20 @@ import { useSupavisorConfigurationQuery } from 'data/database/supavisor-configur
|
||||
import { useProjectAddonsQuery } from 'data/subscriptions/project-addons-query'
|
||||
import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization'
|
||||
import { pluckObjectFields } from 'lib/helpers'
|
||||
import dynamic from 'next/dynamic'
|
||||
import { useMemo, useRef } from 'react'
|
||||
import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
|
||||
|
||||
import type {
|
||||
ConnectionStringPooler,
|
||||
ConnectState,
|
||||
ConnectionStringPooler,
|
||||
ProjectKeys,
|
||||
ResolvedStep,
|
||||
StepContentProps,
|
||||
} from './Connect.types'
|
||||
import { ConnectSheetStep } from './ConnectSheetStep'
|
||||
import { CopyPromptAdmonition } from './CopyPromptAdmonition'
|
||||
import { getConnectionStrings } from './DatabaseSettings.utils'
|
||||
import { getConnectionStringPooler } from './DatabaseSettings.utils'
|
||||
|
||||
interface ConnectStepsSectionProps {
|
||||
steps: ResolvedStep[]
|
||||
@@ -68,7 +67,7 @@ function useConnectionStringPooler(): ConnectionStringPooler {
|
||||
const poolingConfigurationShared = supavisorConfig?.find((x) => x.database_type === 'PRIMARY')
|
||||
const poolingConfigurationDedicated = allowPgBouncerSelection ? pgbouncerConfig : undefined
|
||||
|
||||
const connectionStringsShared = getConnectionStrings({
|
||||
const ConnectionStringPoolerShared = getConnectionStringPooler({
|
||||
connectionInfo,
|
||||
poolingInfo: {
|
||||
connectionString: poolingConfigurationShared?.connection_string ?? '',
|
||||
@@ -80,9 +79,9 @@ function useConnectionStringPooler(): ConnectionStringPooler {
|
||||
metadata: { projectRef },
|
||||
})
|
||||
|
||||
const connectionStringsDedicated =
|
||||
const ConnectionStringPoolerDedicated =
|
||||
poolingConfigurationDedicated !== undefined
|
||||
? getConnectionStrings({
|
||||
? getConnectionStringPooler({
|
||||
connectionInfo,
|
||||
poolingInfo: {
|
||||
connectionString: poolingConfigurationDedicated.connection_string,
|
||||
@@ -97,14 +96,14 @@ function useConnectionStringPooler(): ConnectionStringPooler {
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
transactionShared: connectionStringsShared.pooler.uri,
|
||||
sessionShared: connectionStringsShared.pooler.uri.replace('6543', '5432'),
|
||||
transactionDedicated: connectionStringsDedicated?.pooler.uri,
|
||||
sessionDedicated: connectionStringsDedicated?.pooler.uri.replace('6543', '5432'),
|
||||
transactionShared: ConnectionStringPoolerShared.pooler.uri,
|
||||
sessionShared: ConnectionStringPoolerShared.pooler.uri.replace('6543', '5432'),
|
||||
transactionDedicated: ConnectionStringPoolerDedicated?.pooler.uri,
|
||||
sessionDedicated: ConnectionStringPoolerDedicated?.pooler.uri.replace('6543', '5432'),
|
||||
ipv4SupportedForDedicatedPooler: !!ipv4Addon,
|
||||
direct: connectionStringsShared.direct.uri,
|
||||
direct: ConnectionStringPoolerShared.direct.uri,
|
||||
}),
|
||||
[connectionStringsShared, connectionStringsDedicated, ipv4Addon]
|
||||
[ConnectionStringPoolerShared, ConnectionStringPoolerDedicated, ipv4Addon]
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
type ConnectionStrings = {
|
||||
type ConnectionStringPooler = {
|
||||
psql: string
|
||||
uri: string
|
||||
golang: string
|
||||
@@ -10,7 +10,7 @@ type ConnectionStrings = {
|
||||
sqlalchemy: string
|
||||
}
|
||||
|
||||
export const getConnectionStrings = ({
|
||||
export const getConnectionStringPooler = ({
|
||||
connectionInfo,
|
||||
poolingInfo,
|
||||
metadata,
|
||||
@@ -33,8 +33,8 @@ export const getConnectionStrings = ({
|
||||
pgVersion?: string
|
||||
}
|
||||
}): {
|
||||
direct: ConnectionStrings
|
||||
pooler: ConnectionStrings
|
||||
direct: ConnectionStringPooler
|
||||
pooler: ConnectionStringPooler
|
||||
} => {
|
||||
const isMd5 = poolingInfo?.connectionString.includes('options=reference')
|
||||
const { projectRef } = metadata
|
||||
@@ -65,14 +65,14 @@ export const getConnectionStrings = ({
|
||||
|
||||
// User Id=${directUser};Password=${password};Server=${directHost};Port=${directPort};Database=${directName}`
|
||||
const directDotNetString = `{
|
||||
"ConnectionStrings": {
|
||||
"ConnectionStringPooler": {
|
||||
"DefaultConnection": "Host=${directHost};Database=${directName};Username=${directUser};Password=${password};SSL Mode=Require;Trust Server Certificate=true"
|
||||
}
|
||||
}`
|
||||
|
||||
// `User Id=${poolerUser};Password=${password};Server=${poolerHost};Port=${poolerPort};Database=${poolerName}${isMd5 ? `;Options='reference=${projectRef}'` : ''}`
|
||||
const poolerDotNetString = `{
|
||||
"ConnectionStrings": {
|
||||
"ConnectionStringPooler": {
|
||||
"DefaultConnection": "User Id=${poolerUser};Password=${password};Server=${poolerHost};Port=${poolerPort};Database=${poolerName}${isMd5 ? `;Options='reference=${projectRef}'` : ''}"
|
||||
}
|
||||
}`
|
||||
|
||||
@@ -1,93 +0,0 @@
|
||||
import { Box, Check, ChevronDown } from 'lucide-react'
|
||||
import { useState } from 'react'
|
||||
import {
|
||||
Button,
|
||||
CommandEmpty_Shadcn_,
|
||||
CommandGroup_Shadcn_,
|
||||
CommandInput_Shadcn_,
|
||||
CommandItem_Shadcn_,
|
||||
CommandList_Shadcn_,
|
||||
Command_Shadcn_,
|
||||
PopoverContent_Shadcn_,
|
||||
PopoverTrigger_Shadcn_,
|
||||
Popover_Shadcn_,
|
||||
cn,
|
||||
} from 'ui'
|
||||
|
||||
import { ConnectionType } from '@/components/interfaces/ConnectSheet/Connect.constants'
|
||||
import { ConnectionIcon } from '@/components/interfaces/ConnectSheet/ConnectionIcon'
|
||||
|
||||
interface FrameworkSelectorProps {
|
||||
value: string
|
||||
onChange: (value: string) => void
|
||||
items: ConnectionType[]
|
||||
className?: string
|
||||
size?: 'tiny' | 'small'
|
||||
}
|
||||
|
||||
export const FrameworkSelector = ({
|
||||
value,
|
||||
onChange,
|
||||
items,
|
||||
className,
|
||||
size = 'tiny',
|
||||
}: FrameworkSelectorProps) => {
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
const selectedItem = items.find((item) => item.key === value)
|
||||
|
||||
function handleSelect(key: string) {
|
||||
onChange(key)
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<Popover_Shadcn_ open={open} onOpenChange={setOpen} modal={false}>
|
||||
<div className={cn('flex', className)}>
|
||||
<PopoverTrigger_Shadcn_ asChild>
|
||||
<Button
|
||||
size={size}
|
||||
type="default"
|
||||
className={cn('gap-0 justify-between', className?.includes('w-full') && 'w-full')}
|
||||
iconRight={<ChevronDown strokeWidth={1.5} />}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
{selectedItem?.icon ? <ConnectionIcon icon={selectedItem.icon} /> : <Box size={12} />}
|
||||
{selectedItem?.label}
|
||||
</div>
|
||||
</Button>
|
||||
</PopoverTrigger_Shadcn_>
|
||||
</div>
|
||||
<PopoverContent_Shadcn_
|
||||
className="p-0 w-[var(--radix-popover-trigger-width)] min-w-48"
|
||||
side="bottom"
|
||||
align="start"
|
||||
onOpenAutoFocus={(e) => e.preventDefault()}
|
||||
>
|
||||
<Command_Shadcn_>
|
||||
<CommandInput_Shadcn_ placeholder="Search..." />
|
||||
<CommandList_Shadcn_>
|
||||
<CommandEmpty_Shadcn_>No results found.</CommandEmpty_Shadcn_>
|
||||
<CommandGroup_Shadcn_>
|
||||
{items.map((item) => (
|
||||
<CommandItem_Shadcn_
|
||||
key={item.key}
|
||||
value={item.key}
|
||||
onSelect={() => handleSelect(item.key)}
|
||||
className="flex gap-2 items-center"
|
||||
>
|
||||
{item.icon ? <ConnectionIcon icon={item.icon} /> : <Box size={12} />}
|
||||
{item.label}
|
||||
<Check
|
||||
size={15}
|
||||
className={cn('ml-auto', item.key === value ? 'opacity-100' : 'opacity-0')}
|
||||
/>
|
||||
</CommandItem_Shadcn_>
|
||||
))}
|
||||
</CommandGroup_Shadcn_>
|
||||
</CommandList_Shadcn_>
|
||||
</Command_Shadcn_>
|
||||
</PopoverContent_Shadcn_>
|
||||
</Popover_Shadcn_>
|
||||
)
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
import { describe, expect, test } from 'vitest'
|
||||
|
||||
import type { ConditionalValue, ConnectSchema, ConnectState, StepTree } from './Connect.types'
|
||||
import type { ConditionalValue, ConnectSchema, StepTree } from './Connect.types'
|
||||
import {
|
||||
getActiveFields,
|
||||
getDefaultState,
|
||||
resetDependentFields,
|
||||
resolveConditional,
|
||||
resolveState,
|
||||
resolveSteps,
|
||||
} from './connect.resolver'
|
||||
|
||||
@@ -155,10 +155,6 @@ describe('connect.resolver:resolveConditional', () => {
|
||||
|
||||
describe('connect.resolver:resolveSteps', () => {
|
||||
const createMockSchema = (steps: StepTree): ConnectSchema => ({
|
||||
modes: [
|
||||
{ id: 'framework', label: 'Framework', description: '', fields: [] },
|
||||
{ id: 'direct', label: 'Direct', description: '', fields: [] },
|
||||
],
|
||||
fields: {},
|
||||
steps,
|
||||
})
|
||||
@@ -299,116 +295,118 @@ describe('connect.resolver:resolveSteps', () => {
|
||||
// ============================================================================
|
||||
|
||||
describe('connect.resolver:getActiveFields', () => {
|
||||
const createSchemaWithFields = (
|
||||
modes: ConnectSchema['modes'],
|
||||
fields: ConnectSchema['fields']
|
||||
): ConnectSchema => ({
|
||||
modes,
|
||||
const createSchemaWithFields = (fields: ConnectSchema['fields']): ConnectSchema => ({
|
||||
fields,
|
||||
steps: [],
|
||||
})
|
||||
|
||||
test('should return fields for the current mode', () => {
|
||||
const schema = createSchemaWithFields(
|
||||
[
|
||||
{ id: 'framework', label: 'Framework', description: '', fields: ['framework', 'library'] },
|
||||
{ id: 'direct', label: 'Direct', description: '', fields: ['connectionType'] },
|
||||
],
|
||||
{
|
||||
mode: {
|
||||
id: 'mode',
|
||||
type: 'select',
|
||||
label: 'Mode',
|
||||
defaultValue: 'framework',
|
||||
options: () => [
|
||||
{ value: 'framework', label: 'Framework' },
|
||||
{ value: 'direct', label: 'Direct' },
|
||||
],
|
||||
},
|
||||
framework: {
|
||||
id: 'framework',
|
||||
type: 'radio-grid',
|
||||
label: 'Framework',
|
||||
defaultValue: 'nextjs',
|
||||
dependsOn: { mode: ['framework'] },
|
||||
},
|
||||
library: {
|
||||
id: 'library',
|
||||
type: 'select',
|
||||
label: 'Library',
|
||||
defaultValue: 'supabasejs',
|
||||
dependsOn: { mode: ['framework'] },
|
||||
},
|
||||
connectionType: {
|
||||
id: 'connectionType',
|
||||
type: 'select',
|
||||
label: 'Type',
|
||||
defaultValue: 'uri',
|
||||
dependsOn: { mode: ['direct'] },
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
const frameworkFields = getActiveFields(schema, { mode: 'framework' })
|
||||
expect(frameworkFields).toHaveLength(2)
|
||||
expect(frameworkFields.map((f) => f.id)).toEqual(['framework', 'library'])
|
||||
expect(frameworkFields.map((f) => f.id)).toEqual(['mode', 'framework', 'library'])
|
||||
|
||||
const directFields = getActiveFields(schema, { mode: 'direct' })
|
||||
expect(directFields).toHaveLength(1)
|
||||
expect(directFields[0].id).toBe('connectionType')
|
||||
expect(directFields.map((f) => f.id)).toEqual(['mode', 'connectionType'])
|
||||
})
|
||||
|
||||
test('should filter fields by dependsOn conditions', () => {
|
||||
const schema = createSchemaWithFields(
|
||||
[
|
||||
{
|
||||
id: 'framework',
|
||||
label: 'Framework',
|
||||
description: '',
|
||||
fields: ['framework', 'frameworkVariant', 'frameworkUi'],
|
||||
},
|
||||
],
|
||||
{
|
||||
mode: {
|
||||
id: 'mode',
|
||||
type: 'select',
|
||||
label: 'Mode',
|
||||
defaultValue: 'framework',
|
||||
options: () => [{ value: 'framework', label: 'Framework' }],
|
||||
},
|
||||
framework: {
|
||||
id: 'framework',
|
||||
type: 'radio-grid',
|
||||
label: 'Framework',
|
||||
defaultValue: 'nextjs',
|
||||
dependsOn: { mode: ['framework'] },
|
||||
},
|
||||
frameworkVariant: {
|
||||
id: 'frameworkVariant',
|
||||
type: 'select',
|
||||
label: 'Variant',
|
||||
dependsOn: { framework: ['nextjs', 'react'] },
|
||||
dependsOn: { mode: ['framework'], framework: ['nextjs', 'react'] },
|
||||
},
|
||||
frameworkUi: {
|
||||
id: 'frameworkUi',
|
||||
type: 'switch',
|
||||
label: 'Shadcn',
|
||||
dependsOn: { framework: ['nextjs', 'react'] },
|
||||
dependsOn: { mode: ['framework'], framework: ['nextjs', 'react'] },
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
// With nextjs - should show all fields
|
||||
const nextjsFields = getActiveFields(schema, { mode: 'framework', framework: 'nextjs' })
|
||||
expect(nextjsFields).toHaveLength(3)
|
||||
expect(nextjsFields).toHaveLength(4)
|
||||
|
||||
// With vue - should hide frameworkVariant and frameworkUi
|
||||
const vueFields = getActiveFields(schema, { mode: 'framework', framework: 'vue' })
|
||||
expect(vueFields).toHaveLength(1)
|
||||
expect(vueFields[0].id).toBe('framework')
|
||||
expect(vueFields.map((field) => field.id)).toEqual(['mode', 'framework'])
|
||||
})
|
||||
|
||||
test('should handle multiple dependsOn conditions', () => {
|
||||
const schema = createSchemaWithFields(
|
||||
[
|
||||
{
|
||||
id: 'direct',
|
||||
label: 'Direct',
|
||||
description: '',
|
||||
fields: ['connectionMethod', 'useSharedPooler'],
|
||||
},
|
||||
],
|
||||
{
|
||||
mode: {
|
||||
id: 'mode',
|
||||
type: 'select',
|
||||
label: 'Mode',
|
||||
defaultValue: 'direct',
|
||||
options: () => [{ value: 'direct', label: 'Direct' }],
|
||||
},
|
||||
connectionMethod: {
|
||||
id: 'connectionMethod',
|
||||
type: 'radio-list',
|
||||
label: 'Method',
|
||||
defaultValue: 'direct',
|
||||
dependsOn: { mode: ['direct'] },
|
||||
},
|
||||
useSharedPooler: {
|
||||
id: 'useSharedPooler',
|
||||
type: 'switch',
|
||||
label: 'Use Shared Pooler',
|
||||
dependsOn: { connectionMethod: ['transaction'] },
|
||||
dependsOn: { mode: ['direct'], connectionMethod: ['transaction'] },
|
||||
},
|
||||
}
|
||||
)
|
||||
@@ -418,41 +416,55 @@ describe('connect.resolver:getActiveFields', () => {
|
||||
mode: 'direct',
|
||||
connectionMethod: 'transaction',
|
||||
})
|
||||
expect(transactionFields).toHaveLength(2)
|
||||
expect(transactionFields.map((field) => field.id)).toEqual([
|
||||
'mode',
|
||||
'connectionMethod',
|
||||
'useSharedPooler',
|
||||
])
|
||||
|
||||
// Direct mode - hide shared pooler option
|
||||
const directFields = getActiveFields(schema, { mode: 'direct', connectionMethod: 'direct' })
|
||||
expect(directFields).toHaveLength(1)
|
||||
expect(directFields[0].id).toBe('connectionMethod')
|
||||
expect(directFields.map((field) => field.id)).toEqual(['mode', 'connectionMethod'])
|
||||
})
|
||||
|
||||
test('should return empty array for invalid mode', () => {
|
||||
test('should return only mode field when dependsOn does not match', () => {
|
||||
const schema = createSchemaWithFields(
|
||||
[{ id: 'framework', label: 'Framework', description: '', fields: ['framework'] }],
|
||||
{ framework: { id: 'framework', type: 'radio-grid', label: 'Framework' } }
|
||||
{
|
||||
mode: {
|
||||
id: 'mode',
|
||||
type: 'select',
|
||||
label: 'Mode',
|
||||
defaultValue: 'framework',
|
||||
options: () => [{ value: 'framework', label: 'Framework' }],
|
||||
},
|
||||
framework: {
|
||||
id: 'framework',
|
||||
type: 'radio-grid',
|
||||
label: 'Framework',
|
||||
dependsOn: { mode: ['framework'] },
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
const fields = getActiveFields(schema, { mode: 'invalid' as any })
|
||||
expect(fields).toEqual([])
|
||||
expect(fields.map((field) => field.id)).toEqual(['mode'])
|
||||
})
|
||||
|
||||
test('should include resolvedOptions for each field', () => {
|
||||
const schema = createSchemaWithFields(
|
||||
[{ id: 'framework', label: 'Framework', description: '', fields: ['framework'] }],
|
||||
{
|
||||
framework: {
|
||||
id: 'framework',
|
||||
type: 'radio-grid',
|
||||
label: 'Framework',
|
||||
options: { source: 'frameworks' }, // Source reference - resolved elsewhere
|
||||
options: () => [{ value: 'nextjs', label: 'Next.js' }],
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
const fields = getActiveFields(schema, { mode: 'framework' })
|
||||
expect(fields[0]).toHaveProperty('resolvedOptions')
|
||||
// Source options are resolved by the hook, not the resolver
|
||||
expect(fields[0].resolvedOptions).toEqual([])
|
||||
expect(fields[0].resolvedOptions).toEqual([{ value: 'nextjs', label: 'Next.js' }])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -461,35 +473,29 @@ describe('connect.resolver:getActiveFields', () => {
|
||||
// ============================================================================
|
||||
|
||||
describe('connect.resolver:getDefaultState', () => {
|
||||
test('should return default state with first mode', () => {
|
||||
const schema: ConnectSchema = {
|
||||
modes: [
|
||||
{ id: 'framework', label: 'Framework', description: '', fields: [] },
|
||||
{ id: 'direct', label: 'Direct', description: '', fields: [] },
|
||||
],
|
||||
fields: {},
|
||||
steps: [],
|
||||
}
|
||||
|
||||
const state = getDefaultState(schema)
|
||||
expect(state.mode).toBe('framework')
|
||||
})
|
||||
|
||||
test('should include default values from fields', () => {
|
||||
const schema: ConnectSchema = {
|
||||
modes: [{ id: 'framework', label: 'Framework', description: '', fields: ['framework'] }],
|
||||
fields: {
|
||||
mode: {
|
||||
id: 'mode',
|
||||
type: 'select',
|
||||
label: 'Mode',
|
||||
defaultValue: 'framework',
|
||||
options: () => [{ value: 'framework', label: 'Framework' }],
|
||||
},
|
||||
framework: {
|
||||
id: 'framework',
|
||||
type: 'radio-grid',
|
||||
label: 'Framework',
|
||||
defaultValue: 'nextjs',
|
||||
dependsOn: { mode: ['framework'] },
|
||||
},
|
||||
library: {
|
||||
id: 'library',
|
||||
type: 'select',
|
||||
label: 'Library',
|
||||
defaultValue: 'supabasejs',
|
||||
dependsOn: { mode: ['framework'] },
|
||||
},
|
||||
mcpReadonly: {
|
||||
id: 'mcpReadonly',
|
||||
@@ -506,119 +512,113 @@ describe('connect.resolver:getDefaultState', () => {
|
||||
expect(state.library).toBe('supabasejs')
|
||||
expect(state.mcpReadonly).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
test('should fallback to "direct" if no modes defined', () => {
|
||||
// ============================================================================
|
||||
// resolveState Tests
|
||||
// ============================================================================
|
||||
|
||||
describe('connect.resolver:resolveState', () => {
|
||||
test('should apply defaults from options when valid', () => {
|
||||
const schema: ConnectSchema = {
|
||||
modes: [],
|
||||
fields: {},
|
||||
fields: {
|
||||
mode: {
|
||||
id: 'mode',
|
||||
type: 'select',
|
||||
label: 'Mode',
|
||||
defaultValue: 'framework',
|
||||
options: () => [{ value: 'framework', label: 'Framework' }],
|
||||
},
|
||||
framework: {
|
||||
id: 'framework',
|
||||
type: 'select',
|
||||
label: 'Framework',
|
||||
defaultValue: 'react',
|
||||
options: () => [
|
||||
{ value: 'nextjs', label: 'Next.js' },
|
||||
{ value: 'react', label: 'React' },
|
||||
],
|
||||
dependsOn: { mode: ['framework'] },
|
||||
},
|
||||
},
|
||||
steps: [],
|
||||
}
|
||||
|
||||
const state = getDefaultState(schema)
|
||||
expect(state.mode).toBe('direct')
|
||||
const state = resolveState(schema, {})
|
||||
expect(state.mode).toBe('framework')
|
||||
expect(state.framework).toBe('react')
|
||||
})
|
||||
})
|
||||
|
||||
// ============================================================================
|
||||
// resetDependentFields Tests
|
||||
// ============================================================================
|
||||
|
||||
describe('connect.resolver:resetDependentFields', () => {
|
||||
const createSchemaForReset = (): ConnectSchema => ({
|
||||
modes: [
|
||||
{
|
||||
id: 'framework',
|
||||
label: 'Framework',
|
||||
description: '',
|
||||
fields: ['framework', 'frameworkVariant', 'frameworkUi'],
|
||||
},
|
||||
{ id: 'direct', label: 'Direct', description: '', fields: ['connectionMethod'] },
|
||||
],
|
||||
fields: {
|
||||
framework: {
|
||||
id: 'framework',
|
||||
type: 'radio-grid',
|
||||
label: 'Framework',
|
||||
defaultValue: 'nextjs',
|
||||
},
|
||||
frameworkVariant: {
|
||||
id: 'frameworkVariant',
|
||||
type: 'select',
|
||||
label: 'Variant',
|
||||
dependsOn: { framework: ['nextjs', 'react'] },
|
||||
test('should fall back to first option when default is invalid', () => {
|
||||
const schema: ConnectSchema = {
|
||||
fields: {
|
||||
mode: {
|
||||
id: 'mode',
|
||||
type: 'select',
|
||||
label: 'Mode',
|
||||
defaultValue: 'framework',
|
||||
options: () => [{ value: 'framework', label: 'Framework' }],
|
||||
},
|
||||
framework: {
|
||||
id: 'framework',
|
||||
type: 'select',
|
||||
label: 'Framework',
|
||||
defaultValue: 'angular',
|
||||
options: () => [
|
||||
{ value: 'nextjs', label: 'Next.js' },
|
||||
{ value: 'react', label: 'React' },
|
||||
],
|
||||
dependsOn: { mode: ['framework'] },
|
||||
},
|
||||
},
|
||||
frameworkUi: {
|
||||
id: 'frameworkUi',
|
||||
type: 'switch',
|
||||
label: 'Shadcn',
|
||||
dependsOn: { framework: ['nextjs', 'react'] },
|
||||
},
|
||||
connectionMethod: {
|
||||
id: 'connectionMethod',
|
||||
type: 'radio-list',
|
||||
label: 'Method',
|
||||
defaultValue: 'direct',
|
||||
},
|
||||
},
|
||||
steps: [],
|
||||
})
|
||||
|
||||
test('should reset dependent fields when dependency no longer satisfied', () => {
|
||||
const schema = createSchemaForReset()
|
||||
const state: ConnectState = {
|
||||
mode: 'framework',
|
||||
framework: 'vue', // Changed from nextjs to vue
|
||||
frameworkVariant: 'app', // This should be reset
|
||||
frameworkUi: true, // This should be reset
|
||||
steps: [],
|
||||
}
|
||||
|
||||
const newState = resetDependentFields(state, 'framework', schema)
|
||||
|
||||
expect(newState.frameworkVariant).toBeUndefined()
|
||||
expect(newState.frameworkUi).toBeUndefined()
|
||||
const state = resolveState(schema, {})
|
||||
expect(state.framework).toBe('nextjs')
|
||||
})
|
||||
|
||||
test('should keep dependent fields when dependency still satisfied', () => {
|
||||
const schema = createSchemaForReset()
|
||||
const state: ConnectState = {
|
||||
mode: 'framework',
|
||||
framework: 'react', // Still in the allowed list
|
||||
frameworkVariant: 'vite',
|
||||
frameworkUi: true,
|
||||
}
|
||||
|
||||
const newState = resetDependentFields(state, 'framework', schema)
|
||||
|
||||
expect(newState.frameworkVariant).toBe('vite')
|
||||
expect(newState.frameworkUi).toBe(true)
|
||||
})
|
||||
|
||||
test('should handle mode changes', () => {
|
||||
const schema = createSchemaForReset()
|
||||
const state: ConnectState = {
|
||||
mode: 'direct', // Changed mode
|
||||
framework: 'nextjs',
|
||||
frameworkVariant: 'app',
|
||||
test('should refresh dependent values when options change', () => {
|
||||
const schema: ConnectSchema = {
|
||||
fields: {
|
||||
mode: {
|
||||
id: 'mode',
|
||||
type: 'select',
|
||||
label: 'Mode',
|
||||
defaultValue: 'framework',
|
||||
options: () => [{ value: 'framework', label: 'Framework' }],
|
||||
},
|
||||
framework: {
|
||||
id: 'framework',
|
||||
type: 'select',
|
||||
label: 'Framework',
|
||||
defaultValue: 'nextjs',
|
||||
options: () => [
|
||||
{ value: 'nextjs', label: 'Next.js' },
|
||||
{ value: 'react', label: 'React' },
|
||||
],
|
||||
dependsOn: { mode: ['framework'] },
|
||||
},
|
||||
variant: {
|
||||
id: 'variant',
|
||||
type: 'select',
|
||||
label: 'Variant',
|
||||
options: (state) =>
|
||||
state.framework === 'react'
|
||||
? [{ value: 'vite', label: 'Vite' }]
|
||||
: [{ value: 'app', label: 'App' }],
|
||||
dependsOn: { mode: ['framework'], framework: ['nextjs', 'react'] },
|
||||
},
|
||||
},
|
||||
steps: [],
|
||||
}
|
||||
|
||||
// Note: The current implementation of resetDependentFields for mode changes
|
||||
// looks for fields not in the current mode, but the logic compares against previous mode
|
||||
const newState = resetDependentFields(state, 'mode', schema)
|
||||
|
||||
// Mode-specific field reset logic is handled
|
||||
expect(newState.mode).toBe('direct')
|
||||
})
|
||||
|
||||
test('should not modify state for fields without dependencies', () => {
|
||||
const schema = createSchemaForReset()
|
||||
const state: ConnectState = {
|
||||
const state = resolveState(schema, {
|
||||
mode: 'framework',
|
||||
framework: 'nextjs',
|
||||
}
|
||||
|
||||
const newState = resetDependentFields(state, 'framework', schema)
|
||||
framework: 'react',
|
||||
variant: 'app',
|
||||
})
|
||||
|
||||
expect(newState.mode).toBe('framework')
|
||||
expect(newState.framework).toBe('nextjs')
|
||||
expect(state.variant).toBe('vite')
|
||||
})
|
||||
})
|
||||
@@ -10,22 +10,6 @@ import type {
|
||||
StepTree,
|
||||
} from './Connect.types'
|
||||
|
||||
/**
|
||||
* The order in which state keys are checked during conditional value resolution.
|
||||
* Used for ConditionalValue (value-keyed) resolution, not for step trees.
|
||||
*/
|
||||
const STATE_KEY_ORDER = [
|
||||
'mode',
|
||||
'framework',
|
||||
'frameworkVariant',
|
||||
'library',
|
||||
'frameworkUi',
|
||||
'orm',
|
||||
'connectionMethod',
|
||||
'connectionType',
|
||||
'mcpClient',
|
||||
] as const
|
||||
|
||||
/**
|
||||
* Check if a value is a conditional object (has nested state keys or DEFAULT)
|
||||
*/
|
||||
@@ -38,7 +22,7 @@ function isConditionalObject(value: unknown): value is Record<string, unknown> {
|
||||
* Walks the tree using stateKeys in order, falling back to DEFAULT at each level.
|
||||
*
|
||||
* Example: Given state { mode: 'mcp', mcpClient: 'codex' }
|
||||
* and stateKeys ['mode', 'framework', ..., 'mcpClient']
|
||||
* and stateKeys derived from schema field order
|
||||
*
|
||||
* 1. Look up 'mcp' (state.mode value) in tree -> found, continue
|
||||
* 2. At mcp subtree { codex: [...], DEFAULT: [...] }, skip irrelevant keys
|
||||
@@ -49,7 +33,7 @@ function isConditionalObject(value: unknown): value is Record<string, unknown> {
|
||||
export function resolveConditional<T>(
|
||||
value: ConditionalValue<T>,
|
||||
state: ConnectState,
|
||||
stateKeys: readonly string[] = STATE_KEY_ORDER
|
||||
stateKeys: readonly string[] = Object.keys(state)
|
||||
): T | undefined {
|
||||
// Base case: we've reached a leaf value (string, array, null, boolean, etc.)
|
||||
if (!isConditionalObject(value)) {
|
||||
@@ -87,10 +71,11 @@ export function resolveConditional<T>(
|
||||
export function resolveSteps(schema: ConnectSchema, state: ConnectState): ResolvedStep[] {
|
||||
const steps = resolveStepTree(schema.steps, state)
|
||||
if (steps.length === 0) return []
|
||||
const stateKeys = Object.keys(schema.fields)
|
||||
|
||||
return steps
|
||||
.map((step) => {
|
||||
const content = resolveConditional<string | null>(step.content, state)
|
||||
const content = resolveConditional<string | null>(step.content, state, stateKeys)
|
||||
return {
|
||||
id: step.id,
|
||||
title: step.title,
|
||||
@@ -144,10 +129,9 @@ function resolveStepBranch(
|
||||
* Gets the active fields for the current mode, filtering by dependsOn conditions.
|
||||
*/
|
||||
export function getActiveFields(schema: ConnectSchema, state: ConnectState): ResolvedField[] {
|
||||
const currentMode = schema.modes.find((m) => m.id === state.mode)
|
||||
if (!currentMode) return []
|
||||
const stateKeys = Object.keys(schema.fields)
|
||||
|
||||
return currentMode.fields
|
||||
return stateKeys
|
||||
.map((fieldId) => schema.fields[fieldId])
|
||||
.filter((field): field is NonNullable<typeof field> => !!field)
|
||||
.filter((field) => {
|
||||
@@ -160,14 +144,18 @@ export function getActiveFields(schema: ConnectSchema, state: ConnectState): Res
|
||||
})
|
||||
.map((field) => ({
|
||||
...field,
|
||||
resolvedOptions: resolveFieldOptions(field, state),
|
||||
resolvedOptions: resolveFieldOptions(field, state, stateKeys),
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves field options based on current state.
|
||||
*/
|
||||
function resolveFieldOptions(field: { options?: unknown }, state: ConnectState): FieldOption[] {
|
||||
function resolveFieldOptions(
|
||||
field: { options?: unknown },
|
||||
state: ConnectState,
|
||||
stateKeys: readonly string[]
|
||||
): FieldOption[] {
|
||||
if (!field.options) return []
|
||||
|
||||
// Static options array
|
||||
@@ -175,80 +163,118 @@ function resolveFieldOptions(field: { options?: unknown }, state: ConnectState):
|
||||
return field.options
|
||||
}
|
||||
|
||||
// Reference to data source (handled elsewhere)
|
||||
if (
|
||||
typeof field.options === 'object' &&
|
||||
'source' in field.options &&
|
||||
typeof field.options.source === 'string'
|
||||
) {
|
||||
// This will be resolved by the component using getFieldOptionsFromSource
|
||||
return []
|
||||
if (typeof field.options === 'function') {
|
||||
return (field.options as (state: ConnectState) => FieldOption[])(state)
|
||||
}
|
||||
|
||||
// Conditional options
|
||||
const resolved = resolveConditional<FieldOption[]>(
|
||||
field.options as ConditionalValue<FieldOption[]>,
|
||||
state
|
||||
)
|
||||
return resolved ?? []
|
||||
if (typeof field.options === 'object') {
|
||||
const resolved = resolveConditional<FieldOption[]>(
|
||||
field.options as ConditionalValue<FieldOption[]>,
|
||||
state,
|
||||
stateKeys
|
||||
)
|
||||
return resolved ?? []
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes state values based on schema defaults, options, and dependencies.
|
||||
*/
|
||||
export function resolveState(
|
||||
schema: ConnectSchema,
|
||||
inputState: Partial<ConnectState>
|
||||
): ConnectState {
|
||||
const next: ConnectState = { ...(inputState as ConnectState) }
|
||||
|
||||
const maxIterations = Math.max(1, Object.keys(schema.fields).length + 1)
|
||||
|
||||
for (let iteration = 0; iteration < maxIterations; iteration++) {
|
||||
let changed = false
|
||||
const activeFields = getActiveFields(schema, next)
|
||||
|
||||
for (const field of activeFields) {
|
||||
const currentValue = next[field.id]
|
||||
const optionValues = field.resolvedOptions.map((option) => option.value)
|
||||
const hasOptions = optionValues.length > 0
|
||||
|
||||
if (field.type === 'switch') {
|
||||
if (typeof currentValue !== 'boolean' && typeof field.defaultValue === 'boolean') {
|
||||
next[field.id] = field.defaultValue
|
||||
changed = true
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if (field.type === 'multi-select') {
|
||||
if (Array.isArray(currentValue)) {
|
||||
if (hasOptions) {
|
||||
const filtered = currentValue.filter((value) => optionValues.includes(String(value)))
|
||||
if (filtered.length !== currentValue.length) {
|
||||
next[field.id] = filtered
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
} else if (Array.isArray(field.defaultValue)) {
|
||||
next[field.id] = field.defaultValue
|
||||
changed = true
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if (typeof currentValue !== 'string') {
|
||||
let nextValue: string | undefined
|
||||
|
||||
if (
|
||||
typeof field.defaultValue === 'string' &&
|
||||
(!hasOptions || optionValues.includes(field.defaultValue))
|
||||
) {
|
||||
nextValue = field.defaultValue
|
||||
} else if (hasOptions) {
|
||||
nextValue = optionValues[0]
|
||||
}
|
||||
|
||||
if (nextValue !== undefined) {
|
||||
next[field.id] = nextValue
|
||||
changed = true
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if (hasOptions && !optionValues.includes(currentValue)) {
|
||||
let nextValue: string | undefined
|
||||
|
||||
if (typeof field.defaultValue === 'string' && optionValues.includes(field.defaultValue)) {
|
||||
nextValue = field.defaultValue
|
||||
} else {
|
||||
nextValue = optionValues[0]
|
||||
}
|
||||
|
||||
if (nextValue !== currentValue) {
|
||||
next[field.id] = nextValue
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!changed) break
|
||||
}
|
||||
|
||||
const activeIds = new Set(getActiveFields(schema, next).map((field) => field.id))
|
||||
Object.keys(schema.fields).forEach((fieldId) => {
|
||||
if (!activeIds.has(fieldId)) {
|
||||
delete next[fieldId]
|
||||
}
|
||||
})
|
||||
|
||||
return next
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets default state for the schema, using first mode and default field values.
|
||||
*/
|
||||
export function getDefaultState(schema: ConnectSchema): ConnectState {
|
||||
const defaultMode = schema.modes[0]?.id ?? 'direct'
|
||||
|
||||
const state: ConnectState = { mode: defaultMode }
|
||||
|
||||
// Set default values for all fields
|
||||
Object.values(schema.fields).forEach((field) => {
|
||||
if (field.defaultValue !== undefined) {
|
||||
state[field.id] = field.defaultValue
|
||||
}
|
||||
})
|
||||
|
||||
return state
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets dependent fields when a parent field changes.
|
||||
* For example, changing framework should reset frameworkVariant.
|
||||
*/
|
||||
export function resetDependentFields(
|
||||
state: ConnectState,
|
||||
changedFieldId: string,
|
||||
schema: ConnectSchema
|
||||
): ConnectState {
|
||||
const newState = { ...state }
|
||||
|
||||
// Find fields that depend on the changed field
|
||||
Object.values(schema.fields).forEach((field) => {
|
||||
if (field.dependsOn && changedFieldId in field.dependsOn) {
|
||||
// Only reset if dependency conditions are no longer satisfied
|
||||
const dependencySatisfied = Object.entries(field.dependsOn).every(([key, values]) => {
|
||||
const stateValue = String(newState[key] ?? '')
|
||||
return values.includes(stateValue)
|
||||
})
|
||||
|
||||
if (!dependencySatisfied) {
|
||||
delete newState[field.id]
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Special case: changing mode resets all mode-specific fields
|
||||
if (changedFieldId === 'mode') {
|
||||
const previousMode = schema.modes.find((m) => m.id !== state.mode)
|
||||
const currentMode = schema.modes.find((m) => m.id === state.mode)
|
||||
|
||||
// Reset fields from previous mode that aren't in current mode
|
||||
previousMode?.fields.forEach((fieldId) => {
|
||||
if (!currentMode?.fields.includes(fieldId)) {
|
||||
delete newState[fieldId]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return newState
|
||||
return resolveState(schema, {})
|
||||
}
|
||||
@@ -1,60 +1,22 @@
|
||||
import { describe, test, expect } from 'vitest'
|
||||
import { connectSchema, INSTALL_COMMANDS } from './connect.schema'
|
||||
import { resolveSteps } from './connect.resolver'
|
||||
import { describe, expect, test } from 'vitest'
|
||||
|
||||
import { INSTALL_COMMANDS } from './Connect.constants'
|
||||
import type { ConnectState } from './Connect.types'
|
||||
import { resolveSteps } from './connect.resolver'
|
||||
import { connectSchema } from './connect.schema'
|
||||
|
||||
// ============================================================================
|
||||
// Schema Structure Tests
|
||||
// ============================================================================
|
||||
|
||||
describe('connect.schema:structure', () => {
|
||||
test('should have all required modes', () => {
|
||||
const modeIds = connectSchema.modes.map((m) => m.id)
|
||||
expect(modeIds).toEqual(['framework'])
|
||||
})
|
||||
|
||||
test('each mode should have required properties', () => {
|
||||
connectSchema.modes.forEach((mode) => {
|
||||
expect(mode.id).toBeDefined()
|
||||
expect(mode.label).toBeDefined()
|
||||
expect(mode.description).toBeDefined()
|
||||
expect(mode.fields).toBeDefined()
|
||||
expect(Array.isArray(mode.fields)).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
test('framework mode should have correct fields', () => {
|
||||
const frameworkMode = connectSchema.modes.find((m) => m.id === 'framework')
|
||||
expect(frameworkMode?.fields).toContain('framework')
|
||||
expect(frameworkMode?.fields).toContain('frameworkVariant')
|
||||
expect(frameworkMode?.fields).toContain('library')
|
||||
expect(frameworkMode?.fields).toContain('frameworkUi')
|
||||
})
|
||||
|
||||
test('direct mode should be removed', () => {
|
||||
const directMode = connectSchema.modes.find((m) => m.id === 'direct')
|
||||
expect(directMode).toBeUndefined()
|
||||
})
|
||||
|
||||
test('orm mode should be removed', () => {
|
||||
const ormMode = connectSchema.modes.find((m) => m.id === 'orm')
|
||||
expect(ormMode).toBeUndefined()
|
||||
})
|
||||
|
||||
test('mcp mode should be removed', () => {
|
||||
const mcpMode = connectSchema.modes.find((m) => m.id === 'mcp')
|
||||
expect(mcpMode).toBeUndefined()
|
||||
})
|
||||
|
||||
test('all mode fields should exist in fields definition', () => {
|
||||
connectSchema.modes.forEach((mode) => {
|
||||
mode.fields.forEach((fieldId) => {
|
||||
expect(
|
||||
connectSchema.fields[fieldId],
|
||||
`Field "${fieldId}" in mode "${mode.id}" should exist in fields definition`
|
||||
).toBeDefined()
|
||||
})
|
||||
})
|
||||
test('should define a mode field', () => {
|
||||
const field = connectSchema.fields.mode
|
||||
expect(field).toBeDefined()
|
||||
expect(field.type).toBe('radio-list')
|
||||
expect(field.defaultValue).toBe('framework')
|
||||
const options = Array.isArray(field.options) ? field.options : []
|
||||
expect(options.some((option) => option.value === 'framework')).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -65,21 +27,25 @@ describe('connect.schema:structure', () => {
|
||||
describe('connect.schema:fields', () => {
|
||||
test('framework field should have correct type', () => {
|
||||
const field = connectSchema.fields.framework
|
||||
expect(field.type).toBe('radio-grid')
|
||||
expect(field.options).toEqual({ source: 'frameworks' })
|
||||
expect(field.type).toBe('select')
|
||||
expect(Array.isArray(field.options)).toBe(true)
|
||||
const options = Array.isArray(field.options) ? field.options : []
|
||||
expect(options.some((option) => option.value === 'nextjs')).toBe(true)
|
||||
expect(field.defaultValue).toBe('nextjs')
|
||||
expect(field.dependsOn).toEqual({ mode: ['framework'] })
|
||||
})
|
||||
|
||||
test('frameworkVariant field should depend on framework', () => {
|
||||
const field = connectSchema.fields.frameworkVariant
|
||||
expect(field.dependsOn).toEqual({ framework: ['nextjs', 'react'] })
|
||||
expect(field.dependsOn).toEqual({ mode: ['framework'], framework: ['nextjs', 'react'] })
|
||||
expect(typeof field.options).toBe('function')
|
||||
})
|
||||
|
||||
test('frameworkUi field should be a switch type', () => {
|
||||
const field = connectSchema.fields.frameworkUi
|
||||
expect(field.type).toBe('switch')
|
||||
expect(field.defaultValue).toBe(false)
|
||||
expect(field.dependsOn).toEqual({ framework: ['nextjs', 'react'] })
|
||||
expect(field.dependsOn).toEqual({ mode: ['framework'], framework: ['nextjs', 'react'] })
|
||||
})
|
||||
|
||||
test('connectionMethod field should be removed', () => {
|
||||
|
||||
@@ -1,15 +1,63 @@
|
||||
import type { ConnectSchema, StepDefinition } from './Connect.types'
|
||||
import { FRAMEWORKS, MOBILES } from './Connect.constants'
|
||||
import type { ConnectSchema, ConnectState, FieldOption, StepDefinition } from './Connect.types'
|
||||
|
||||
/**
|
||||
* Install commands for different packages
|
||||
*/
|
||||
export const INSTALL_COMMANDS: Record<string, string> = {
|
||||
supabasejs: 'npm install @supabase/supabase-js',
|
||||
supabasepy: 'pip install supabase',
|
||||
supabaseflutter: 'flutter pub add supabase_flutter',
|
||||
supabaseswift:
|
||||
'swift package add-dependency https://github.com/supabase-community/supabase-swift',
|
||||
supabasekt: 'implementation("io.github.jan-tennert.supabase:supabase-kt:VERSION")',
|
||||
const frameworkOptions: FieldOption[] = [...FRAMEWORKS, ...MOBILES].map((framework) => ({
|
||||
value: framework.key,
|
||||
label: framework.label,
|
||||
icon: framework.icon,
|
||||
}))
|
||||
|
||||
const modeOptions: FieldOption[] = [
|
||||
{
|
||||
value: 'framework',
|
||||
label: 'Framework',
|
||||
description: 'Use a client library',
|
||||
},
|
||||
]
|
||||
|
||||
const getFrameworkVariantOptions = (state: ConnectState): FieldOption[] => {
|
||||
const allFrameworks = [...FRAMEWORKS, ...MOBILES]
|
||||
const selected = allFrameworks.find((framework) => framework.key === state.framework)
|
||||
if (!selected?.children?.length) return []
|
||||
if (selected.children.length <= 1) return []
|
||||
|
||||
return selected.children.map((variant) => ({
|
||||
value: variant.key,
|
||||
label: variant.label,
|
||||
icon: variant.icon,
|
||||
}))
|
||||
}
|
||||
|
||||
const getLibraryOptions = (state: ConnectState): FieldOption[] => {
|
||||
const allFrameworks = [...FRAMEWORKS, ...MOBILES]
|
||||
const selectedFramework = allFrameworks.find((framework) => framework.key === state.framework)
|
||||
if (!selectedFramework) return []
|
||||
|
||||
if (selectedFramework.children?.length > 1 && state.frameworkVariant) {
|
||||
const variant = selectedFramework.children.find((child) => child.key === state.frameworkVariant)
|
||||
if (variant?.children?.length) {
|
||||
return variant.children.map((child) => ({
|
||||
value: child.key,
|
||||
label: child.label,
|
||||
icon: child.icon,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedFramework.children?.length === 1) {
|
||||
const child = selectedFramework.children[0]
|
||||
if (child.children?.length) {
|
||||
return child.children.map((library) => ({
|
||||
value: library.key,
|
||||
label: library.label,
|
||||
icon: library.icon,
|
||||
}))
|
||||
}
|
||||
|
||||
return [{ value: child.key, label: child.label, icon: child.icon }]
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
@@ -73,44 +121,41 @@ const skillsInstallStep: StepDefinition = {
|
||||
// ============================================================================
|
||||
|
||||
export const connectSchema: ConnectSchema = {
|
||||
// -------------------------------------------------------------------------
|
||||
// Mode Definitions
|
||||
// -------------------------------------------------------------------------
|
||||
modes: [
|
||||
{
|
||||
id: 'framework',
|
||||
label: 'Framework',
|
||||
description: 'Use a client library',
|
||||
fields: ['framework', 'frameworkVariant', 'library', 'frameworkUi'],
|
||||
},
|
||||
],
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Field Definitions
|
||||
// -------------------------------------------------------------------------
|
||||
fields: {
|
||||
mode: {
|
||||
id: 'mode',
|
||||
type: 'radio-list',
|
||||
label: 'Mode',
|
||||
options: modeOptions,
|
||||
defaultValue: 'framework',
|
||||
},
|
||||
// Framework fields
|
||||
framework: {
|
||||
id: 'framework',
|
||||
type: 'radio-grid',
|
||||
type: 'select',
|
||||
label: 'Framework',
|
||||
options: { source: 'frameworks' },
|
||||
options: frameworkOptions,
|
||||
defaultValue: 'nextjs',
|
||||
dependsOn: { mode: ['framework'] },
|
||||
},
|
||||
frameworkVariant: {
|
||||
id: 'frameworkVariant',
|
||||
type: 'select',
|
||||
label: 'Variant',
|
||||
options: { source: 'frameworkVariants' },
|
||||
options: getFrameworkVariantOptions,
|
||||
defaultValue: 'app',
|
||||
dependsOn: { framework: ['nextjs', 'react'] }, // Only show for frameworks with multiple variants
|
||||
dependsOn: { mode: ['framework'], framework: ['nextjs', 'react'] }, // Only show for frameworks with multiple variants
|
||||
},
|
||||
library: {
|
||||
id: 'library',
|
||||
type: 'select',
|
||||
label: 'Library',
|
||||
options: { source: 'libraries' },
|
||||
options: getLibraryOptions,
|
||||
defaultValue: 'supabasejs',
|
||||
dependsOn: { mode: ['framework'] },
|
||||
},
|
||||
frameworkUi: {
|
||||
id: 'frameworkUi',
|
||||
@@ -118,7 +163,7 @@ export const connectSchema: ConnectSchema = {
|
||||
label: 'Shadcn',
|
||||
description: 'Install components via the Supabase shadcn registry.',
|
||||
defaultValue: false,
|
||||
dependsOn: { framework: ['nextjs', 'react'] },
|
||||
dependsOn: { mode: ['framework'], framework: ['nextjs', 'react'] },
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
import { MultipleCodeBlock } from 'ui-patterns/MultipleCodeBlock'
|
||||
|
||||
import type { ContentFileProps } from '@/components/interfaces/ConnectSheet/Connect.types'
|
||||
import type { StepContentProps } from '@/components/interfaces/ConnectSheet/Connect.types'
|
||||
|
||||
const ContentFile = ({ projectKeys }: ContentFileProps) => {
|
||||
const ContentFile = ({ projectKeys }: StepContentProps) => {
|
||||
const files = [
|
||||
{
|
||||
name: 'MainActivity.kt',
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { MultipleCodeBlock } from 'ui-patterns/MultipleCodeBlock'
|
||||
|
||||
import type { ContentFileProps } from '@/components/interfaces/ConnectSheet/Connect.types'
|
||||
import type { StepContentProps } from '@/components/interfaces/ConnectSheet/Connect.types'
|
||||
|
||||
const ContentFile = ({ projectKeys }: ContentFileProps) => {
|
||||
const ContentFile = ({ projectKeys }: StepContentProps) => {
|
||||
const files = [
|
||||
{
|
||||
name: '.env.local',
|
||||
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
import { MultipleCodeBlock } from 'ui-patterns/MultipleCodeBlock'
|
||||
|
||||
import type { ContentFileProps } from '@/components/interfaces/ConnectSheet/Connect.types'
|
||||
import type { StepContentProps } from '@/components/interfaces/ConnectSheet/Connect.types'
|
||||
|
||||
const ContentFile = ({ projectKeys }: ContentFileProps) => {
|
||||
const ContentFile = ({ projectKeys }: StepContentProps) => {
|
||||
const files = [
|
||||
{
|
||||
name: '.env.local',
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { MultipleCodeBlock } from 'ui-patterns/MultipleCodeBlock'
|
||||
|
||||
import type { ContentFileProps } from '@/components/interfaces/ConnectSheet/Connect.types'
|
||||
import type { StepContentProps } from '@/components/interfaces/ConnectSheet/Connect.types'
|
||||
|
||||
const ContentFile = ({ projectKeys }: ContentFileProps) => {
|
||||
const ContentFile = ({ projectKeys }: StepContentProps) => {
|
||||
const files = [
|
||||
{
|
||||
name: '.env',
|
||||
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
import { MultipleCodeBlock } from 'ui-patterns/MultipleCodeBlock'
|
||||
|
||||
import type { ContentFileProps } from '@/components/interfaces/ConnectSheet/Connect.types'
|
||||
import type { StepContentProps } from '@/components/interfaces/ConnectSheet/Connect.types'
|
||||
|
||||
const ContentFile = ({ projectKeys }: ContentFileProps) => {
|
||||
const ContentFile = ({ projectKeys }: StepContentProps) => {
|
||||
const files = [
|
||||
{
|
||||
name: 'lib/main.dart',
|
||||
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
import { MultipleCodeBlock } from 'ui-patterns/MultipleCodeBlock'
|
||||
|
||||
import type { ContentFileProps } from '@/components/interfaces/ConnectSheet/Connect.types'
|
||||
import type { StepContentProps } from '@/components/interfaces/ConnectSheet/Connect.types'
|
||||
|
||||
const ContentFile = ({ projectKeys }: ContentFileProps) => {
|
||||
const ContentFile = ({ projectKeys }: StepContentProps) => {
|
||||
const files = [
|
||||
{
|
||||
name: 'environments/environment.ts',
|
||||
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
import { MultipleCodeBlock } from 'ui-patterns/MultipleCodeBlock'
|
||||
|
||||
import type { ContentFileProps } from '@/components/interfaces/ConnectSheet/Connect.types'
|
||||
import type { StepContentProps } from '@/components/interfaces/ConnectSheet/Connect.types'
|
||||
|
||||
const ContentFile = ({ projectKeys }: ContentFileProps) => {
|
||||
const ContentFile = ({ projectKeys }: StepContentProps) => {
|
||||
const files = [
|
||||
{
|
||||
name: '.env',
|
||||
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
import { MultipleCodeBlock } from 'ui-patterns/MultipleCodeBlock'
|
||||
|
||||
import type { ContentFileProps } from '@/components/interfaces/ConnectSheet/Connect.types'
|
||||
import type { StepContentProps } from '@/components/interfaces/ConnectSheet/Connect.types'
|
||||
|
||||
const ContentFile = ({ projectKeys }: ContentFileProps) => {
|
||||
const ContentFile = ({ projectKeys }: StepContentProps) => {
|
||||
const files = [
|
||||
{
|
||||
name: '.env.local',
|
||||
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
import { MultipleCodeBlock } from 'ui-patterns/MultipleCodeBlock'
|
||||
|
||||
import type { ContentFileProps } from '@/components/interfaces/ConnectSheet/Connect.types'
|
||||
import type { StepContentProps } from '@/components/interfaces/ConnectSheet/Connect.types'
|
||||
|
||||
const ContentFile = ({ projectKeys }: ContentFileProps) => {
|
||||
const ContentFile = ({ projectKeys }: StepContentProps) => {
|
||||
const files = [
|
||||
{
|
||||
name: '.env.local',
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { MultipleCodeBlock } from 'ui-patterns/MultipleCodeBlock'
|
||||
|
||||
import type { ContentFileProps } from '@/components/interfaces/ConnectSheet/Connect.types'
|
||||
import type { StepContentProps } from '@/components/interfaces/ConnectSheet/Connect.types'
|
||||
|
||||
const ContentFile = ({ projectKeys }: ContentFileProps) => {
|
||||
const ContentFile = ({ projectKeys }: StepContentProps) => {
|
||||
const files = [
|
||||
{
|
||||
name: '.env.local',
|
||||
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
import { MultipleCodeBlock } from 'ui-patterns/MultipleCodeBlock'
|
||||
|
||||
import type { ContentFileProps } from '@/components/interfaces/ConnectSheet/Connect.types'
|
||||
import type { StepContentProps } from '@/components/interfaces/ConnectSheet/Connect.types'
|
||||
|
||||
const ContentFile = ({ projectKeys }: ContentFileProps) => {
|
||||
const ContentFile = ({ projectKeys }: StepContentProps) => {
|
||||
const files = [
|
||||
{
|
||||
name: '.env.local',
|
||||
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
import { MultipleCodeBlock } from 'ui-patterns/MultipleCodeBlock'
|
||||
|
||||
import type { ContentFileProps } from '@/components/interfaces/ConnectSheet/Connect.types'
|
||||
import type { StepContentProps } from '@/components/interfaces/ConnectSheet/Connect.types'
|
||||
|
||||
const ContentFile = ({ projectKeys }: ContentFileProps) => {
|
||||
const ContentFile = ({ projectKeys }: StepContentProps) => {
|
||||
const files = [
|
||||
{
|
||||
name: '.env',
|
||||
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
import { MultipleCodeBlock } from 'ui-patterns/MultipleCodeBlock'
|
||||
|
||||
import type { ContentFileProps } from '@/components/interfaces/ConnectSheet/Connect.types'
|
||||
import type { StepContentProps } from '@/components/interfaces/ConnectSheet/Connect.types'
|
||||
|
||||
const ContentFile = ({ projectKeys }: ContentFileProps) => {
|
||||
const ContentFile = ({ projectKeys }: StepContentProps) => {
|
||||
const files = [
|
||||
{
|
||||
name: '.env.local',
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { MultipleCodeBlock } from 'ui-patterns/MultipleCodeBlock'
|
||||
|
||||
import type { ContentFileProps } from '@/components/interfaces/ConnectSheet/Connect.types'
|
||||
import type { StepContentProps } from '@/components/interfaces/ConnectSheet/Connect.types'
|
||||
|
||||
const ContentFile = ({ projectKeys }: ContentFileProps) => {
|
||||
const ContentFile = ({ projectKeys }: StepContentProps) => {
|
||||
const files = [
|
||||
{
|
||||
name: '.env',
|
||||
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
import { MultipleCodeBlock } from 'ui-patterns/MultipleCodeBlock'
|
||||
|
||||
import type { ContentFileProps } from '@/components/interfaces/ConnectSheet/Connect.types'
|
||||
import type { StepContentProps } from '@/components/interfaces/ConnectSheet/Connect.types'
|
||||
|
||||
const ContentFile = ({ projectKeys }: ContentFileProps) => {
|
||||
const ContentFile = ({ projectKeys }: StepContentProps) => {
|
||||
const files = [
|
||||
{
|
||||
name: '.env.local',
|
||||
|
||||
@@ -2,15 +2,14 @@ import { Copy } from 'lucide-react'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { Button, copyToClipboard } from 'ui'
|
||||
|
||||
import { INSTALL_COMMANDS } from '../../../Connect.constants'
|
||||
import type { StepContentProps } from '../../../Connect.types'
|
||||
import { resolveFrameworkLibraryKey } from '../../../Connect.utils'
|
||||
import { INSTALL_COMMANDS } from '../../../connect.schema'
|
||||
|
||||
/**
|
||||
* Gets the install command for the current framework selection.
|
||||
*/
|
||||
function getInstallCommand(state: StepContentProps['state']): string | null {
|
||||
const libraryKey = resolveFrameworkLibraryKey(state)
|
||||
const libraryKey = typeof state.library === 'string' ? state.library : null
|
||||
|
||||
if (libraryKey && INSTALL_COMMANDS[libraryKey]) return INSTALL_COMMANDS[libraryKey]
|
||||
|
||||
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
import { MultipleCodeBlock } from 'ui-patterns/MultipleCodeBlock'
|
||||
|
||||
import type { ContentFileProps } from '@/components/interfaces/ConnectSheet/Connect.types'
|
||||
import type { StepContentProps } from '@/components/interfaces/ConnectSheet/Connect.types'
|
||||
|
||||
const ContentFile = ({ projectKeys }: ContentFileProps) => {
|
||||
const ContentFile = ({ projectKeys }: StepContentProps) => {
|
||||
const files = [
|
||||
{
|
||||
name: '.env.local',
|
||||
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
import { MultipleCodeBlock } from 'ui-patterns/MultipleCodeBlock'
|
||||
|
||||
import type { ContentFileProps } from '@/components/interfaces/ConnectSheet/Connect.types'
|
||||
import type { StepContentProps } from '@/components/interfaces/ConnectSheet/Connect.types'
|
||||
|
||||
const ContentFile = ({ projectKeys }: ContentFileProps) => {
|
||||
const ContentFile = ({ projectKeys }: StepContentProps) => {
|
||||
const files = [
|
||||
{
|
||||
name: 'Supabase.swift',
|
||||
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
import { MultipleCodeBlock } from 'ui-patterns/MultipleCodeBlock'
|
||||
|
||||
import type { ContentFileProps } from '@/components/interfaces/ConnectSheet/Connect.types'
|
||||
import type { StepContentProps } from '@/components/interfaces/ConnectSheet/Connect.types'
|
||||
|
||||
const ContentFile = ({ projectKeys }: ContentFileProps) => {
|
||||
const ContentFile = ({ projectKeys }: StepContentProps) => {
|
||||
const files = [
|
||||
{
|
||||
name: '.env',
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { MultipleCodeBlock } from 'ui-patterns/MultipleCodeBlock'
|
||||
|
||||
import type { ContentFileProps } from '@/components/interfaces/ConnectSheet/Connect.types'
|
||||
import type { StepContentProps } from '@/components/interfaces/ConnectSheet/Connect.types'
|
||||
|
||||
const ContentFile = ({ projectKeys }: ContentFileProps) => {
|
||||
const ContentFile = ({ projectKeys }: StepContentProps) => {
|
||||
const files = [
|
||||
{
|
||||
name: '.env.local',
|
||||
|
||||
@@ -44,28 +44,6 @@ describe('useConnectState', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// ============================================================================
|
||||
// Mode Switching Tests
|
||||
// ============================================================================
|
||||
|
||||
describe('setMode', () => {
|
||||
test('should preserve framework state when setting framework mode', () => {
|
||||
const { result } = renderHook(() => useConnectState())
|
||||
|
||||
// Change framework
|
||||
act(() => {
|
||||
result.current.updateField('framework', 'react')
|
||||
})
|
||||
|
||||
// Set framework mode again
|
||||
act(() => {
|
||||
result.current.setMode('framework')
|
||||
})
|
||||
|
||||
expect(result.current.state.framework).toBe('react')
|
||||
})
|
||||
})
|
||||
|
||||
// ============================================================================
|
||||
// Field Update Tests
|
||||
// ============================================================================
|
||||
@@ -205,25 +183,15 @@ describe('useConnectState', () => {
|
||||
// ============================================================================
|
||||
|
||||
describe('getFieldOptions', () => {
|
||||
test('should return framework options', () => {
|
||||
test('should return framework options from schema', () => {
|
||||
const { result } = renderHook(() => useConnectState())
|
||||
|
||||
const options = result.current.getFieldOptions('framework')
|
||||
expect(options.length).toBeGreaterThan(0)
|
||||
expect(options.some((o) => o.value === 'nextjs')).toBe(true)
|
||||
expect(options.some((o) => o.value === 'react')).toBe(true)
|
||||
})
|
||||
|
||||
test('should return variant options for nextjs', () => {
|
||||
const { result } = renderHook(() => useConnectState({ framework: 'nextjs' }))
|
||||
|
||||
const options = result.current.getFieldOptions('frameworkVariant')
|
||||
expect(options.length).toBeGreaterThan(0)
|
||||
expect(options.some((o) => o.value === 'app')).toBe(true)
|
||||
expect(options.some((o) => o.value === 'pages')).toBe(true)
|
||||
})
|
||||
|
||||
test('should return empty variant options for frameworks without variants', () => {
|
||||
test('should return empty array for inactive field', () => {
|
||||
const { result } = renderHook(() => useConnectState({ framework: 'remix' }))
|
||||
|
||||
const options = result.current.getFieldOptions('frameworkVariant')
|
||||
@@ -236,15 +204,6 @@ describe('useConnectState', () => {
|
||||
const options = result.current.getFieldOptions('unknownField')
|
||||
expect(options).toEqual([])
|
||||
})
|
||||
|
||||
test('should return library options for selected framework', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useConnectState({ framework: 'nextjs', frameworkVariant: 'app' })
|
||||
)
|
||||
|
||||
const options = result.current.getFieldOptions('library')
|
||||
expect(options.length).toBeGreaterThan(0)
|
||||
})
|
||||
})
|
||||
|
||||
// ============================================================================
|
||||
@@ -256,16 +215,14 @@ describe('useConnectState', () => {
|
||||
const { result } = renderHook(() => useConnectState())
|
||||
|
||||
expect(result.current.schema).toBeDefined()
|
||||
expect(result.current.schema.modes).toBeDefined()
|
||||
expect(result.current.schema.fields).toBeDefined()
|
||||
expect(result.current.schema.steps).toBeDefined()
|
||||
})
|
||||
|
||||
test('should have all expected modes in schema', () => {
|
||||
test('should include mode field in schema', () => {
|
||||
const { result } = renderHook(() => useConnectState())
|
||||
|
||||
const modeIds = result.current.schema.modes.map((m) => m.id)
|
||||
expect(modeIds).toEqual(['framework'])
|
||||
expect(result.current.schema.fields.mode).toBeDefined()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,153 +1,13 @@
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
|
||||
import { FEATURE_GROUPS_PLATFORM, MCP_CLIENTS } from 'ui-patterns/McpUrlBuilder'
|
||||
import {
|
||||
connectionStringMethodOptions,
|
||||
DATABASE_CONNECTION_TYPES,
|
||||
FRAMEWORKS,
|
||||
MOBILES,
|
||||
ORMS,
|
||||
} from './Connect.constants'
|
||||
import type {
|
||||
ConnectMode,
|
||||
ConnectSchema,
|
||||
ConnectState,
|
||||
FieldOption,
|
||||
ResolvedField,
|
||||
ResolvedStep,
|
||||
} from './Connect.types'
|
||||
import { resolveFrameworkLibraryKey } from './Connect.utils'
|
||||
import type { ConnectSchema, ConnectState, FieldOption, ResolvedField, ResolvedStep } from './Connect.types'
|
||||
import {
|
||||
getActiveFields,
|
||||
getDefaultState,
|
||||
resetDependentFields,
|
||||
resolveState,
|
||||
resolveSteps,
|
||||
} from './connect.resolver'
|
||||
import { connectSchema } from './connect.schema'
|
||||
|
||||
// ============================================================================
|
||||
// Data Source Helpers
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Get field options from a data source reference.
|
||||
* This maps source names to actual data.
|
||||
*/
|
||||
function getFieldOptionsFromSource(source: string, state: ConnectState): FieldOption[] {
|
||||
switch (source) {
|
||||
case 'frameworks':
|
||||
return [...FRAMEWORKS, ...MOBILES].map((f) => ({
|
||||
value: f.key,
|
||||
label: f.label,
|
||||
icon: f.icon,
|
||||
}))
|
||||
|
||||
case 'frameworkVariants': {
|
||||
// Get variants for the selected framework
|
||||
const allFrameworks = [...FRAMEWORKS, ...MOBILES]
|
||||
const selected = allFrameworks.find((f) => f.key === state.framework)
|
||||
if (!selected?.children?.length) return []
|
||||
// Only return if there are multiple children (variants)
|
||||
if (selected.children.length <= 1) return []
|
||||
return selected.children.map((c) => ({
|
||||
value: c.key,
|
||||
label: c.label,
|
||||
icon: c.icon,
|
||||
}))
|
||||
}
|
||||
|
||||
case 'libraries': {
|
||||
// Get libraries for the selected framework and variant
|
||||
const allFrameworks = [...FRAMEWORKS, ...MOBILES]
|
||||
const selectedFramework = allFrameworks.find((f) => f.key === state.framework)
|
||||
if (!selectedFramework) return []
|
||||
|
||||
// If framework has variants, look in the variant
|
||||
if (selectedFramework.children?.length > 1 && state.frameworkVariant) {
|
||||
const variant = selectedFramework.children.find((c) => c.key === state.frameworkVariant)
|
||||
if (variant?.children?.length) {
|
||||
return variant.children.map((c) => ({
|
||||
value: c.key,
|
||||
label: c.label,
|
||||
icon: c.icon,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise look directly in framework children
|
||||
if (selectedFramework.children?.length === 1) {
|
||||
const child = selectedFramework.children[0]
|
||||
if (child.children?.length) {
|
||||
return child.children.map((c) => ({
|
||||
value: c.key,
|
||||
label: c.label,
|
||||
icon: c.icon,
|
||||
}))
|
||||
}
|
||||
// The child itself is the library
|
||||
return [{ value: child.key, label: child.label, icon: child.icon }]
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
case 'connectionMethods':
|
||||
return Object.values(connectionStringMethodOptions).map((m) => ({
|
||||
value: m.value,
|
||||
label: m.label,
|
||||
description: m.description,
|
||||
}))
|
||||
|
||||
case 'connectionTypes':
|
||||
return DATABASE_CONNECTION_TYPES.map((t) => ({
|
||||
value: t.id,
|
||||
label: t.label,
|
||||
}))
|
||||
|
||||
case 'orms':
|
||||
return ORMS.map((o) => ({
|
||||
value: o.key,
|
||||
label: o.label,
|
||||
icon: o.icon,
|
||||
}))
|
||||
|
||||
case 'mcpClients':
|
||||
return MCP_CLIENTS.map((c) => ({
|
||||
value: c.key,
|
||||
label: c.label,
|
||||
icon: c.icon,
|
||||
}))
|
||||
|
||||
case 'mcpFeatures':
|
||||
return FEATURE_GROUPS_PLATFORM.map((f) => ({
|
||||
value: f.id,
|
||||
label: f.name,
|
||||
description: f.description,
|
||||
}))
|
||||
|
||||
default:
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve field options, handling both static options and data source references.
|
||||
*/
|
||||
function resolveFieldOptionsWithSource(field: ResolvedField, state: ConnectState): FieldOption[] {
|
||||
// If already resolved (from conditional resolution)
|
||||
if (field.resolvedOptions.length > 0) {
|
||||
return field.resolvedOptions
|
||||
}
|
||||
|
||||
// Check if it's a source reference
|
||||
const options = connectSchema.fields[field.id]?.options
|
||||
if (options && typeof options === 'object' && 'source' in options) {
|
||||
return getFieldOptionsFromSource(options.source as string, state)
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Hook
|
||||
// ============================================================================
|
||||
@@ -155,7 +15,6 @@ function resolveFieldOptionsWithSource(field: ResolvedField, state: ConnectState
|
||||
export interface UseConnectStateReturn {
|
||||
state: ConnectState
|
||||
updateField: (fieldId: string, value: string | boolean | string[]) => void
|
||||
setMode: (mode: ConnectMode) => void
|
||||
activeFields: ResolvedField[]
|
||||
resolvedSteps: ResolvedStep[]
|
||||
getFieldOptions: (fieldId: string) => FieldOption[]
|
||||
@@ -164,124 +23,13 @@ export interface UseConnectStateReturn {
|
||||
|
||||
export function useConnectState(initialState?: Partial<ConnectState>): UseConnectStateReturn {
|
||||
const [state, setState] = useState<ConnectState>(() => {
|
||||
const defaults = getDefaultState(connectSchema)
|
||||
|
||||
// Set initial framework if mode is framework
|
||||
if (defaults.mode === 'framework' && !defaults.framework) {
|
||||
const firstFramework = FRAMEWORKS[0]
|
||||
defaults.framework = firstFramework?.key ?? ''
|
||||
|
||||
// Set initial variant if framework has variants
|
||||
if (firstFramework?.children?.length > 1) {
|
||||
defaults.frameworkVariant = firstFramework.children[0]?.key ?? ''
|
||||
}
|
||||
|
||||
// Set initial library
|
||||
const libraryKey = resolveFrameworkLibraryKey({
|
||||
framework: defaults.framework,
|
||||
frameworkVariant: defaults.frameworkVariant,
|
||||
library: defaults.library,
|
||||
})
|
||||
if (libraryKey) defaults.library = libraryKey
|
||||
}
|
||||
|
||||
// Set initial ORM if mode is orm
|
||||
if (defaults.mode === 'orm' && !defaults.orm) {
|
||||
defaults.orm = ORMS[0]?.key ?? ''
|
||||
}
|
||||
|
||||
// Set initial MCP client if mode is mcp
|
||||
if (defaults.mode === 'mcp' && !defaults.mcpClient) {
|
||||
defaults.mcpClient = MCP_CLIENTS[0]?.key ?? ''
|
||||
}
|
||||
|
||||
return { ...defaults, ...initialState } as ConnectState
|
||||
return resolveState(connectSchema, initialState ?? {})
|
||||
})
|
||||
|
||||
const updateField = useCallback((fieldId: string, value: string | boolean | string[]) => {
|
||||
setState((prev) => {
|
||||
const next = { ...prev, [fieldId]: value }
|
||||
|
||||
// Handle cascading updates for framework selection
|
||||
if (fieldId === 'framework') {
|
||||
const allFrameworks = [...FRAMEWORKS, ...MOBILES]
|
||||
const selected = allFrameworks.find((f) => f.key === value)
|
||||
|
||||
// Reset variant if framework changed
|
||||
if (selected?.children && selected.children.length > 1) {
|
||||
next.frameworkVariant = selected.children[0]?.key ?? ''
|
||||
} else {
|
||||
delete next.frameworkVariant
|
||||
}
|
||||
|
||||
// Reset library
|
||||
delete next.library
|
||||
const libraryKey = resolveFrameworkLibraryKey({
|
||||
framework: next.framework,
|
||||
frameworkVariant: next.frameworkVariant,
|
||||
library: next.library,
|
||||
})
|
||||
if (libraryKey) {
|
||||
next.library = libraryKey
|
||||
} else {
|
||||
delete next.library
|
||||
}
|
||||
}
|
||||
|
||||
// Handle cascading updates for variant selection
|
||||
if (fieldId === 'frameworkVariant') {
|
||||
delete next.library
|
||||
const libraryKey = resolveFrameworkLibraryKey({
|
||||
framework: next.framework,
|
||||
frameworkVariant: String(value),
|
||||
library: next.library,
|
||||
})
|
||||
if (libraryKey) next.library = libraryKey
|
||||
}
|
||||
|
||||
// Reset useSharedPooler when connectionMethod changes to 'direct'
|
||||
if (fieldId === 'connectionMethod' && value === 'direct') {
|
||||
next.useSharedPooler = false
|
||||
}
|
||||
|
||||
return resetDependentFields(next, fieldId, connectSchema)
|
||||
})
|
||||
}, [])
|
||||
|
||||
const setMode = useCallback((mode: ConnectMode) => {
|
||||
setState((prev) => {
|
||||
const next: ConnectState = { ...prev, mode }
|
||||
|
||||
// Initialize mode-specific defaults
|
||||
if (mode === 'framework' && !next.framework) {
|
||||
const firstFramework = FRAMEWORKS[0]
|
||||
next.framework = firstFramework?.key ?? ''
|
||||
if (firstFramework?.children?.length > 1) {
|
||||
next.frameworkVariant = firstFramework.children[0]?.key ?? ''
|
||||
}
|
||||
delete next.library
|
||||
const libraryKey = resolveFrameworkLibraryKey({
|
||||
framework: next.framework,
|
||||
frameworkVariant: next.frameworkVariant,
|
||||
library: next.library,
|
||||
})
|
||||
if (libraryKey) next.library = libraryKey
|
||||
}
|
||||
|
||||
if (mode === 'direct') {
|
||||
next.connectionMethod = next.connectionMethod ?? 'direct'
|
||||
next.connectionType = next.connectionType ?? 'uri'
|
||||
}
|
||||
|
||||
if (mode === 'orm' && !next.orm) {
|
||||
next.orm = ORMS[0]?.key ?? ''
|
||||
}
|
||||
|
||||
if (mode === 'mcp' && !next.mcpClient) {
|
||||
next.mcpClient = MCP_CLIENTS[0]?.key ?? ''
|
||||
}
|
||||
|
||||
return next
|
||||
return resolveState(connectSchema, next)
|
||||
})
|
||||
}, [])
|
||||
|
||||
@@ -293,15 +41,14 @@ export function useConnectState(initialState?: Partial<ConnectState>): UseConnec
|
||||
(fieldId: string): FieldOption[] => {
|
||||
const field = activeFields.find((f) => f.id === fieldId)
|
||||
if (!field) return []
|
||||
return resolveFieldOptionsWithSource(field, state)
|
||||
return field.resolvedOptions
|
||||
},
|
||||
[activeFields, state]
|
||||
[activeFields]
|
||||
)
|
||||
|
||||
return {
|
||||
state,
|
||||
updateField,
|
||||
setMode,
|
||||
activeFields,
|
||||
resolvedSteps,
|
||||
getFieldOptions,
|
||||
|
||||
Reference in new issue
Block a user