diff --git a/src/components/furni-editor/FurniEditorView.tsx b/src/components/furni-editor/FurniEditorView.tsx index 65206ba..c36ed09 100644 --- a/src/components/furni-editor/FurniEditorView.tsx +++ b/src/components/furni-editor/FurniEditorView.tsx @@ -18,7 +18,8 @@ export const FurniEditorView: FC<{}> = () => items, total, page, loading, error, clearError, selectedItem, setSelectedItem, furniDataEntry, interactions, - searchItems, loadDetail, loadBySpriteId, updateItem, deleteItem, loadInteractions + searchItems, loadDetail, loadBySpriteId, updateItem, deleteItem, loadInteractions, + updateFurnidata, revertFurnidata, importText, importResult } = useFurniEditor(); const isMod = useHasPermission('acc_catalogfurni'); @@ -155,6 +156,10 @@ export const FurniEditorView: FC<{}> = () => onUpdate={ updateItem } onDelete={ deleteItem } onBack={ handleBack } + onUpdateFurnidata={ updateFurnidata } + onRevertFurnidata={ revertFurnidata } + onImportText={ importText } + importResult={ importResult } /> } diff --git a/src/components/furni-editor/views/FurniEditorEditView.tsx b/src/components/furni-editor/views/FurniEditorEditView.tsx index 0d9fe2d..5c7a86b 100644 --- a/src/components/furni-editor/views/FurniEditorEditView.tsx +++ b/src/components/furni-editor/views/FurniEditorEditView.tsx @@ -1,4 +1,5 @@ import { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { createPortal } from 'react-dom'; import { Button, Column, Flex, LayoutFurniIconImageView, Text } from '../../../common'; import { FurniDetail } from '../../../hooks/furni-editor'; @@ -11,6 +12,10 @@ interface FurniEditorEditViewProps onUpdate: (id: number, fields: Record) => void; onDelete: (id: number) => void; onBack: () => void; + onUpdateFurnidata: (id: number, name: string, description: string) => void; + onRevertFurnidata: (id: number) => void; + onImportText: (id: number) => void; + importResult: { found: boolean; name: string; description: string; classname: string; nonce: number } | null; } const FIELD_TIPS: Record = { @@ -21,9 +26,8 @@ const FIELD_TIPS: Record = { }; const PERM_GROUPS = [ - { label: 'Gameplay', keys: [ 'allowStack', 'allowWalk', 'allowSit', 'allowLay' ] }, + { label: 'Gameplay', keys: [ 'allowStack', 'allowWalk', 'allowSit', 'allowLay', 'allowInventoryStack' ] }, { label: 'Trading', keys: [ 'allowGift', 'allowTrade', 'allowRecycle', 'allowMarketplaceSell' ] }, - { label: 'Inventory', keys: [ 'allowInventoryStack' ] }, ]; interface SectionProps { title: string; children: React.ReactNode; defaultOpen?: boolean } @@ -33,16 +37,16 @@ const Section: FC = ({ title, children, defaultOpen = true }) => const [ open, setOpen ] = useState(defaultOpen); return ( -
+
- { open &&
{ children }
} + { open &&
{ children }
}
); }; @@ -50,22 +54,74 @@ const Section: FC = ({ title, children, defaultOpen = true }) => const Tip: FC<{ field: string }> = ({ field }) => { const tip = FIELD_TIPS[field]; + const ref = useRef(null); + const [ pos, setPos ] = useState<{ left: number; top: number } | null>(null); + + const show = useCallback(() => + { + const r = ref.current?.getBoundingClientRect(); + if(r) setPos({ left: r.left + (r.width / 2), top: r.top - 6 }); + }, []); + const hide = useCallback(() => setPos(null), []); if(!tip) return null; return ( - - ? - - { tip } - + + ? + { pos && createPortal( + + { tip } + , document.body) } ); }; +const CopyValue: FC<{ value: string | number }> = ({ value }) => +{ + const [ copied, setCopied ] = useState(false); + + const copy = useCallback(() => + { + const text = String(value); + if(navigator.clipboard?.writeText) navigator.clipboard.writeText(text).then(() => setCopied(true)).catch(() => setCopied(true)); + else setCopied(true); + }, [ value ]); + + // Reset the "copied!" flag after 1s, with cleanup so the timer never fires after unmount. + useEffect(() => + { + if(!copied) return; + + const handle = window.setTimeout(() => setCopied(false), 1000); + + return () => window.clearTimeout(handle); + }, [ copied ]); + + return ( +
+ { String(value) } + { copied ? 'copied!' : 'copy' } +
+ ); +}; + export const FurniEditorEditView: FC = props => { - const { item, furniDataEntry, interactions, loading, onUpdate, onDelete, onBack } = props; + const { item, furniDataEntry, interactions, loading, onUpdate, onDelete, onBack, onUpdateFurnidata, onRevertFurnidata, onImportText, importResult } = props; const saveRef = useRef<() => void>(null); const [ form, setForm ] = useState({ @@ -91,6 +147,11 @@ export const FurniEditorEditView: FC = props => }); const [ showDeleteDialog, setShowDeleteDialog ] = useState(false); + const [ furniName, setFurniName ] = useState(''); + const [ furniDescription, setFurniDescription ] = useState(''); + const [ confirmFurnidata, setConfirmFurnidata ] = useState(false); + const [ importNote, setImportNote ] = useState(''); + const appliedImportNonce = useRef(0); useEffect(() => { @@ -119,7 +180,11 @@ export const FurniEditorEditView: FC = props => }); setShowDeleteDialog(false); - }, [ item ]); + setFurniName(String(furniDataEntry?.name ?? '')); + setFurniDescription(String(furniDataEntry?.description ?? '')); + setConfirmFurnidata(false); + setImportNote(''); + }, [ item, furniDataEntry ]); const setField = useCallback((key: string, value: unknown) => { @@ -166,6 +231,48 @@ export const FurniEditorEditView: FC = props => const isValid = useMemo(() => Object.keys(validation).length === 0, [ validation ]); + // Furnidata name editing only works when the furni has a matching furnidata + // entry: the server writer is edit-only and refuses classnames absent from + // furnidata (pets, custom items, …). furniDataEntry is the entry resolved by + // the server (by id); guard on it + a classname match so we never trigger the + // cryptic "Classname not found in furnidata" error on save. + const furnidataEditable = useMemo(() => + { + if(!furniDataEntry) return false; + const cn = String((furniDataEntry as { classname?: unknown }).classname ?? '').trim().toLowerCase(); + const itemCn = String(item?.itemName ?? '').trim().toLowerCase(); + return cn ? (cn === itemCn) : true; + }, [ furniDataEntry, item ]); + + // True only when the name/description actually differ from the stored furnidata + // entry. Used to gate the Save button: saving an unchanged value makes the + // server writer return false, which the handler misreports as "Classname not + // found in furnidata" — so we never let an unchanged save fire. + const furnidataDirty = useMemo(() => + furniName !== String(furniDataEntry?.name ?? '') || furniDescription !== String(furniDataEntry?.description ?? ''), + [ furniName, furniDescription, furniDataEntry ]); + + // Apply an "Import from Habbo" result into the editable fields (review then Save). + useEffect(() => + { + if(!importResult || importResult.nonce === appliedImportNonce.current) return; + appliedImportNonce.current = importResult.nonce; + + // Ignore a result that belongs to a different furni (user navigated away). + if(importResult.classname && importResult.classname.trim().toLowerCase() !== String(item?.itemName ?? '').trim().toLowerCase()) return; + + if(importResult.found) + { + setFurniName(importResult.name); + setFurniDescription(importResult.description); + setImportNote('Imported from Habbo — review and Save'); + } + else + { + setImportNote('Not found on Habbo for this classname'); + } + }, [ importResult, item ]); + const handleSave = useCallback(() => { if(!isValid) return; @@ -207,54 +314,116 @@ export const FurniEditorEditView: FC = props => }, []); 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' : '' }`; - const labelClass = 'text-[11px] font-bold text-[#333] mb-0 flex items-center gap-0.5'; + `w-full px-3 py-1.5 text-sm leading-normal rounded-lg border border-slate-300 bg-[#ffffff] focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/15 transition${ field && validation[field] ? ' border-red-400 bg-red-50' : '' }`; + const labelClass = 'text-[11px] font-medium text-slate-500 mb-1 flex items-center gap-0.5'; return ( - + { /* Header */ } - - -
- + +
+
- - - ID: { item.id } - | - Sprite: { item.spriteId } + + { furniName || form.publicName || form.itemName } + { form.itemName } + + + ID + { item.id } + + + Sprite + { item.spriteId } + + 0 ? 'border-[#a7f3d0] bg-[#ecfdf5] text-[#047857]' : 'border-slate-200 bg-slate-50 text-slate-500' }` }> + 0 ? 'bg-[#10b981]' : 'bg-slate-300' }` } /> + { item.usageCount } in use + + { isDirty && + + Unsaved + } - ({ item.usageCount } in use) - { isDirty && Unsaved changes } + + { /* Primary edit surface: furnidata display name + description (server-authoritative, live) */ } +
+
+ Display name & description + { furnidataEditable + ? LIVE + : NO FURNIDATA } + { furnidataEditable && furnidataDirty && + Unsaved } +
+ { furnidataEditable ? ( + <> +
+
+ + setFurniName(e.target.value) } maxLength={ 256 } /> +
+
+ + setFurniDescription(e.target.value) } maxLength={ 256 } /> +
+
+ + + + + + { importNote && + { importNote } } + + ) : ( +
+ + This furni has no matching furnidata entry (e.g. a pet or custom item), so its display name can't be edited here. Clients fall back to the DB Public Name below. +
+ ) } +
+
- - setField('itemName', e.target.value) } /> - { validation.itemName && { validation.itemName } } + +
- - setField('publicName', e.target.value) } /> - { validation.publicName && { validation.publicName } } + +
- setField('spriteId', Number(e.target.value)) } /> +
- +
+ { furniDataEntry && +
+ Read-only — how this furni resolves from the furnidata JSON (source of truth for the display name). +
{ JSON.stringify(furniDataEntry, null, 2) }
+
+ } +
@@ -279,19 +448,24 @@ export const FurniEditorEditView: FC = props =>
{ PERM_GROUPS.map(group => (
- { group.label } -
- { group.keys.map(key => ( - - )) } + { group.label } +
+ { group.keys.map(key => { + const on = (form as any)[key]; + return ( + + ); + }) }
)) } @@ -302,7 +476,7 @@ export const FurniEditorEditView: FC = props =>
- setField('interactionType', e.target.value) }> { interactions.map(i => ( @@ -320,19 +494,6 @@ export const FurniEditorEditView: FC = props =>
- { furniDataEntry && -
-
- { Object.entries(furniDataEntry).map(([ key, value ]) => ( -
- { key } - { String(value ?? '') } -
- )) } -
-
- } - { /* Actions */ } @@ -352,8 +513,8 @@ export const FurniEditorEditView: FC = props => { /* Delete Confirmation Dialog */ } { showDeleteDialog && -
setShowDeleteDialog(false) }> -
e.stopPropagation() }> +
setShowDeleteDialog(false) }> +
e.stopPropagation() }> Delete Item? Are you sure you want to delete { item.publicName || item.itemName } (ID: { item.id })? @@ -366,6 +527,21 @@ export const FurniEditorEditView: FC = props =>
} + + { /* Furnidata Confirmation Dialog */ } + { confirmFurnidata && +
setConfirmFurnidata(false) }> +
e.stopPropagation() }> + Apply furnidata change to ALL clients? +
Name: { String(furniDataEntry?.name ?? '') } → { furniName }
+
Desc: { String(furniDataEntry?.description ?? '') } → { furniDescription }
+ + + + +
+
+ } ); }; diff --git a/src/components/furni-editor/views/FurniEditorSearchView.tsx b/src/components/furni-editor/views/FurniEditorSearchView.tsx index a1eccfb..9164951 100644 --- a/src/components/furni-editor/views/FurniEditorSearchView.tsx +++ b/src/components/furni-editor/views/FurniEditorSearchView.tsx @@ -1,5 +1,5 @@ -import { FC, useCallback, useEffect, useEffectEvent, useMemo, useState } from 'react'; -import { Button, Column, Flex, LayoutFurniIconImageView, Text } from '../../../common'; +import { FC, ReactNode, useCallback, useEffect, useRef, useState } from 'react'; +import { Column, Flex, LayoutFurniIconImageView, Text } from '../../../common'; import { FurniItem } from '../../../hooks/furni-editor'; interface FurniEditorSearchViewProps @@ -8,20 +8,43 @@ interface FurniEditorSearchViewProps total: number; page: number; loading: boolean; - onSearch: (query: string, type: string, page: number) => void; + onSearch: (query: string, type: string, page: number, sortField: string, sortDir: string) => void; onSelect: (id: number) => void; } type SortField = 'id' | 'spriteId' | 'itemName' | 'publicName' | 'type' | 'interactionType'; type SortDir = 'asc' | 'desc'; +const PAGE_SIZE = 20; + +const COLUMNS: { field: SortField; label: string; align: 'left' | 'center' }[] = [ + { field: 'id', label: 'ID', align: 'left' }, + { field: 'spriteId', label: 'Sprite', align: 'left' }, + { field: 'itemName', label: 'Name', align: 'left' }, + { field: 'publicName', label: 'Public Name', align: 'left' }, + { field: 'type', label: 'Type', align: 'center' }, + { field: 'interactionType', label: 'Interaction', align: 'left' }, +]; + const SortArrow: FC<{ field: SortField; active: SortField; dir: SortDir }> = ({ field, active, dir }) => { - if(field !== active) return ; + if(field !== active) return ; - return { dir === 'asc' ? '▲' : '▼' }; + return { dir === 'asc' ? '▲' : '▼' }; }; +const PagBtn: FC<{ disabled?: boolean; onClick: () => void; children: ReactNode; title?: string }> = ({ disabled, onClick, children, title }) => ( + +); + export const FurniEditorSearchView: FC = props => { const { items, total, page, loading, onSearch, onSelect } = props; @@ -29,184 +52,198 @@ export const FurniEditorSearchView: FC = props => const [ typeFilter, setTypeFilter ] = useState(''); const [ sortField, setSortField ] = useState('id'); const [ sortDir, setSortDir ] = useState('asc'); + const [ pageInput, setPageInput ] = useState('1'); - const initialSearch = useEffectEvent(() => onSearch('', '', 1)); + const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE)); + const from = total === 0 ? 0 : ((page - 1) * PAGE_SIZE) + 1; + const to = Math.min(page * PAGE_SIZE, total); + // Latest filter/sort for the debounced query effect (avoids stale closure). + const stateRef = useRef({ typeFilter, sortField, sortDir }); + stateRef.current = { typeFilter, sortField, sortDir }; + + // Initial fetch (once). + const didInit = useRef(false); useEffect(() => { - initialSearch(); - }, []); + if(didInit.current) return; + didInit.current = true; + onSearch('', '', 1, 'id', 'asc'); + }, [ onSearch ]); - const handleSearch = useCallback(() => - { - onSearch(query, typeFilter, 1); - }, [ query, typeFilter, onSearch ]); + // Keep the page input synced with the authoritative page from the server. + useEffect(() => { setPageInput(String(page)); }, [ page ]); - const handleKeyDown = useCallback((e: React.KeyboardEvent) => + // Debounced live search as the user types (skips the first render). + const firstQuery = useRef(true); + useEffect(() => { - if(e.key === 'Enter') handleSearch(); - }, [ handleSearch ]); + if(firstQuery.current) { firstQuery.current = false; return; } - const handleSort = useCallback((field: SortField) => - { - setSortDir(prev => (sortField === field ? (prev === 'asc' ? 'desc' : 'asc') : 'asc')); - setSortField(field); - }, [ sortField ]); - - const handleTypeToggle = useCallback((type: string) => - { - setTypeFilter(prev => + const handle = window.setTimeout(() => { - const next = prev === type ? '' : type; + const s = stateRef.current; + onSearch(query, s.typeFilter, 1, s.sortField, s.sortDir); + }, 350); - onSearch(query, next, 1); - - return next; - }); + return () => window.clearTimeout(handle); }, [ query, onSearch ]); - const sortedItems = useMemo(() => + const applyType = useCallback((t: string) => { - const sorted = [ ...items ]; + const next = typeFilter === t ? '' : t; + setTypeFilter(next); + onSearch(query, next, 1, sortField, sortDir); + }, [ typeFilter, query, sortField, sortDir, onSearch ]); - sorted.sort((a, b) => - { - let va: string | number = a[sortField] ?? ''; - let vb: string | number = b[sortField] ?? ''; + const applySort = useCallback((field: SortField) => + { + const nextDir: SortDir = (sortField === field && sortDir === 'asc') ? 'desc' : 'asc'; + setSortField(field); + setSortDir(nextDir); + onSearch(query, typeFilter, 1, field, nextDir); + }, [ sortField, sortDir, query, typeFilter, onSearch ]); - if(typeof va === 'string') va = va.toLowerCase(); - if(typeof vb === 'string') vb = vb.toLowerCase(); + const goTo = useCallback((pg: number) => + { + const clamped = Math.min(Math.max(1, pg || 1), totalPages); + onSearch(query, typeFilter, clamped, sortField, sortDir); + }, [ totalPages, query, typeFilter, sortField, sortDir, onSearch ]); - if(va < vb) return sortDir === 'asc' ? -1 : 1; - if(va > vb) return sortDir === 'asc' ? 1 : -1; - - return 0; - }); - - return sorted; - }, [ items, sortField, sortDir ]); - - const totalPages = Math.ceil(total / 20); + const inputClass = 'w-full pl-9 pr-8 py-2 text-sm leading-normal rounded-lg border border-slate-300 bg-[#ffffff] focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/15 transition'; return ( - - - - Search + + { /* Search + filters */ } + +
+ + + setQuery(e.target.value) } - onKeyDown={ handleKeyDown } /> - - - { [ '', 's', 'i' ].map(t => ( + { query && - )) } + type="button" + onClick={ () => setQuery('') } + className="absolute right-2 top-1/2 -translate-y-1/2 w-5 h-5 flex items-center justify-center rounded-full text-[11px] text-slate-300 hover:text-slate-500 hover:bg-slate-100" + >✕ } +
+ + { [ '', 's', 'i' ].map(t => + { + const on = typeFilter === t; + + return ( + + ); + }) } -
- { total > 0 && - - { total } items found + { /* Result count + activity */ } + + + { total > 0 ? `Showing ${ from }–${ to } of ${ total.toLocaleString() }` : (loading ? 'Searching…' : 'No results') } - } + { loading && } + - - + { /* Table */ } +
+
- - - - - - - - + + + { COLUMNS.map(c => ( + + )) } - { sortedItems.map(item => ( + { items.map(item => ( onSelect(item.id) } + className="group cursor-pointer border-b border-slate-100 last:border-0 hover:bg-slate-50 transition-colors" > - - - - - - + + + + - + )) } + { items.length === 0 && loading && + Array.from({ length: 8 }).map((_, i) => ( + + + { COLUMNS.map(c => ) } + + )) } { items.length === 0 && !loading && - - - } + + }
handleSort('id') }> - ID - handleSort('spriteId') }> - Sprite - handleSort('itemName') }> - Name - handleSort('publicName') }> - Public Name - handleSort('type') }> - Type - handleSort('interactionType') }> - Interaction -
applySort(c.field) } + className={ `px-2 py-2 text-[10px] font-semibold uppercase tracking-wide text-slate-500 cursor-pointer hover:text-slate-700 ${ c.align === 'center' ? 'text-center' : 'text-left' }` } + > + { c.label } +
- + +
+ +
{ item.id }{ item.spriteId }{ item.itemName }{ item.publicName } - + { item.id }{ item.spriteId }{ item.itemName }{ item.publicName || '—' } + { item.type === 's' ? 'Floor' : 'Wall' } { item.interactionType || '-' } + { item.interactionType + ? { item.interactionType } + : } +
No items found
+
No furni found
+
Try a different search or filter
+
-
+
- { totalPages > 1 && - - - Page { page }/{ totalPages } - - - - + { /* Pagination */ } + + { total.toLocaleString() } items + + goTo(1) }>« + goTo(page - 1) }>‹ + + setPageInput(e.target.value.replace(/[^0-9]/g, '')) } + onKeyDown={ e => { if(e.key === 'Enter') goTo(Number(pageInput)); } } + className="w-12 px-1.5 py-1 text-center rounded-lg border border-slate-200 bg-[#ffffff] focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/15" + /> + / { totalPages.toLocaleString() } + = totalPages } onClick={ () => goTo(page + 1) }>› + = totalPages } onClick={ () => goTo(totalPages) }>» - } + ); }; diff --git a/src/hooks/furni-editor/useFurniEditor.ts b/src/hooks/furni-editor/useFurniEditor.ts index cb62d86..3cd8ec8 100644 --- a/src/hooks/furni-editor/useFurniEditor.ts +++ b/src/hooks/furni-editor/useFurniEditor.ts @@ -1,4 +1,4 @@ -import { FurniEditorBySpriteComposer, FurniEditorDeleteComposer, FurniEditorDetailComposer, FurniEditorDetailResultEvent, FurniEditorInteractionsComposer, FurniEditorInteractionsResultEvent, FurniEditorResultEvent, FurniEditorSearchComposer, FurniEditorSearchResultEvent, FurniEditorUpdateComposer } from '@nitrots/nitro-renderer'; +import { FurniEditorBySpriteComposer, FurniEditorDeleteComposer, FurniEditorDetailComposer, FurniEditorDetailResultEvent, FurniEditorInteractionsComposer, FurniEditorInteractionsResultEvent, FurniEditorResultEvent, FurniEditorRevertFurnidataComposer, FurniEditorSearchComposer, FurniEditorSearchResultEvent, FurniEditorUpdateComposer, FurniEditorUpdateFurnidataComposer, FurniEditorImportTextComposer, FurniEditorImportTextResultEvent } from '@nitrots/nitro-renderer'; import { useCallback, useRef, useState } from 'react'; import { NotificationAlertType, SendMessageComposer } from '../../api'; import { useMessageEvent, useNotification } from '../../hooks'; @@ -61,6 +61,8 @@ export const useFurniEditor = () => const [ interactions, setInteractions ] = useState([]); const [ furniDataEntry, setFurniDataEntry ] = useState | null>(null); const pendingActionRef = useRef<{ action: string; itemId: number } | null>(null); + const [ importResult, setImportResult ] = useState<{ found: boolean; name: string; description: string; classname: string; nonce: number } | null>(null); + const importNonceRef = useRef(0); const { simpleAlert = null } = useNotification(); const clearError = useCallback(() => setError(null), []); @@ -209,11 +211,11 @@ export const useFurniEditor = () => } }); - const searchItems = useCallback((query: string, type: string, pg: number) => + const searchItems = useCallback((query: string, type: string, pg: number, sortField: string = 'id', sortDir: string = 'asc') => { setLoading(true); setError(null); - SendMessageComposer(new FurniEditorSearchComposer(query, type, pg)); + SendMessageComposer(new FurniEditorSearchComposer(query, type, pg, sortField, sortDir)); }, []); const loadDetail = useCallback((id: number) => @@ -246,6 +248,46 @@ export const useFurniEditor = () => SendMessageComposer(new FurniEditorDeleteComposer(id)); }, []); + const updateFurnidata = useCallback((id: number, name: string, description: string) => + { + pendingActionRef.current = { action: 'update', itemId: id }; + // Optimistic: the server now mirrors the furnidata display name into + // items_base.public_name, so reflect it immediately in the read-only + // "Public Name" field. The auto re-fetch that follows will agree (no flicker). + setSelectedItem(prev => (prev && prev.id === id ? { ...prev, publicName: name } : prev)); + setLoading(true); + SendMessageComposer(new FurniEditorUpdateFurnidataComposer(id, JSON.stringify({ name, description }))); + }, []); + + const revertFurnidata = useCallback((id: number) => + { + pendingActionRef.current = { action: 'update', itemId: id }; + setLoading(true); + SendMessageComposer(new FurniEditorRevertFurnidataComposer(id)); + }, []); + + const importText = useCallback((id: number) => + { + setLoading(true); + setError(null); + SendMessageComposer(new FurniEditorImportTextComposer(id)); + }, []); + + useMessageEvent(FurniEditorImportTextResultEvent, (event: FurniEditorImportTextResultEvent) => + { + const parser = event.getParser(); + + setLoading(false); + importNonceRef.current += 1; + setImportResult({ + found: parser.found, + name: parser.name, + description: parser.description, + classname: parser.classname, + nonce: importNonceRef.current + }); + }); + const loadInteractions = useCallback(() => { SendMessageComposer(new FurniEditorInteractionsComposer()); @@ -255,6 +297,7 @@ export const useFurniEditor = () => items, total, page, loading, error, clearError, selectedItem, setSelectedItem, catalogItems, furniDataEntry, interactions, - searchItems, loadDetail, loadBySpriteId, updateItem, deleteItem, loadInteractions + searchItems, loadDetail, loadBySpriteId, updateItem, deleteItem, loadInteractions, + updateFurnidata, revertFurnidata, importText, importResult }; };