Merge pull request #224 from johndoe6345789/codex/add-dropdownconfigform-and-previewpane

Refactor dropdown config manager into modular components
This commit is contained in:
2025-12-27 18:34:31 +00:00
committed by GitHub
3 changed files with 260 additions and 167 deletions

View File

@@ -1,26 +1,16 @@
import { useState, useEffect } from 'react'
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui'
import { Button } from '@/components/ui'
import { Input } from '@/components/ui'
import { Label } from '@/components/ui'
import { ScrollArea } from '@/components/ui'
import { Card } from '@/components/ui'
import { Badge } from '@/components/ui'
import { Separator } from '@/components/ui'
import { useEffect, useState } from 'react'
import { Button, Card } from '@/components/ui'
import { Database } from '@/lib/database'
import { Plus, X, FloppyDisk, Trash, Pencil } from '@phosphor-icons/react'
import { Plus } from '@phosphor-icons/react'
import { toast } from 'sonner'
import type { DropdownConfig } from '@/lib/database'
import { DropdownConfigForm } from './dropdown/DropdownConfigForm'
import { PreviewPane } from './dropdown/PreviewPane'
export function DropdownConfigManager() {
const [dropdowns, setDropdowns] = useState<DropdownConfig[]>([])
const [isEditing, setIsEditing] = useState(false)
const [editingDropdown, setEditingDropdown] = useState<DropdownConfig | null>(null)
const [dropdownName, setDropdownName] = useState('')
const [dropdownLabel, setDropdownLabel] = useState('')
const [options, setOptions] = useState<Array<{ value: string; label: string }>>([])
const [newOptionValue, setNewOptionValue] = useState('')
const [newOptionLabel, setNewOptionLabel] = useState('')
useEffect(() => {
loadDropdowns()
@@ -31,63 +21,34 @@ export function DropdownConfigManager() {
setDropdowns(configs)
}
const startEdit = (dropdown?: DropdownConfig) => {
if (dropdown) {
setEditingDropdown(dropdown)
setDropdownName(dropdown.name)
setDropdownLabel(dropdown.label)
setOptions(dropdown.options)
} else {
setEditingDropdown(null)
setDropdownName('')
setDropdownLabel('')
setOptions([])
}
const openEditor = (dropdown?: DropdownConfig) => {
setEditingDropdown(dropdown ?? null)
setIsEditing(true)
}
const addOption = () => {
if (newOptionValue && newOptionLabel) {
setOptions(current => [...current, { value: newOptionValue, label: newOptionLabel }])
setNewOptionValue('')
setNewOptionLabel('')
}
}
const removeOption = (index: number) => {
setOptions(current => current.filter((_, i) => i !== index))
}
const handleSave = async () => {
if (!dropdownName || !dropdownLabel || options.length === 0) {
toast.error('Please fill all fields and add at least one option')
return
}
const newDropdown: DropdownConfig = {
id: editingDropdown?.id || `dropdown_${Date.now()}`,
name: dropdownName,
label: dropdownLabel,
options,
}
if (editingDropdown) {
await Database.updateDropdownConfig(newDropdown.id, newDropdown)
const handleSave = async (config: DropdownConfig, isEdit: boolean) => {
if (isEdit) {
await Database.updateDropdownConfig(config.id, config)
toast.success('Dropdown updated successfully')
} else {
await Database.addDropdownConfig(newDropdown)
await Database.addDropdownConfig(config)
toast.success('Dropdown created successfully')
}
setIsEditing(false)
loadDropdowns()
await loadDropdowns()
}
const handleDelete = async (id: string) => {
if (confirm('Are you sure you want to delete this dropdown configuration?')) {
await Database.deleteDropdownConfig(id)
toast.success('Dropdown deleted')
loadDropdowns()
await Database.deleteDropdownConfig(id)
toast.success('Dropdown deleted')
await loadDropdowns()
}
const handleDialogChange = (open: boolean) => {
setIsEditing(open)
if (!open) {
setEditingDropdown(null)
}
}
@@ -98,7 +59,7 @@ export function DropdownConfigManager() {
<h2 className="text-2xl font-bold">Dropdown Configurations</h2>
<p className="text-sm text-muted-foreground">Manage dynamic dropdown options for properties</p>
</div>
<Button onClick={() => startEdit()}>
<Button onClick={() => openEditor()}>
<Plus className="mr-2" />
Create Dropdown
</Button>
@@ -106,30 +67,12 @@ export function DropdownConfigManager() {
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{dropdowns.map(dropdown => (
<Card key={dropdown.id} className="p-4 space-y-3">
<div className="flex items-start justify-between">
<div>
<h3 className="font-semibold">{dropdown.label}</h3>
<p className="text-xs text-muted-foreground font-mono">{dropdown.name}</p>
</div>
<div className="flex gap-1">
<Button size="sm" variant="ghost" onClick={() => startEdit(dropdown)}>
<Pencil size={16} />
</Button>
<Button size="sm" variant="ghost" onClick={() => handleDelete(dropdown.id)}>
<Trash size={16} />
</Button>
</div>
</div>
<Separator />
<div className="flex flex-wrap gap-1">
{dropdown.options.map((opt, i) => (
<Badge key={i} variant="secondary" className="text-xs">
{opt.label}
</Badge>
))}
</div>
</Card>
<PreviewPane
key={dropdown.id}
dropdown={dropdown}
onEdit={openEditor}
onDelete={handleDelete}
/>
))}
</div>
@@ -139,88 +82,12 @@ export function DropdownConfigManager() {
</Card>
)}
<Dialog open={isEditing} onOpenChange={setIsEditing}>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle>{editingDropdown ? 'Edit' : 'Create'} Dropdown Configuration</DialogTitle>
</DialogHeader>
<div className="space-y-4">
<div className="space-y-2">
<Label>Dropdown Name (ID)</Label>
<Input
placeholder="e.g., status_options"
value={dropdownName}
onChange={(e) => setDropdownName(e.target.value)}
/>
<p className="text-xs text-muted-foreground">Unique identifier for this dropdown</p>
</div>
<div className="space-y-2">
<Label>Display Label</Label>
<Input
placeholder="e.g., Status"
value={dropdownLabel}
onChange={(e) => setDropdownLabel(e.target.value)}
/>
</div>
<Separator />
<div className="space-y-2">
<Label>Options</Label>
<div className="flex gap-2">
<Input
placeholder="Value"
value={newOptionValue}
onChange={(e) => setNewOptionValue(e.target.value)}
/>
<Input
placeholder="Label"
value={newOptionLabel}
onChange={(e) => setNewOptionLabel(e.target.value)}
/>
<Button onClick={addOption} type="button">
<Plus size={16} />
</Button>
</div>
</div>
{options.length > 0 && (
<ScrollArea className="h-[200px] border rounded-lg p-3">
<div className="space-y-2">
{options.map((opt, i) => (
<div key={i} className="flex items-center justify-between p-2 border rounded bg-muted/50">
<div className="flex-1">
<span className="font-mono text-sm">{opt.value}</span>
<span className="mx-2 text-muted-foreground"></span>
<span className="text-sm">{opt.label}</span>
</div>
<Button
size="sm"
variant="ghost"
onClick={() => removeOption(i)}
>
<X size={16} />
</Button>
</div>
))}
</div>
</ScrollArea>
)}
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setIsEditing(false)}>
Cancel
</Button>
<Button onClick={handleSave}>
<FloppyDisk className="mr-2" />
Save
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<DropdownConfigForm
open={isEditing}
editingDropdown={editingDropdown}
onOpenChange={handleDialogChange}
onSave={handleSave}
/>
</div>
)
}

View File

@@ -0,0 +1,182 @@
import { useEffect, useMemo, useState } from 'react'
import { Badge, Button, Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, Input, Label, ScrollArea, Separator } from '@/components/ui'
import { FloppyDisk, Plus, X } from '@phosphor-icons/react'
import { toast } from 'sonner'
import type { DropdownConfig } from '@/lib/database'
interface DropdownConfigFormProps {
open: boolean
editingDropdown: DropdownConfig | null
onOpenChange: (open: boolean) => void
onSave: (config: DropdownConfig, isEdit: boolean) => Promise<void> | void
}
const getDefaultOptions = (dropdown?: DropdownConfig | null) => dropdown?.options ?? []
const buildDropdownConfig = (
dropdown: DropdownConfig | null,
name: string,
label: string,
options: Array<{ value: string; label: string }>
): DropdownConfig => ({
id: dropdown?.id ?? `dropdown_${Date.now()}`,
name: name.trim(),
label: label.trim(),
options,
})
export function DropdownConfigForm({ open, editingDropdown, onOpenChange, onSave }: DropdownConfigFormProps) {
const [dropdownName, setDropdownName] = useState('')
const [dropdownLabel, setDropdownLabel] = useState('')
const [options, setOptions] = useState<Array<{ value: string; label: string }>>([])
const [newOptionValue, setNewOptionValue] = useState('')
const [newOptionLabel, setNewOptionLabel] = useState('')
const isEditMode = useMemo(() => Boolean(editingDropdown), [editingDropdown])
useEffect(() => {
if (open) {
setDropdownName(editingDropdown?.name ?? '')
setDropdownLabel(editingDropdown?.label ?? '')
setOptions(getDefaultOptions(editingDropdown))
} else {
setDropdownName('')
setDropdownLabel('')
setOptions([])
setNewOptionValue('')
setNewOptionLabel('')
}
}, [open, editingDropdown])
const addOption = () => {
if (!newOptionValue.trim() || !newOptionLabel.trim()) {
toast.error('Please provide both a value and label for the option')
return
}
const duplicate = options.some(
(opt) => opt.value.toLowerCase() === newOptionValue.trim().toLowerCase()
)
if (duplicate) {
toast.error('An option with this value already exists')
return
}
setOptions((current) => [
...current,
{ value: newOptionValue.trim(), label: newOptionLabel.trim() },
])
setNewOptionValue('')
setNewOptionLabel('')
}
const removeOption = (index: number) => {
setOptions((current) => current.filter((_, i) => i !== index))
}
const handleSave = async () => {
if (!dropdownName.trim() || !dropdownLabel.trim() || options.length === 0) {
toast.error('Please fill all fields and add at least one option')
return
}
const config = buildDropdownConfig(editingDropdown, dropdownName, dropdownLabel, options)
await onSave(config, isEditMode)
onOpenChange(false)
}
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle>{isEditMode ? 'Edit' : 'Create'} Dropdown Configuration</DialogTitle>
</DialogHeader>
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="dropdownName">Dropdown Name (ID)</Label>
<Input
id="dropdownName"
placeholder="e.g., status_options"
value={dropdownName}
onChange={(e) => setDropdownName(e.target.value)}
/>
<p className="text-xs text-muted-foreground">Unique identifier for this dropdown</p>
</div>
<div className="space-y-2">
<Label htmlFor="dropdownLabel">Display Label</Label>
<Input
id="dropdownLabel"
placeholder="e.g., Status"
value={dropdownLabel}
onChange={(e) => setDropdownLabel(e.target.value)}
/>
</div>
<Separator />
<div className="space-y-2">
<Label>Options</Label>
<div className="flex gap-2">
<Input
placeholder="Value"
value={newOptionValue}
onChange={(e) => setNewOptionValue(e.target.value)}
/>
<Input
placeholder="Label"
value={newOptionLabel}
onChange={(e) => setNewOptionLabel(e.target.value)}
/>
<Button onClick={addOption} type="button">
<Plus size={16} />
</Button>
</div>
</div>
{options.length > 0 && (
<ScrollArea className="h-[200px] border rounded-lg p-3">
<div className="space-y-2">
{options.map((opt, i) => (
<div key={i} className="flex items-center justify-between p-2 border rounded bg-muted/50">
<div className="flex-1">
<span className="font-mono text-sm">{opt.value}</span>
<span className="mx-2 text-muted-foreground"></span>
<span className="text-sm">{opt.label}</span>
</div>
<Button
size="sm"
variant="ghost"
onClick={() => removeOption(i)}
>
<X size={16} />
</Button>
</div>
))}
</div>
</ScrollArea>
)}
</div>
{options.length === 0 && (
<div className="flex items-center gap-2 rounded-md border bg-muted/40 p-3 text-sm text-muted-foreground">
<Badge variant="outline">Tip</Badge>
Add at least one option to save this dropdown configuration.
</div>
)}
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)}>
Cancel
</Button>
<Button onClick={handleSave}>
<FloppyDisk className="mr-2" />
Save
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}

View File

@@ -0,0 +1,44 @@
import { Badge, Button, Card, Separator } from '@/components/ui'
import { Pencil, Trash } from '@phosphor-icons/react'
import type { DropdownConfig } from '@/lib/database'
interface PreviewPaneProps {
dropdown: DropdownConfig
onEdit: (dropdown: DropdownConfig) => void
onDelete: (id: string) => void
}
export function PreviewPane({ dropdown, onEdit, onDelete }: PreviewPaneProps) {
const handleDelete = () => {
if (confirm('Are you sure you want to delete this dropdown configuration?')) {
onDelete(dropdown.id)
}
}
return (
<Card className="p-4 space-y-3">
<div className="flex items-start justify-between">
<div>
<h3 className="font-semibold">{dropdown.label}</h3>
<p className="text-xs text-muted-foreground font-mono">{dropdown.name}</p>
</div>
<div className="flex gap-1">
<Button size="sm" variant="ghost" onClick={() => onEdit(dropdown)}>
<Pencil size={16} />
</Button>
<Button size="sm" variant="ghost" onClick={handleDelete}>
<Trash size={16} />
</Button>
</div>
</div>
<Separator />
<div className="flex flex-wrap gap-1">
{dropdown.options.map((opt, i) => (
<Badge key={i} variant="secondary" className="text-xs">
{opt.label}
</Badge>
))}
</div>
</Card>
)
}