mirror of
https://github.com/supabase/supabase.git
synced 2026-10-12 21:05:06 +03:00
Simplify table quickstart UX by consolidating variants into single view
- Restore simple templates variant with category tabs (no dropdown navigation) - Add AI variant with direct input field and quick idea chips - Remove complex multi-view navigation system - Delete 5 unnecessary component files (Initial/Category/AIInput/Results/Shared) - Remove ViewMode enum from types - Consolidate all logic into single TableTemplateSelector component
This commit is contained in:
1 parent
767e646f68
commit
d2c5ccd5ef
8 files changed
+209
-483
No files matched your search
+208
-132
@@ -1,18 +1,17 @@
|
||||
import { useState, useCallback, useRef, useEffect } from 'react'
|
||||
import { Columns3, Wand2 } from 'lucide-react'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
import { useAITableGeneration } from './useAITableGeneration'
|
||||
import { tableTemplates } from './templates'
|
||||
import { QuickstartVariant, ViewMode } from './types'
|
||||
import { convertTableSuggestionToTableField } from './utils'
|
||||
import { InitialView } from './components/InitialView'
|
||||
import { CategoryView } from './components/CategoryView'
|
||||
import { AIInputView } from './components/AIInputView'
|
||||
import { ResultsView } from './components/ResultsView'
|
||||
import type { TableSuggestion } from './types'
|
||||
import { Button, Input_Shadcn_ as Input, cn } from 'ui'
|
||||
import type { TableField } from '../TableEditor.types'
|
||||
import { AI_QUICK_IDEAS } from './constants'
|
||||
import { tableTemplates } from './templates'
|
||||
import type { TableSuggestion } from './types'
|
||||
import { QuickstartVariant } from './types'
|
||||
import { useAITableGeneration } from './useAITableGeneration'
|
||||
import { convertTableSuggestionToTableField } from './utils'
|
||||
|
||||
interface TableTemplateSelectorProps {
|
||||
variant: Exclude<QuickstartVariant, QuickstartVariant.CONTROL>
|
||||
variant: Exclude<QuickstartVariant, QuickstartVariant.CONTROL>
|
||||
onSelectTemplate: (tableField: Partial<TableField>) => void
|
||||
onDismiss?: () => void
|
||||
disabled?: boolean
|
||||
@@ -20,45 +19,44 @@ interface TableTemplateSelectorProps {
|
||||
|
||||
const SUCCESS_MESSAGE_DURATION_MS = 3000
|
||||
|
||||
interface ViewState {
|
||||
mode: ViewMode
|
||||
selectedCategory: string | null
|
||||
selectedTemplate: TableSuggestion | null
|
||||
generatedTables: TableSuggestion[]
|
||||
isLoading: boolean
|
||||
}
|
||||
|
||||
const initialViewState: ViewState = {
|
||||
mode: ViewMode.INITIAL,
|
||||
selectedCategory: null,
|
||||
selectedTemplate: null,
|
||||
generatedTables: [],
|
||||
isLoading: false,
|
||||
}
|
||||
|
||||
export const TableTemplateSelector = ({
|
||||
variant,
|
||||
onSelectTemplate,
|
||||
onDismiss,
|
||||
disabled,
|
||||
}: TableTemplateSelectorProps) => {
|
||||
const [viewState, setViewState] = useState<ViewState>(initialViewState)
|
||||
// Templates variant state
|
||||
const [activeCategory, setActiveCategory] = useState<string | null>(null)
|
||||
const [selectedTemplate, setSelectedTemplate] = useState<TableSuggestion | null>(null)
|
||||
|
||||
// AI variant state
|
||||
const [aiPrompt, setAiPrompt] = useState('')
|
||||
const [generatedTables, setGeneratedTables] = useState<TableSuggestion[]>([])
|
||||
const inputRef = useRef<HTMLInputElement>(null)
|
||||
const { generateTables, isGenerating, error: apiError } = useAITableGeneration()
|
||||
|
||||
// Focus management
|
||||
const isAI = variant === QuickstartVariant.AI
|
||||
const categories = useMemo(() => Object.keys(tableTemplates), [])
|
||||
|
||||
// Auto-select first category for templates variant
|
||||
useEffect(() => {
|
||||
if (viewState.mode === ViewMode.AI_INPUT && inputRef.current) {
|
||||
if (!isAI && activeCategory === null && categories.length > 0) {
|
||||
setActiveCategory(categories[0])
|
||||
}
|
||||
}, [categories, activeCategory, isAI])
|
||||
|
||||
// Focus AI input when in AI mode
|
||||
useEffect(() => {
|
||||
if (isAI && inputRef.current) {
|
||||
inputRef.current.focus()
|
||||
}
|
||||
}, [viewState.mode])
|
||||
}, [isAI])
|
||||
|
||||
const handleSelectTemplate = useCallback(
|
||||
(template: TableSuggestion) => {
|
||||
const tableField = convertTableSuggestionToTableField(template)
|
||||
onSelectTemplate(tableField)
|
||||
setViewState((prev) => ({ ...prev, selectedTemplate: template }))
|
||||
setSelectedTemplate(template)
|
||||
toast.success(
|
||||
`${template.tableName} template applied. You can add or modify the fields below.`,
|
||||
{
|
||||
@@ -69,122 +67,200 @@ export const TableTemplateSelector = ({
|
||||
[onSelectTemplate]
|
||||
)
|
||||
|
||||
const handleGenerateTables = useCallback(async (promptOverride?: string) => {
|
||||
const promptToUse = promptOverride ?? aiPrompt
|
||||
if (!promptToUse.trim() || isGenerating) return
|
||||
const handleGenerateTables = useCallback(
|
||||
async (promptOverride?: string) => {
|
||||
const promptToUse = promptOverride ?? aiPrompt
|
||||
if (!promptToUse.trim() || isGenerating) return
|
||||
|
||||
setViewState((prev) => ({ ...prev, isLoading: true }))
|
||||
try {
|
||||
const tables = await generateTables(promptToUse)
|
||||
|
||||
try {
|
||||
const tables = await generateTables(promptToUse)
|
||||
|
||||
if (tables.length > 0) {
|
||||
setViewState({
|
||||
mode: ViewMode.AI_RESULTS,
|
||||
selectedCategory: null,
|
||||
selectedTemplate: tables[0],
|
||||
generatedTables: tables,
|
||||
isLoading: false,
|
||||
})
|
||||
handleSelectTemplate(tables[0])
|
||||
} else {
|
||||
setViewState((prev) => ({
|
||||
...prev,
|
||||
isLoading: false,
|
||||
}))
|
||||
if (tables.length > 0) {
|
||||
setGeneratedTables(tables)
|
||||
handleSelectTemplate(tables[0])
|
||||
}
|
||||
} catch (error) {
|
||||
// Error handling is done in the hook
|
||||
}
|
||||
} catch (error) {
|
||||
setViewState((prev) => ({
|
||||
...prev,
|
||||
isLoading: false,
|
||||
}))
|
||||
}
|
||||
}, [aiPrompt, generateTables, isGenerating, handleSelectTemplate])
|
||||
},
|
||||
[aiPrompt, generateTables, isGenerating, handleSelectTemplate]
|
||||
)
|
||||
|
||||
const handleQuickIdea = useCallback(
|
||||
(idea: string) => {
|
||||
setAiPrompt(idea)
|
||||
setViewState((prev) => ({ ...prev, mode: ViewMode.AI_INPUT }))
|
||||
handleGenerateTables(idea)
|
||||
},
|
||||
[handleGenerateTables]
|
||||
)
|
||||
|
||||
const handleCategorySelect = useCallback((category: string) => {
|
||||
setViewState((prev) => ({
|
||||
...prev,
|
||||
mode: ViewMode.CATEGORY_SELECTED,
|
||||
selectedCategory: category,
|
||||
}))
|
||||
}, [])
|
||||
const displayedTemplates = useMemo(() => {
|
||||
if (isAI) {
|
||||
return generatedTables
|
||||
}
|
||||
return activeCategory ? tableTemplates[activeCategory] || [] : []
|
||||
}, [isAI, generatedTables, activeCategory])
|
||||
|
||||
const handleAISelect = useCallback(() => {
|
||||
setViewState((prev) => ({ ...prev, mode: ViewMode.AI_INPUT }))
|
||||
}, [])
|
||||
return (
|
||||
<div className="rounded-lg border border-default bg-surface-75 p-4">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div>
|
||||
<h3 className="text-sm font-medium">
|
||||
{isAI ? 'Generate tables with AI' : 'Start faster with a table template'}
|
||||
</h3>
|
||||
<p className="text-xs text-foreground-lighter mt-1">
|
||||
{isAI
|
||||
? "Describe the app you're building and AI will create ready-made table schema."
|
||||
: 'Save time by starting from a ready-made table schema.'}
|
||||
</p>
|
||||
</div>
|
||||
{onDismiss && (
|
||||
<Button type="text" size="tiny" onClick={onDismiss}>
|
||||
Dismiss
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
const handleBack = useCallback(() => {
|
||||
setViewState(initialViewState)
|
||||
setAiPrompt('')
|
||||
}, [])
|
||||
{/* AI Variant: Input and Quick Ideas */}
|
||||
{isAI && (
|
||||
<div className="space-y-3">
|
||||
<div className="relative">
|
||||
<Input
|
||||
ref={inputRef}
|
||||
placeholder="Describe your app (e.g., 'recipe sharing app')"
|
||||
value={aiPrompt}
|
||||
onChange={(e) => setAiPrompt(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault()
|
||||
if (aiPrompt.trim()) {
|
||||
handleGenerateTables()
|
||||
}
|
||||
}
|
||||
}}
|
||||
disabled={isGenerating || disabled}
|
||||
aria-label="Table description for AI generation"
|
||||
className="pr-24"
|
||||
/>
|
||||
<Button
|
||||
type="default"
|
||||
size="tiny"
|
||||
disabled={!aiPrompt.trim() || isGenerating || disabled}
|
||||
onClick={() => handleGenerateTables()}
|
||||
loading={isGenerating}
|
||||
className="absolute right-1 top-1/2 -translate-y-1/2"
|
||||
>
|
||||
{isGenerating ? 'Generating...' : 'Generate'}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
const { mode, selectedCategory, selectedTemplate, generatedTables } = viewState
|
||||
{apiError && (
|
||||
<div className="text-sm text-red-600" role="alert">
|
||||
{apiError}
|
||||
</div>
|
||||
)}
|
||||
|
||||
// Initial view
|
||||
if (mode === ViewMode.INITIAL) {
|
||||
return (
|
||||
<InitialView
|
||||
variant={variant}
|
||||
disabled={disabled}
|
||||
onDismiss={onDismiss}
|
||||
onCategorySelect={handleCategorySelect}
|
||||
onAISelect={handleAISelect}
|
||||
onQuickIdea={handleQuickIdea}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{/* Quick Ideas */}
|
||||
{generatedTables.length === 0 && (
|
||||
<div>
|
||||
<div className="text-xs text-foreground-light mb-2">Quick ideas:</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{AI_QUICK_IDEAS.map((idea) => (
|
||||
<button
|
||||
key={idea}
|
||||
onClick={() => handleQuickIdea(idea)}
|
||||
disabled={isGenerating || disabled}
|
||||
className={cn(
|
||||
'px-2 py-1 rounded-md text-xs border border-default',
|
||||
'hover:border-foreground-muted hover:bg-surface-100',
|
||||
'disabled:opacity-50 disabled:cursor-not-allowed',
|
||||
'transition-all'
|
||||
)}
|
||||
>
|
||||
<Wand2 size={12} className="inline mr-1" />
|
||||
{idea}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
// Category selection view
|
||||
if (mode === ViewMode.CATEGORY_SELECTED && selectedCategory) {
|
||||
const templates = tableTemplates[selectedCategory] ?? []
|
||||
return (
|
||||
<CategoryView
|
||||
category={selectedCategory}
|
||||
templates={templates}
|
||||
selectedTemplate={selectedTemplate}
|
||||
onBack={handleBack}
|
||||
onSelectTemplate={handleSelectTemplate}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{/* Templates Variant: Category Tabs */}
|
||||
{!isAI && (
|
||||
<div className="flex flex-wrap gap-2 mb-3">
|
||||
{categories.map((category) => (
|
||||
<button
|
||||
key={category}
|
||||
onClick={() => setActiveCategory(category)}
|
||||
disabled={disabled}
|
||||
className={cn(
|
||||
'px-2 py-1 rounded-md text-xs capitalize border',
|
||||
activeCategory === category
|
||||
? 'border-foreground bg-surface-200'
|
||||
: 'border-default hover:border-foreground-muted hover:bg-surface-100',
|
||||
'disabled:opacity-50 disabled:cursor-not-allowed'
|
||||
)}
|
||||
>
|
||||
{category}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
// AI input view
|
||||
if (mode === ViewMode.AI_INPUT) {
|
||||
return (
|
||||
<AIInputView
|
||||
prompt={aiPrompt}
|
||||
error={apiError}
|
||||
isGenerating={isGenerating}
|
||||
isLoading={viewState.isLoading}
|
||||
inputRef={inputRef}
|
||||
onBack={handleBack}
|
||||
onPromptChange={setAiPrompt}
|
||||
onGenerate={handleGenerateTables}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
// AI results view
|
||||
if (mode === ViewMode.AI_RESULTS && generatedTables.length > 0) {
|
||||
return (
|
||||
<ResultsView
|
||||
tables={generatedTables}
|
||||
selectedTemplate={selectedTemplate}
|
||||
title="AI Generated Tables"
|
||||
onBack={handleBack}
|
||||
onSelectTemplate={handleSelectTemplate}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
{/* Templates List (both variants use this) */}
|
||||
{displayedTemplates.length > 0 && (
|
||||
<div className="grid gap-2">
|
||||
{isAI && (
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<span className="text-xs text-foreground-light">
|
||||
Generated {generatedTables.length} table{generatedTables.length !== 1 ? 's' : ''}
|
||||
</span>
|
||||
<Button
|
||||
type="text"
|
||||
size="tiny"
|
||||
onClick={() => {
|
||||
setGeneratedTables([])
|
||||
setAiPrompt('')
|
||||
if (inputRef.current) inputRef.current.focus()
|
||||
}}
|
||||
disabled={isGenerating}
|
||||
>
|
||||
Generate new
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{displayedTemplates.map((template) => (
|
||||
<button
|
||||
key={`${isAI ? 'ai' : activeCategory}:${template.tableName}`}
|
||||
onClick={() => handleSelectTemplate(template)}
|
||||
disabled={disabled}
|
||||
className={cn(
|
||||
'text-left p-3 rounded-md border transition-all w-full',
|
||||
selectedTemplate?.tableName === template.tableName
|
||||
? 'border-foreground bg-surface-200'
|
||||
: 'border-default hover:border-foreground-muted hover:bg-surface-100',
|
||||
'disabled:opacity-50 disabled:cursor-not-allowed'
|
||||
)}
|
||||
>
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
<div className="text-sm font-medium font-mono">{template.tableName}</div>
|
||||
{template.rationale && (
|
||||
<div className="text-sm text-foreground-light mt-1">{template.rationale}</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-1 text-sm text-foreground-muted ml-3">
|
||||
<Columns3 size={14} aria-hidden="true" />
|
||||
<span>{template.fields.length}</span>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
-74
@@ -1,74 +0,0 @@
|
||||
import { memo } from 'react'
|
||||
import { Button, Input_Shadcn_ as Input } from 'ui'
|
||||
import { BackButton } from './SharedComponents'
|
||||
|
||||
interface AIInputViewProps {
|
||||
prompt: string
|
||||
error: string | null
|
||||
isGenerating: boolean
|
||||
isLoading: boolean
|
||||
inputRef: React.RefObject<HTMLInputElement>
|
||||
onBack: () => void
|
||||
onPromptChange: (value: string) => void
|
||||
onGenerate: () => void
|
||||
}
|
||||
|
||||
export const AIInputView = memo(
|
||||
({
|
||||
prompt,
|
||||
error,
|
||||
isGenerating,
|
||||
isLoading,
|
||||
inputRef,
|
||||
onBack,
|
||||
onPromptChange,
|
||||
onGenerate,
|
||||
}: AIInputViewProps) => (
|
||||
<div className="space-y-2">
|
||||
<div className="flex gap-2">
|
||||
<BackButton onClick={onBack} disabled={isGenerating} />
|
||||
</div>
|
||||
{error && (
|
||||
<div className="text-sm text-red-600" role="alert">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
<div className="relative">
|
||||
<Input
|
||||
ref={inputRef}
|
||||
placeholder="Describe your table (e.g., 'user profiles with social features')"
|
||||
value={prompt}
|
||||
onChange={(e) => onPromptChange(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault()
|
||||
if (prompt.trim()) {
|
||||
onGenerate()
|
||||
}
|
||||
}
|
||||
}}
|
||||
disabled={isGenerating || isLoading}
|
||||
aria-label="Table description for AI generation"
|
||||
aria-describedby="ai-prompt-help"
|
||||
className="pr-24"
|
||||
/>
|
||||
<Button
|
||||
type="default"
|
||||
size="tiny"
|
||||
disabled={!prompt.trim() || isGenerating || isLoading}
|
||||
onClick={onGenerate}
|
||||
loading={isGenerating || isLoading}
|
||||
className="absolute right-1 top-1/2 -translate-y-1/2"
|
||||
aria-label={isGenerating ? 'Generating tables' : 'Generate tables'}
|
||||
>
|
||||
{isGenerating || isLoading ? 'Generating...' : 'Generate'}
|
||||
</Button>
|
||||
</div>
|
||||
<span id="ai-prompt-help" className="sr-only">
|
||||
Enter a description of your tables and press Enter or click Generate
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
)
|
||||
|
||||
AIInputView.displayName = 'AIInputView'
|
||||
-42
@@ -1,42 +0,0 @@
|
||||
import { memo } from 'react'
|
||||
import type { TableSuggestion } from '../types'
|
||||
import { BackButton, TemplateList } from './SharedComponents'
|
||||
|
||||
interface CategoryViewProps {
|
||||
category: string
|
||||
templates: TableSuggestion[]
|
||||
selectedTemplate: TableSuggestion | null
|
||||
onBack: () => void
|
||||
onSelectTemplate: (template: TableSuggestion) => void
|
||||
}
|
||||
|
||||
export const CategoryView = memo(
|
||||
({ category, templates, selectedTemplate, onBack, onSelectTemplate }: CategoryViewProps) => {
|
||||
if (templates.length === 0) {
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<BackButton onClick={onBack} />
|
||||
<p className="text-sm text-foreground-lighter">
|
||||
No templates available for this category.
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<BackButton onClick={onBack} />
|
||||
<span className="text-xs text-foreground-light">{category}</span>
|
||||
</div>
|
||||
<TemplateList
|
||||
templates={templates}
|
||||
selectedTemplate={selectedTemplate}
|
||||
onSelectTemplate={onSelectTemplate}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
CategoryView.displayName = 'CategoryView'
|
||||
-87
@@ -1,87 +0,0 @@
|
||||
import { memo } from 'react'
|
||||
import { Wand2, ChevronDown, Database } from 'lucide-react'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
Button,
|
||||
} from 'ui'
|
||||
import { QuickstartVariant } from '../types'
|
||||
import { tableTemplates } from '../templates'
|
||||
import { AI_QUICK_IDEAS } from '../constants'
|
||||
import { SelectorHeader } from './SharedComponents'
|
||||
|
||||
interface InitialViewProps {
|
||||
variant: Exclude<QuickstartVariant, QuickstartVariant.CONTROL>
|
||||
disabled?: boolean
|
||||
onDismiss?: () => void
|
||||
onCategorySelect: (category: string) => void
|
||||
onAISelect: () => void
|
||||
onQuickIdea: (idea: string) => void
|
||||
}
|
||||
|
||||
export const InitialView = memo(
|
||||
({
|
||||
variant,
|
||||
disabled,
|
||||
onDismiss,
|
||||
onCategorySelect,
|
||||
onAISelect,
|
||||
onQuickIdea,
|
||||
}: InitialViewProps) => {
|
||||
const isAI = variant === QuickstartVariant.AI
|
||||
const icon = isAI ? <Wand2 size={14} /> : <Database size={14} />
|
||||
const label = isAI ? 'Generate with AI' : 'Select from templates'
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<SelectorHeader onDismiss={onDismiss} />
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
type="default"
|
||||
size="small"
|
||||
disabled={disabled}
|
||||
className="w-full justify-between"
|
||||
iconRight={<ChevronDown size={16} />}
|
||||
aria-label={label}
|
||||
>
|
||||
<span className="flex items-center gap-2">
|
||||
{icon}
|
||||
{label}
|
||||
</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" className="w-[320px]">
|
||||
{isAI ? (
|
||||
<>
|
||||
<DropdownMenuItem onClick={onAISelect}>
|
||||
<Wand2 size={14} className="mr-2" aria-hidden="true" />
|
||||
Generate with AI...
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<div className="px-2 py-1.5 text-xs text-foreground-light">Quick ideas</div>
|
||||
{AI_QUICK_IDEAS.map((example) => (
|
||||
<DropdownMenuItem key={example} onClick={() => onQuickIdea(example)}>
|
||||
{example}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</>
|
||||
) : (
|
||||
Object.keys(tableTemplates).map((category) => (
|
||||
<DropdownMenuItem key={category} onClick={() => onCategorySelect(category)}>
|
||||
<Database size={14} className="mr-2" aria-hidden="true" />
|
||||
{category}
|
||||
</DropdownMenuItem>
|
||||
))
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
InitialView.displayName = 'InitialView'
|
||||
-31
@@ -1,31 +0,0 @@
|
||||
import { memo } from 'react'
|
||||
import type { TableSuggestion } from '../types'
|
||||
import { BackButton, TemplateList } from './SharedComponents'
|
||||
|
||||
interface ResultsViewProps {
|
||||
tables: TableSuggestion[]
|
||||
selectedTemplate: TableSuggestion | null
|
||||
title: string
|
||||
onBack: () => void
|
||||
onSelectTemplate: (template: TableSuggestion) => void
|
||||
}
|
||||
|
||||
export const ResultsView = memo(
|
||||
({ tables, selectedTemplate, title, onBack, onSelectTemplate }: ResultsViewProps) => (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<BackButton onClick={onBack} label="Generate new" />
|
||||
<span className="text-xs text-foreground-light">{title}</span>
|
||||
</div>
|
||||
<div aria-live="polite">
|
||||
<TemplateList
|
||||
templates={tables}
|
||||
selectedTemplate={selectedTemplate}
|
||||
onSelectTemplate={onSelectTemplate}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
)
|
||||
|
||||
ResultsView.displayName = 'ResultsView'
|
||||
-110
@@ -1,110 +0,0 @@
|
||||
import { memo } from 'react'
|
||||
import { ArrowLeft, Columns3, X } from 'lucide-react'
|
||||
import { Button, cn } from 'ui'
|
||||
import type { TableSuggestion } from '../types'
|
||||
|
||||
export const BackButton = memo(
|
||||
({
|
||||
onClick,
|
||||
disabled = false,
|
||||
label = 'Back',
|
||||
}: {
|
||||
onClick: () => void
|
||||
disabled?: boolean
|
||||
label?: string
|
||||
}) => (
|
||||
<Button
|
||||
type="text"
|
||||
size="tiny"
|
||||
icon={<ArrowLeft size={14} />}
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
aria-label={`${label} to previous view`}
|
||||
>
|
||||
{label}
|
||||
</Button>
|
||||
)
|
||||
)
|
||||
BackButton.displayName = 'BackButton'
|
||||
|
||||
export const SelectorHeader = memo(({ onDismiss }: { onDismiss?: () => void }) => (
|
||||
<div className="flex items-center justify-between">
|
||||
<label className="text-sm text-foreground-light">Start from template (optional)</label>
|
||||
{onDismiss && (
|
||||
<Button
|
||||
type="text"
|
||||
size="tiny"
|
||||
icon={<X size={14} />}
|
||||
onClick={onDismiss}
|
||||
className="text-foreground-lighter hover:text-foreground"
|
||||
aria-label="Dismiss template selector"
|
||||
>
|
||||
Dismiss
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
SelectorHeader.displayName = 'SelectorHeader'
|
||||
|
||||
export const TemplateItem = memo(
|
||||
({
|
||||
template,
|
||||
isSelected,
|
||||
onClick,
|
||||
}: {
|
||||
template: TableSuggestion
|
||||
isSelected: boolean
|
||||
onClick: () => void
|
||||
}) => (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
className={cn(
|
||||
'text-left p-3 rounded-md border transition-all w-full',
|
||||
isSelected
|
||||
? 'border-foreground bg-surface-200'
|
||||
: 'border-default hover:border-foreground-muted hover:bg-surface-100'
|
||||
)}
|
||||
data-selected={isSelected}
|
||||
aria-label={`Select ${template.tableName} table template`}
|
||||
>
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
<div className="text-sm font-medium font-mono">{template.tableName}</div>
|
||||
{template.rationale && (
|
||||
<div className="text-sm text-foreground-light mt-1">{template.rationale}</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-1 text-sm text-foreground-muted ml-3">
|
||||
<Columns3 size={14} aria-hidden="true" />
|
||||
<span>{template.fields.length}</span>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
)
|
||||
)
|
||||
TemplateItem.displayName = 'TemplateItem'
|
||||
|
||||
export const TemplateList = memo(
|
||||
({
|
||||
templates,
|
||||
selectedTemplate,
|
||||
onSelectTemplate,
|
||||
}: {
|
||||
templates: TableSuggestion[]
|
||||
selectedTemplate: TableSuggestion | null
|
||||
onSelectTemplate: (template: TableSuggestion) => void
|
||||
}) => (
|
||||
<div className="grid gap-2" role="list">
|
||||
{templates.map((template) => (
|
||||
<TemplateItem
|
||||
key={template.tableName}
|
||||
template={template}
|
||||
isSelected={selectedTemplate?.tableName === template.tableName}
|
||||
onClick={() => onSelectTemplate(template)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
)
|
||||
TemplateList.displayName = 'TemplateList'
|
||||
-7
@@ -47,13 +47,6 @@ export enum QuickstartVariant {
|
||||
TEMPLATES = 'templates',
|
||||
}
|
||||
|
||||
export enum ViewMode {
|
||||
INITIAL = 'initial',
|
||||
AI_INPUT = 'ai-input',
|
||||
AI_RESULTS = 'ai-results',
|
||||
CATEGORY_SELECTED = 'category-selected',
|
||||
}
|
||||
|
||||
export type TableSuggestion = {
|
||||
tableName: string
|
||||
fields: TableField[]
|
||||
|
||||
@@ -15,6 +15,7 @@ const HOSTED_SUPPORTED_API_URLS = [
|
||||
'/ai/onboarding/design',
|
||||
'/ai/feedback/classify',
|
||||
'/ai/docs',
|
||||
'/ai/table-quickstart/generate-schemas',
|
||||
'/get-ip-address',
|
||||
'/get-utc-time',
|
||||
'/get-deployment-commit',
|
||||
|
||||
Reference in new issue
Block a user