Files
plunk/apps/web/src/components/KeyValueEditor.tsx
T

133 lines
4.2 KiB
TypeScript

import {Button, Input, Label} from '@plunk/ui';
import {Plus, Trash2} from 'lucide-react';
import {useState} from 'react';
interface KeyValuePair {
id: string;
key: string;
value: string;
}
interface KeyValueEditorProps {
initialData?: Record<string, string | number | boolean> | null;
onChange?: (data: Record<string, string | number | boolean> | null) => void;
}
export function KeyValueEditor({initialData, onChange}: KeyValueEditorProps) {
const [pairs, setPairs] = useState<KeyValuePair[]>(() => {
// Initialize state from initialData only once on mount
if (initialData && typeof initialData === 'object') {
return Object.entries(initialData).map(([key, value], index) => ({
id: `initial-${index}-${Date.now()}`,
key,
value: String(value),
}));
}
return [];
});
// Notify parent of changes based on given pairs
const notifyChange = (updatedPairs: KeyValuePair[]) => {
if (!onChange) return;
// Filter out empty pairs
const validPairs = updatedPairs.filter(pair => pair.key.trim() !== '');
if (validPairs.length === 0) {
onChange(null);
} else {
const data = validPairs.reduce(
(acc, pair) => {
// Try to parse as number or boolean, otherwise keep as string
let value: string | number | boolean = pair.value;
if (pair.value === 'true') value = true;
else if (pair.value === 'false') value = false;
else if (!isNaN(Number(pair.value)) && pair.value.trim() !== '') value = Number(pair.value);
acc[pair.key] = value;
return acc;
},
{} as Record<string, string | number | boolean>,
);
onChange(data);
}
};
const addPair = () => {
const newPairs = [...pairs, {id: `new-${Date.now()}`, key: '', value: ''}];
setPairs(newPairs);
notifyChange(newPairs);
};
const updateKey = (id: string, newKey: string) => {
const updated = pairs.map(pair => (pair.id === id ? {...pair, key: newKey} : pair));
setPairs(updated);
notifyChange(updated);
};
const updateValue = (id: string, newValue: string) => {
const updated = pairs.map(pair => (pair.id === id ? {...pair, value: newValue} : pair));
setPairs(updated);
notifyChange(updated);
};
const removePair = (id: string) => {
const updated = pairs.filter(pair => pair.id !== id);
setPairs(updated);
notifyChange(updated);
};
return (
<div className="space-y-3">
<div className="flex items-center justify-between">
<Label>Custom Data</Label>
<Button type="button" variant="outline" size="sm" onClick={addPair}>
<Plus className="h-3 w-3" />
Add Field
</Button>
</div>
{pairs.length === 0 ? (
<div className="text-center py-8 border border-dashed border-neutral-200 rounded-lg bg-neutral-50">
<p className="text-sm text-neutral-500">No custom fields yet</p>
<p className="text-xs text-neutral-400 mt-1">Click &quot;Add Field&quot; to create custom data fields</p>
</div>
) : (
<div className="space-y-2">
{pairs.map(pair => (
<div key={pair.id} className="flex gap-2 items-start">
<div className="flex-1">
<Input
type="text"
placeholder="Key"
value={pair.key}
onChange={e => updateKey(pair.id, e.target.value)}
className="text-sm"
/>
</div>
<div className="flex-1">
<Input
type="text"
placeholder="Value"
value={pair.value}
onChange={e => updateValue(pair.id, e.target.value)}
className="text-sm"
/>
</div>
<Button
type="button"
variant="ghost"
size="icon"
onClick={() => removePair(pair.id)}
className="text-red-500 hover:text-red-700 hover:bg-red-50"
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
))}
</div>
)}
</div>
);
}