feat(furni-editor): editable furnidata name/desc section + read-only classname/public_name + diff-confirm + revert

This commit is contained in:
simoleo89
2026-06-06 02:46:48 +02:00
parent c63702bdac
commit 09ca51aedf
2 changed files with 51 additions and 20 deletions
@@ -18,7 +18,8 @@ export const FurniEditorView: FC<{}> = () =>
items, total, page, loading, error, clearError, items, total, page, loading, error, clearError,
selectedItem, setSelectedItem, furniDataEntry, selectedItem, setSelectedItem, furniDataEntry,
interactions, interactions,
searchItems, loadDetail, loadBySpriteId, updateItem, deleteItem, loadInteractions searchItems, loadDetail, loadBySpriteId, updateItem, deleteItem, loadInteractions,
updateFurnidata, revertFurnidata
} = useFurniEditor(); } = useFurniEditor();
const isMod = useHasPermission('acc_catalogfurni'); const isMod = useHasPermission('acc_catalogfurni');
@@ -155,6 +156,8 @@ export const FurniEditorView: FC<{}> = () =>
onUpdate={ updateItem } onUpdate={ updateItem }
onDelete={ deleteItem } onDelete={ deleteItem }
onBack={ handleBack } onBack={ handleBack }
onUpdateFurnidata={ updateFurnidata }
onRevertFurnidata={ revertFurnidata }
/> />
} }
@@ -11,6 +11,8 @@ interface FurniEditorEditViewProps
onUpdate: (id: number, fields: Record<string, unknown>) => void; onUpdate: (id: number, fields: Record<string, unknown>) => void;
onDelete: (id: number) => void; onDelete: (id: number) => void;
onBack: () => void; onBack: () => void;
onUpdateFurnidata: (id: number, name: string, description: string) => void;
onRevertFurnidata: (id: number) => void;
} }
const FIELD_TIPS: Record<string, string> = { const FIELD_TIPS: Record<string, string> = {
@@ -65,7 +67,7 @@ const Tip: FC<{ field: string }> = ({ field }) =>
export const FurniEditorEditView: FC<FurniEditorEditViewProps> = props => export const FurniEditorEditView: FC<FurniEditorEditViewProps> = props =>
{ {
const { item, furniDataEntry, interactions, loading, onUpdate, onDelete, onBack } = props; const { item, furniDataEntry, interactions, loading, onUpdate, onDelete, onBack, onUpdateFurnidata, onRevertFurnidata } = props;
const saveRef = useRef<() => void>(null); const saveRef = useRef<() => void>(null);
const [ form, setForm ] = useState({ const [ form, setForm ] = useState({
@@ -91,6 +93,9 @@ export const FurniEditorEditView: FC<FurniEditorEditViewProps> = props =>
}); });
const [ showDeleteDialog, setShowDeleteDialog ] = useState(false); const [ showDeleteDialog, setShowDeleteDialog ] = useState(false);
const [ furniName, setFurniName ] = useState('');
const [ furniDescription, setFurniDescription ] = useState('');
const [ confirmFurnidata, setConfirmFurnidata ] = useState(false);
useEffect(() => useEffect(() =>
{ {
@@ -119,7 +124,10 @@ export const FurniEditorEditView: FC<FurniEditorEditViewProps> = props =>
}); });
setShowDeleteDialog(false); setShowDeleteDialog(false);
}, [ item ]); setFurniName(String(furniDataEntry?.name ?? ''));
setFurniDescription(String(furniDataEntry?.description ?? ''));
setConfirmFurnidata(false);
}, [ item, furniDataEntry ]);
const setField = useCallback((key: string, value: unknown) => const setField = useCallback((key: string, value: unknown) =>
{ {
@@ -209,6 +217,7 @@ export const FurniEditorEditView: FC<FurniEditorEditViewProps> = props =>
const inputClass = (field?: string) => const inputClass = (field?: string) =>
`w-full px-2 py-1 text-xs leading-normal rounded-sm border border-[#ccc] min-h-[calc(1.5em+0.5rem+2px)] ${ field && validation[field] ? 'border-red-500 bg-red-50' : '' }`; `w-full px-2 py-1 text-xs leading-normal rounded-sm border border-[#ccc] min-h-[calc(1.5em+0.5rem+2px)] ${ field && validation[field] ? 'border-red-500 bg-red-50' : '' }`;
const labelClass = 'text-[11px] font-bold text-[#333] mb-0 flex items-center gap-0.5'; const labelClass = 'text-[11px] font-bold text-[#333] mb-0 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 ( return (
<Column gap={ 1 } className="h-full overflow-auto"> <Column gap={ 1 } className="h-full overflow-auto">
@@ -232,14 +241,12 @@ export const FurniEditorEditView: FC<FurniEditorEditViewProps> = props =>
<Section title="Basic Info"> <Section title="Basic Info">
<div className="grid grid-cols-2 gap-2"> <div className="grid grid-cols-2 gap-2">
<div> <div>
<label className={ labelClass }>Item Name</label> <label className={ labelClass }>Classname</label>
<input className={ inputClass('itemName') } value={ form.itemName } onChange={ e => setField('itemName', e.target.value) } /> <div className={ readonlyClass }>{ form.itemName }</div>
{ validation.itemName && <span className="text-[9px] text-red-500">{ validation.itemName }</span> }
</div> </div>
<div> <div>
<label className={ labelClass }>Public Name</label> <label className={ labelClass }>Public Name (DB fallback)</label>
<input className={ inputClass('publicName') } value={ form.publicName } onChange={ e => setField('publicName', e.target.value) } /> <div className={ readonlyClass }>{ form.publicName }</div>
{ validation.publicName && <span className="text-[9px] text-red-500">{ validation.publicName }</span> }
</div> </div>
<div> <div>
<label className={ labelClass }>Sprite ID</label> <label className={ labelClass }>Sprite ID</label>
@@ -320,18 +327,24 @@ export const FurniEditorEditView: FC<FurniEditorEditViewProps> = props =>
</div> </div>
</Section> </Section>
{ furniDataEntry && <Section title="Furnidata (display name)" defaultOpen={ true }>
<Section title="FurniData.json" defaultOpen={ false }> <Column gap={ 1 }>
<div className="grid grid-cols-2 gap-x-3 gap-y-0.5 text-[10px]"> <div>
{ Object.entries(furniDataEntry).map(([ key, value ]) => ( <label className={ labelClass }>Display Name</label>
<div key={ key } className="flex justify-between bg-[#f5f5f5] px-2 py-0.5 rounded"> <input className={ inputClass() } value={ furniName } onChange={ e => setFurniName(e.target.value) } maxLength={ 256 } />
<span className="font-bold text-[#555]">{ key }</span>
<span className="text-[#333] truncate ml-1 max-w-[120px] text-right">{ String(value ?? '') }</span>
</div>
)) }
</div> </div>
</Section> <div>
} <label className={ labelClass }>Description</label>
<textarea className={ inputClass() } rows={ 3 } value={ furniDescription } onChange={ e => setFurniDescription(e.target.value) } maxLength={ 256 } />
</div>
{ (furniName !== String(furniDataEntry?.name ?? '') || furniDescription !== String(furniDataEntry?.description ?? '')) &&
<span className="text-[10px] text-orange-500 font-bold">Unsaved furnidata changes</span> }
<Flex gap={ 1 }>
<Button variant="success" disabled={ loading } onClick={ () => setConfirmFurnidata(true) }>Save name/desc</Button>
<Button variant="secondary" disabled={ loading } onClick={ () => onRevertFurnidata(item.id) }>Revert</Button>
</Flex>
</Column>
</Section>
{ /* Actions */ } { /* Actions */ }
<Flex gap={ 1 } justifyContent="between" alignItems="center" className="mt-1"> <Flex gap={ 1 } justifyContent="between" alignItems="center" className="mt-1">
@@ -366,6 +379,21 @@ export const FurniEditorEditView: FC<FurniEditorEditViewProps> = props =>
</div> </div>
</div> </div>
} }
{ /* Furnidata Confirmation Dialog */ }
{ confirmFurnidata &&
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50" onClick={ () => setConfirmFurnidata(false) }>
<div className="bg-white rounded-lg shadow-xl p-4 w-[320px]" onClick={ e => e.stopPropagation() }>
<Text bold className="text-[14px] mb-2 block">Apply furnidata change to ALL clients?</Text>
<div className="text-xs mb-1"><b>Name:</b> { String(furniDataEntry?.name ?? '') } { furniName }</div>
<div className="text-xs mb-3"><b>Desc:</b> { String(furniDataEntry?.description ?? '') } { furniDescription }</div>
<Flex gap={ 1 } justifyContent="end">
<Button variant="secondary" onClick={ () => setConfirmFurnidata(false) }>Cancel</Button>
<Button variant="success" onClick={ () => { onUpdateFurnidata(item.id, furniName, furniDescription); setConfirmFurnidata(false); } }>Confirm</Button>
</Flex>
</div>
</div>
}
</Column> </Column>
); );
}; };