import { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { Button, Column, Flex, LayoutFurniIconImageView, Text } from '../../../common'; import { FurniDetail } from '../../../hooks/furni-editor'; interface FurniEditorEditViewProps { item: FurniDetail; furniDataEntry: Record | null; interactions: string[]; loading: boolean; onUpdate: (id: number, fields: Record) => void; onDelete: (id: number) => void; onBack: () => void; onUpdateFurnidata: (id: number, name: string, description: string) => void; onRevertFurnidata: (id: number) => void; } const FIELD_TIPS: Record = { stackHeight: 'Visual height when items are stacked on top of this furniture', interactionType: 'Defines behavior when user interacts (e.g. default, gate, teleport, vendingmachine)', customparams: 'Extra parameters for the interaction type (format depends on interaction)', interactionModesCount: 'Number of visual states/animations this furniture has', }; const PERM_GROUPS = [ { label: 'Gameplay', keys: [ 'allowStack', 'allowWalk', 'allowSit', 'allowLay' ] }, { label: 'Trading', keys: [ 'allowGift', 'allowTrade', 'allowRecycle', 'allowMarketplaceSell' ] }, { label: 'Inventory', keys: [ 'allowInventoryStack' ] }, ]; interface SectionProps { title: string; children: React.ReactNode; defaultOpen?: boolean } const Section: FC = ({ title, children, defaultOpen = true }) => { const [ open, setOpen ] = useState(defaultOpen); return (
{ open &&
{ children }
}
); }; const Tip: FC<{ field: string }> = ({ field }) => { const tip = FIELD_TIPS[field]; if(!tip) return null; return ( ? { tip } ); }; export const FurniEditorEditView: FC = props => { const { item, furniDataEntry, interactions, loading, onUpdate, onDelete, onBack, onUpdateFurnidata, onRevertFurnidata } = props; const saveRef = useRef<() => void>(null); const [ form, setForm ] = useState({ itemName: '', publicName: '', spriteId: 0, type: 's', width: 1, length: 1, stackHeight: 0, allowStack: true, allowWalk: false, allowSit: false, allowLay: false, allowGift: true, allowTrade: true, allowRecycle: true, allowMarketplaceSell: true, allowInventoryStack: true, interactionType: '', interactionModesCount: 0, customparams: '', }); const [ showDeleteDialog, setShowDeleteDialog ] = useState(false); const [ furniName, setFurniName ] = useState(''); const [ furniDescription, setFurniDescription ] = useState(''); const [ confirmFurnidata, setConfirmFurnidata ] = useState(false); useEffect(() => { if(!item) return; setForm({ itemName: item.itemName || '', publicName: item.publicName || '', spriteId: item.spriteId || 0, type: item.type || 's', width: item.width || 1, length: item.length || 1, stackHeight: item.stackHeight || 0, allowStack: !!item.allowStack, allowWalk: !!item.allowWalk, allowSit: !!item.allowSit, allowLay: !!item.allowLay, allowGift: !!item.allowGift, allowTrade: !!item.allowTrade, allowRecycle: !!item.allowRecycle, allowMarketplaceSell: !!item.allowMarketplaceSell, allowInventoryStack: !!item.allowInventoryStack, interactionType: item.interactionType || '', interactionModesCount: item.interactionModesCount || 0, customparams: item.customparams || '', }); setShowDeleteDialog(false); setFurniName(String(furniDataEntry?.name ?? '')); setFurniDescription(String(furniDataEntry?.description ?? '')); setConfirmFurnidata(false); }, [ item, furniDataEntry ]); const setField = useCallback((key: string, value: unknown) => { setForm(prev => ({ ...prev, [key]: value })); }, []); const isDirty = useMemo(() => { if(!item) return false; return form.itemName !== (item.itemName || '') || form.publicName !== (item.publicName || '') || form.spriteId !== (item.spriteId || 0) || form.type !== (item.type || 's') || form.width !== (item.width || 1) || form.length !== (item.length || 1) || form.stackHeight !== (item.stackHeight || 0) || form.allowStack !== !!item.allowStack || form.allowWalk !== !!item.allowWalk || form.allowSit !== !!item.allowSit || form.allowLay !== !!item.allowLay || form.allowGift !== !!item.allowGift || form.allowTrade !== !!item.allowTrade || form.allowRecycle !== !!item.allowRecycle || form.allowMarketplaceSell !== !!item.allowMarketplaceSell || form.allowInventoryStack !== !!item.allowInventoryStack || form.interactionType !== (item.interactionType || '') || form.interactionModesCount !== (item.interactionModesCount || 0) || form.customparams !== (item.customparams || ''); }, [ form, item ]); const validation = useMemo(() => { const errors: Record = {}; if(!form.itemName.trim()) errors.itemName = 'Required'; if(!form.publicName.trim()) errors.publicName = 'Required'; if(form.width < 1) errors.width = 'Min 1'; if(form.length < 1) errors.length = 'Min 1'; if(form.stackHeight < 0) errors.stackHeight = 'Min 0'; return errors; }, [ form ]); const isValid = useMemo(() => Object.keys(validation).length === 0, [ validation ]); const handleSave = useCallback(() => { if(!isValid) return; onUpdate(item.id, form); }, [ item, form, isValid, onUpdate ]); // Expose save for keyboard shortcut saveRef.current = handleSave; const handleBack = useCallback(() => { if(isDirty && !window.confirm('You have unsaved changes. Discard and go back?')) return; onBack(); }, [ isDirty, onBack ]); const handleDeleteConfirm = useCallback(() => { onDelete(item.id); setShowDeleteDialog(false); }, [ item, onDelete ]); // Keyboard shortcuts useEffect(() => { const handler = (e: KeyboardEvent) => { if(e.ctrlKey && e.key === 's') { e.preventDefault(); saveRef.current?.(); } }; window.addEventListener('keydown', handler); return () => window.removeEventListener('keydown', handler); }, []); const inputClass = (field?: string) => `w-full px-2 py-1 text-sm leading-normal rounded-sm border border-[#bbb] focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/40 min-h-[calc(1.5em+0.5rem+2px)]${ field && validation[field] ? ' border-red-500 bg-red-50' : '' }`; const labelClass = 'text-[11px] font-bold text-[#374151] mb-0.5 flex items-center gap-0.5'; const readonlyClass = 'w-full px-2 py-1 text-sm font-mono rounded-sm border border-[#ddd] bg-[#f2f2eb] text-[#555] select-all'; return ( { /* Header */ }
ID: { item.id } | Sprite: { item.spriteId } ({ item.usageCount } in use) { isDirty && Unsaved changes }
{ /* Primary edit surface: furnidata display name + description (server-authoritative, live) */ }
Display Name & Description { (furniName !== String(furniDataEntry?.name ?? '') || furniDescription !== String(furniDataEntry?.description ?? '')) && Unsaved }
setFurniName(e.target.value) } maxLength={ 256 } />