mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-14 17:37:55 -05:00
improvement(settings): added sections to settings and improved layout
This commit is contained in:
@@ -0,0 +1,268 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from '@/components/ui/alert-dialog'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { useEnvironmentStore } from '@/stores/environment/store'
|
||||
import { EnvironmentVariable as StoreEnvironmentVariable } from '@/stores/environment/types'
|
||||
|
||||
// Extend the store type with our UI-specific fields
|
||||
interface UIEnvironmentVariable extends StoreEnvironmentVariable {
|
||||
id?: number
|
||||
}
|
||||
|
||||
interface EnvironmentVariablesProps {
|
||||
onOpenChange: (open: boolean) => void
|
||||
}
|
||||
|
||||
const GRID_COLS = 'grid grid-cols-[minmax(0,1fr),minmax(0,1fr),40px] gap-4'
|
||||
const INITIAL_ENV_VAR: UIEnvironmentVariable = { key: '', value: '' }
|
||||
|
||||
export function EnvironmentVariables({ onOpenChange }: EnvironmentVariablesProps) {
|
||||
const { variables, setVariable, removeVariable } = useEnvironmentStore()
|
||||
const [envVars, setEnvVars] = useState<UIEnvironmentVariable[]>([])
|
||||
const [focusedValueIndex, setFocusedValueIndex] = useState<number | null>(null)
|
||||
const [showUnsavedChanges, setShowUnsavedChanges] = useState(false)
|
||||
const scrollContainerRef = useRef<HTMLDivElement>(null)
|
||||
const pendingClose = useRef(false)
|
||||
const initialVarsRef = useRef<UIEnvironmentVariable[]>([])
|
||||
|
||||
// Check if there are unsaved changes by comparing with initial state
|
||||
const hasChanges = useMemo(() => {
|
||||
const initialVars = initialVarsRef.current.filter((v) => v.key || v.value)
|
||||
const currentVars = envVars.filter((v) => v.key || v.value)
|
||||
|
||||
const initialMap = new Map(initialVars.map((v) => [v.key, v.value]))
|
||||
const currentMap = new Map(currentVars.map((v) => [v.key, v.value]))
|
||||
|
||||
if (initialMap.size !== currentMap.size) return true
|
||||
|
||||
for (const [key, value] of currentMap) {
|
||||
const initialValue = initialMap.get(key)
|
||||
if (initialValue !== value) return true
|
||||
}
|
||||
|
||||
for (const key of initialMap.keys()) {
|
||||
if (!currentMap.has(key)) return true
|
||||
}
|
||||
|
||||
return false
|
||||
}, [envVars])
|
||||
|
||||
// Initialize environment variables
|
||||
useEffect(() => {
|
||||
const existingVars = Object.values(variables)
|
||||
const initialVars = existingVars.length ? existingVars : [INITIAL_ENV_VAR]
|
||||
initialVarsRef.current = JSON.parse(JSON.stringify(initialVars))
|
||||
setEnvVars(JSON.parse(JSON.stringify(initialVars)))
|
||||
pendingClose.current = false
|
||||
}, [variables])
|
||||
|
||||
const handleClose = () => {
|
||||
if (hasChanges) {
|
||||
setShowUnsavedChanges(true)
|
||||
pendingClose.current = true
|
||||
} else {
|
||||
onOpenChange(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleCancel = () => {
|
||||
setEnvVars(JSON.parse(JSON.stringify(initialVarsRef.current)))
|
||||
setShowUnsavedChanges(false)
|
||||
if (pendingClose.current) {
|
||||
onOpenChange(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (scrollContainerRef.current) {
|
||||
// Smooth scroll to bottom when new variables are added
|
||||
scrollContainerRef.current.scrollTo({
|
||||
top: scrollContainerRef.current.scrollHeight,
|
||||
behavior: 'smooth',
|
||||
})
|
||||
}
|
||||
}, [envVars.length]) // Only trigger on length changes
|
||||
|
||||
const handleValueFocus = (index: number, e: React.FocusEvent<HTMLInputElement>) => {
|
||||
setFocusedValueIndex(index)
|
||||
e.target.scrollLeft = 0
|
||||
}
|
||||
|
||||
const handleValueClick = (e: React.MouseEvent<HTMLInputElement>) => {
|
||||
e.preventDefault()
|
||||
e.currentTarget.scrollLeft = 0
|
||||
}
|
||||
|
||||
const handlePaste = (e: React.ClipboardEvent<HTMLInputElement>, index: number) => {
|
||||
const text = e.clipboardData.getData('text').trim()
|
||||
if (!text) return
|
||||
|
||||
const lines = text.split('\n').filter((line) => line.trim())
|
||||
if (lines.length === 0) return
|
||||
|
||||
e.preventDefault()
|
||||
|
||||
const inputType = (e.target as HTMLInputElement).getAttribute('data-input-type') as
|
||||
| 'key'
|
||||
| 'value'
|
||||
const containsKeyValuePair = text.includes('=')
|
||||
|
||||
if (inputType && !containsKeyValuePair) {
|
||||
handleSingleValuePaste(text, index, inputType)
|
||||
return
|
||||
}
|
||||
|
||||
handleKeyValuePaste(lines)
|
||||
}
|
||||
|
||||
const handleSingleValuePaste = (text: string, index: number, inputType: 'key' | 'value') => {
|
||||
const newEnvVars = [...envVars]
|
||||
newEnvVars[index][inputType] = text
|
||||
setEnvVars(newEnvVars)
|
||||
}
|
||||
|
||||
const handleKeyValuePaste = (lines: string[]) => {
|
||||
const parsedVars = lines
|
||||
.map((line) => {
|
||||
const [key, ...valueParts] = line.split('=')
|
||||
const value = valueParts.join('=').trim()
|
||||
return {
|
||||
key: key.trim(),
|
||||
value,
|
||||
id: Date.now() + Math.random(),
|
||||
}
|
||||
})
|
||||
.filter(({ key, value }) => key && value)
|
||||
|
||||
if (parsedVars.length > 0) {
|
||||
const existingVars = envVars.filter((v) => v.key || v.value)
|
||||
setEnvVars([...existingVars, ...parsedVars])
|
||||
}
|
||||
}
|
||||
|
||||
const addEnvVar = () => {
|
||||
const newVar = { key: '', value: '', id: Date.now() }
|
||||
setEnvVars([...envVars, newVar])
|
||||
}
|
||||
|
||||
const updateEnvVar = (index: number, field: 'key' | 'value', value: string) => {
|
||||
const newEnvVars = [...envVars]
|
||||
newEnvVars[index][field] = value
|
||||
setEnvVars(newEnvVars)
|
||||
}
|
||||
|
||||
const removeEnvVar = (index: number) => {
|
||||
const newEnvVars = envVars.filter((_, i) => i !== index)
|
||||
setEnvVars(newEnvVars.length ? newEnvVars : [INITIAL_ENV_VAR])
|
||||
}
|
||||
|
||||
const handleSave = () => {
|
||||
const validVars = envVars.filter((v) => v.key && v.value)
|
||||
validVars.forEach((v) => setVariable(v.key, v.value))
|
||||
|
||||
const currentKeys = new Set(validVars.map((v) => v.key))
|
||||
Object.keys(variables).forEach((key) => {
|
||||
if (!currentKeys.has(key)) {
|
||||
removeVariable(key)
|
||||
}
|
||||
})
|
||||
|
||||
setShowUnsavedChanges(false)
|
||||
onOpenChange(false)
|
||||
}
|
||||
|
||||
const renderEnvVarRow = (envVar: UIEnvironmentVariable, index: number) => (
|
||||
<div key={envVar.id || index} className={`${GRID_COLS} items-center`}>
|
||||
<Input
|
||||
data-input-type="key"
|
||||
value={envVar.key}
|
||||
onChange={(e) => updateEnvVar(index, 'key', e.target.value)}
|
||||
onPaste={(e) => handlePaste(e, index)}
|
||||
placeholder="e.g. API_KEY"
|
||||
/>
|
||||
<Input
|
||||
data-input-type="value"
|
||||
value={envVar.value}
|
||||
onChange={(e) => updateEnvVar(index, 'value', e.target.value)}
|
||||
type={focusedValueIndex === index ? 'text' : 'password'}
|
||||
onFocus={(e) => handleValueFocus(index, e)}
|
||||
onClick={handleValueClick}
|
||||
onBlur={() => setFocusedValueIndex(null)}
|
||||
onPaste={(e) => handlePaste(e, index)}
|
||||
placeholder="Enter value"
|
||||
className="allow-scroll"
|
||||
/>
|
||||
<Button variant="ghost" size="icon" onClick={() => removeEnvVar(index)} className="h-10 w-10">
|
||||
×
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
{/* Fixed Header */}
|
||||
<div className="px-6 pt-6">
|
||||
<h2 className="text-lg font-medium mb-4">Environment Variables</h2>
|
||||
<div className={`${GRID_COLS} px-0.5 mb-2`}>
|
||||
<Label>Key</Label>
|
||||
<Label>Value</Label>
|
||||
<div />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Scrollable Content */}
|
||||
<div
|
||||
ref={scrollContainerRef}
|
||||
className="flex-1 px-6 overflow-y-auto min-h-0 scrollbar-thin scrollbar-thumb-muted-foreground/20 hover:scrollbar-thumb-muted-foreground/25 scrollbar-track-transparent"
|
||||
>
|
||||
<div className="space-y-2 py-2">{envVars.map(renderEnvVarRow)}</div>
|
||||
</div>
|
||||
|
||||
{/* Fixed Footer */}
|
||||
<div className="px-6 pb-6 pt-4 border-t mt-auto">
|
||||
<div className="flex flex-col gap-4">
|
||||
<Button variant="outline" size="sm" onClick={addEnvVar}>
|
||||
Add Variable
|
||||
</Button>
|
||||
|
||||
<div className="flex justify-end space-x-2">
|
||||
<Button variant="outline" onClick={handleClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleSave} disabled={!hasChanges}>
|
||||
Save Changes
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<AlertDialog open={showUnsavedChanges} onOpenChange={setShowUnsavedChanges}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Unsaved Changes</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
You have unsaved changes. Do you want to save them before closing?
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel onClick={handleCancel}>Discard Changes</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={handleSave}>Save Changes</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Switch } from '@/components/ui/switch'
|
||||
|
||||
export function General() {
|
||||
return (
|
||||
<div className="p-6 space-y-6">
|
||||
<div>
|
||||
<h2 className="text-lg font-medium mb-4">General Settings</h2>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between py-1">
|
||||
<Label htmlFor="debug-mode" className="font-medium">
|
||||
Debug Mode
|
||||
</Label>
|
||||
<Switch id="debug-mode" />
|
||||
</div>
|
||||
<div className="flex items-center justify-between py-1">
|
||||
<Label htmlFor="auto-save" className="font-medium">
|
||||
Auto-save Workflows
|
||||
</Label>
|
||||
<Switch id="auto-save" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import { KeyRound, Settings } from 'lucide-react'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
interface SettingsNavigationProps {
|
||||
activeSection: string
|
||||
onSectionChange: (section: 'general' | 'environment') => void
|
||||
}
|
||||
|
||||
const navigationItems = [
|
||||
{
|
||||
id: 'general',
|
||||
label: 'General',
|
||||
icon: Settings,
|
||||
},
|
||||
{
|
||||
id: 'environment',
|
||||
label: 'Environment',
|
||||
icon: KeyRound,
|
||||
},
|
||||
] as const
|
||||
|
||||
export function SettingsNavigation({ activeSection, onSectionChange }: SettingsNavigationProps) {
|
||||
return (
|
||||
<div className="py-4">
|
||||
{navigationItems.map((item) => (
|
||||
<button
|
||||
key={item.id}
|
||||
onClick={() => onSectionChange(item.id)}
|
||||
className={cn(
|
||||
'w-full flex items-center gap-3 px-4 py-2.5 text-sm transition-colors',
|
||||
'hover:bg-accent/50',
|
||||
activeSection === item.id
|
||||
? 'bg-accent/50 text-foreground font-medium'
|
||||
: 'text-muted-foreground'
|
||||
)}
|
||||
>
|
||||
<item.icon className="h-4 w-4" />
|
||||
<span>{item.label}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,301 +1,59 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from '@/components/ui/alert-dialog'
|
||||
import { useState } from 'react'
|
||||
import { X } from 'lucide-react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { useEnvironmentStore } from '@/stores/environment/store'
|
||||
import { EnvironmentVariable as StoreEnvironmentVariable } from '@/stores/environment/types'
|
||||
|
||||
// Extend the store type with our UI-specific fields
|
||||
interface UIEnvironmentVariable extends StoreEnvironmentVariable {
|
||||
id?: number
|
||||
}
|
||||
import { cn } from '@/lib/utils'
|
||||
import { EnvironmentVariables } from './components/environment-variables/environment-variables'
|
||||
import { General } from './components/general/general'
|
||||
import { SettingsNavigation } from './components/settings-navigation/settings-navigation'
|
||||
|
||||
interface SettingsModalProps {
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
}
|
||||
|
||||
const GRID_COLS = 'grid grid-cols-[minmax(0,1fr),minmax(0,1fr),40px] gap-4'
|
||||
const INITIAL_ENV_VAR: UIEnvironmentVariable = { key: '', value: '' }
|
||||
type SettingsSection = 'general' | 'environment'
|
||||
|
||||
export function SettingsModal({ open, onOpenChange }: SettingsModalProps) {
|
||||
const { variables, setVariable, removeVariable } = useEnvironmentStore()
|
||||
const [envVars, setEnvVars] = useState<UIEnvironmentVariable[]>([])
|
||||
const [focusedValueIndex, setFocusedValueIndex] = useState<number | null>(null)
|
||||
const [showUnsavedChanges, setShowUnsavedChanges] = useState(false)
|
||||
const scrollContainerRef = useRef<HTMLDivElement>(null)
|
||||
const pendingClose = useRef(false)
|
||||
const initialVarsRef = useRef<UIEnvironmentVariable[]>([])
|
||||
|
||||
// Check if there are unsaved changes by comparing with initial state
|
||||
const hasChanges = useMemo(() => {
|
||||
// Filter out empty rows from both initial and current state
|
||||
const initialVars = initialVarsRef.current.filter((v) => v.key || v.value)
|
||||
const currentVars = envVars.filter((v) => v.key || v.value)
|
||||
|
||||
// Create maps for easier comparison
|
||||
const initialMap = new Map(initialVars.map((v) => [v.key, v.value]))
|
||||
const currentMap = new Map(currentVars.map((v) => [v.key, v.value]))
|
||||
|
||||
// Different number of non-empty variables
|
||||
if (initialMap.size !== currentMap.size) return true
|
||||
|
||||
// Check for any differences in keys or values
|
||||
for (const [key, value] of currentMap) {
|
||||
const initialValue = initialMap.get(key)
|
||||
// If key doesn't exist in initial or value is different
|
||||
if (initialValue !== value) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// Check if any initial keys are missing from current
|
||||
for (const key of initialMap.keys()) {
|
||||
if (!currentMap.has(key)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}, [envVars])
|
||||
|
||||
// Reset state when modal is opened
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
const existingVars = Object.values(variables)
|
||||
const initialVars = existingVars.length ? existingVars : [INITIAL_ENV_VAR]
|
||||
// Create deep copy of initial vars to prevent reference issues
|
||||
initialVarsRef.current = JSON.parse(JSON.stringify(initialVars))
|
||||
setEnvVars(JSON.parse(JSON.stringify(initialVars)))
|
||||
pendingClose.current = false
|
||||
}
|
||||
}, [open, variables])
|
||||
|
||||
const handleClose = () => {
|
||||
if (hasChanges) {
|
||||
setShowUnsavedChanges(true)
|
||||
pendingClose.current = true
|
||||
} else {
|
||||
onOpenChange(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleCancel = () => {
|
||||
// Reset to initial state when cancelling (without saving)
|
||||
setEnvVars(JSON.parse(JSON.stringify(initialVarsRef.current)))
|
||||
setShowUnsavedChanges(false)
|
||||
if (pendingClose.current) {
|
||||
onOpenChange(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (scrollContainerRef.current) {
|
||||
scrollContainerRef.current.scrollTop = scrollContainerRef.current.scrollHeight
|
||||
}
|
||||
}, [envVars])
|
||||
|
||||
const handleValueFocus = (index: number, e: React.FocusEvent<HTMLInputElement>) => {
|
||||
setFocusedValueIndex(index)
|
||||
// Always scroll to the start of the input
|
||||
e.target.scrollLeft = 0
|
||||
}
|
||||
|
||||
const handleValueClick = (e: React.MouseEvent<HTMLInputElement>) => {
|
||||
e.preventDefault()
|
||||
// Always scroll to the start of the input
|
||||
e.currentTarget.scrollLeft = 0
|
||||
}
|
||||
|
||||
const handleValueKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === 'ArrowLeft' || e.key === 'ArrowRight') {
|
||||
e.preventDefault()
|
||||
}
|
||||
}
|
||||
|
||||
const handlePaste = (e: React.ClipboardEvent<HTMLInputElement>, index: number) => {
|
||||
const text = e.clipboardData.getData('text').trim()
|
||||
if (!text) return
|
||||
|
||||
const lines = text.split('\n').filter((line) => line.trim())
|
||||
if (lines.length === 0) return
|
||||
|
||||
e.preventDefault()
|
||||
|
||||
const inputType = (e.target as HTMLInputElement).getAttribute('data-input-type') as
|
||||
| 'key'
|
||||
| 'value'
|
||||
const containsKeyValuePair = text.includes('=')
|
||||
|
||||
// Handle single value paste into specific field
|
||||
if (inputType && !containsKeyValuePair) {
|
||||
handleSingleValuePaste(text, index, inputType)
|
||||
return
|
||||
}
|
||||
|
||||
// Handle key-value pair(s) paste
|
||||
handleKeyValuePaste(lines)
|
||||
}
|
||||
|
||||
const handleSingleValuePaste = (text: string, index: number, inputType: 'key' | 'value') => {
|
||||
const newEnvVars = [...envVars]
|
||||
newEnvVars[index][inputType] = text
|
||||
setEnvVars(newEnvVars)
|
||||
}
|
||||
|
||||
const handleKeyValuePaste = (lines: string[]) => {
|
||||
const parsedVars = lines
|
||||
.map((line) => {
|
||||
const [key, ...valueParts] = line.split('=')
|
||||
const value = valueParts.join('=').trim()
|
||||
return {
|
||||
key: key.trim(),
|
||||
value,
|
||||
id: Date.now() + Math.random(),
|
||||
}
|
||||
})
|
||||
.filter(({ key, value }) => key && value)
|
||||
|
||||
if (parsedVars.length > 0) {
|
||||
// Merge existing vars with new ones, removing any empty rows at the end
|
||||
const existingVars = envVars.filter((v) => v.key || v.value)
|
||||
setEnvVars([...existingVars, ...parsedVars])
|
||||
}
|
||||
}
|
||||
|
||||
const addEnvVar = () => {
|
||||
// Create a fresh empty variable with a unique id
|
||||
const newVar = { key: '', value: '', id: Date.now() }
|
||||
setEnvVars([...envVars, newVar])
|
||||
}
|
||||
|
||||
const updateEnvVar = (index: number, field: 'key' | 'value', value: string) => {
|
||||
const newEnvVars = [...envVars]
|
||||
newEnvVars[index][field] = value
|
||||
setEnvVars(newEnvVars)
|
||||
}
|
||||
|
||||
const removeEnvVar = (index: number) => {
|
||||
const newEnvVars = envVars.filter((_, i) => i !== index)
|
||||
setEnvVars(newEnvVars.length ? newEnvVars : [INITIAL_ENV_VAR])
|
||||
}
|
||||
|
||||
const handleSave = () => {
|
||||
// Save all valid environment variables to the store
|
||||
const validVars = envVars.filter((v) => v.key && v.value)
|
||||
validVars.forEach((v) => setVariable(v.key, v.value))
|
||||
|
||||
// Remove any variables that were deleted
|
||||
const currentKeys = new Set(validVars.map((v) => v.key))
|
||||
Object.keys(variables).forEach((key) => {
|
||||
if (!currentKeys.has(key)) {
|
||||
removeVariable(key)
|
||||
}
|
||||
})
|
||||
|
||||
// Close both the alert dialog and the main modal
|
||||
setShowUnsavedChanges(false)
|
||||
onOpenChange(false)
|
||||
}
|
||||
|
||||
const renderEnvVarRow = (envVar: UIEnvironmentVariable, index: number) => (
|
||||
<div key={envVar.id || index} className={`${GRID_COLS} items-center`}>
|
||||
<Input
|
||||
data-input-type="key"
|
||||
value={envVar.key}
|
||||
onChange={(e) => updateEnvVar(index, 'key', e.target.value)}
|
||||
onPaste={(e) => handlePaste(e, index)}
|
||||
placeholder="e.g. API_KEY"
|
||||
/>
|
||||
<Input
|
||||
data-input-type="value"
|
||||
value={envVar.value}
|
||||
onChange={(e) => updateEnvVar(index, 'value', e.target.value)}
|
||||
type={focusedValueIndex === index ? 'text' : 'password'}
|
||||
onFocus={(e) => handleValueFocus(index, e)}
|
||||
onClick={handleValueClick}
|
||||
onBlur={() => setFocusedValueIndex(null)}
|
||||
onPaste={(e) => handlePaste(e, index)}
|
||||
placeholder="Enter value"
|
||||
className="allow-scroll"
|
||||
/>
|
||||
<Button variant="ghost" size="icon" onClick={() => removeEnvVar(index)} className="h-10 w-10">
|
||||
×
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
const [activeSection, setActiveSection] = useState<SettingsSection>('general')
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dialog open={open} onOpenChange={handleClose}>
|
||||
<DialogContent className="sm:max-w-[600px] max-h-[85vh] flex flex-col">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Environment Variables</DialogTitle>
|
||||
</DialogHeader>
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-[660px] h-[60vh] flex flex-col p-0 gap-0" hideCloseButton>
|
||||
<DialogHeader className="px-6 py-4 border-b">
|
||||
<div className="flex items-center justify-between">
|
||||
<DialogTitle className="text-lg font-medium">Settings</DialogTitle>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 p-0"
|
||||
onClick={() => onOpenChange(false)}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</Button>
|
||||
</div>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="flex flex-col flex-1 min-h-0">
|
||||
<div className="space-y-1.5">
|
||||
<div className={`${GRID_COLS} px-0.5`}>
|
||||
<Label>Key</Label>
|
||||
<Label>Value</Label>
|
||||
<div />
|
||||
</div>
|
||||
<div className="flex flex-1 min-h-0">
|
||||
{/* Navigation Sidebar */}
|
||||
<div className="w-[200px] border-r">
|
||||
<SettingsNavigation activeSection={activeSection} onSectionChange={setActiveSection} />
|
||||
</div>
|
||||
|
||||
<div className="relative">
|
||||
<div
|
||||
ref={scrollContainerRef}
|
||||
className="overflow-y-auto max-h-[40vh] space-y-2 scrollbar-thin scrollbar-thumb-muted-foreground/20 hover:scrollbar-thumb-muted-foreground/25 scrollbar-track-transparent pr-6 -mr-6 pb-2 pt-2 px-2 -mx-2"
|
||||
>
|
||||
{envVars.map(renderEnvVarRow)}
|
||||
</div>
|
||||
</div>
|
||||
{/* Content Area */}
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
<div className={cn('h-full', activeSection === 'general' ? 'block' : 'hidden')}>
|
||||
<General />
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-4 pt-4 border-t mt-4">
|
||||
<Button variant="outline" size="sm" onClick={addEnvVar}>
|
||||
Add Variable
|
||||
</Button>
|
||||
|
||||
<div className="flex justify-end space-x-2">
|
||||
<Button variant="outline" onClick={handleClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleSave} disabled={!hasChanges}>
|
||||
Save Changes
|
||||
</Button>
|
||||
</div>
|
||||
<div className={cn('h-full', activeSection === 'environment' ? 'block' : 'hidden')}>
|
||||
<EnvironmentVariables onOpenChange={onOpenChange} />
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<AlertDialog open={showUnsavedChanges} onOpenChange={setShowUnsavedChanges}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Unsaved Changes</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
You have unsaved changes. Do you want to save them before closing?
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel onClick={handleCancel}>Discard Changes</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={handleSave}>Save Changes</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ const DialogOverlay = React.forwardRef<
|
||||
<DialogPrimitive.Overlay
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
|
||||
'fixed inset-0 z-50 bg-black/50 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
@@ -30,8 +30,8 @@ DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
|
||||
|
||||
const DialogContent = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> & { hideCloseButton?: boolean }
|
||||
>(({ className, children, hideCloseButton = false, ...props }, ref) => (
|
||||
<DialogPortal>
|
||||
<DialogOverlay />
|
||||
<DialogPrimitive.Content
|
||||
@@ -43,10 +43,12 @@ const DialogContent = React.forwardRef<
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
|
||||
<X className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
{!hideCloseButton && (
|
||||
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
|
||||
<X className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
)}
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
))
|
||||
|
||||
Reference in New Issue
Block a user