diff --git a/src/assets/images/toolbar/icons/furnieditor.png b/src/assets/images/toolbar/icons/furnieditor.png new file mode 100644 index 0000000..df621f3 Binary files /dev/null and b/src/assets/images/toolbar/icons/furnieditor.png differ diff --git a/src/components/MainView.tsx b/src/components/MainView.tsx index b03d21d..4a39a59 100644 --- a/src/components/MainView.tsx +++ b/src/components/MainView.tsx @@ -9,6 +9,7 @@ import { CampaignView } from './campaign/CampaignView'; import { CatalogView } from './catalog/CatalogView'; import { ChatHistoryView } from './chat-history/ChatHistoryView'; import { FloorplanEditorView } from './floorplan-editor/FloorplanEditorView'; +import { FurniEditorView } from './furni-editor/FurniEditorView'; import { FriendsView } from './friends/FriendsView'; import { GameCenterView } from './game-center/GameCenterView'; import { GroupsView } from './groups/GroupsView'; @@ -120,6 +121,7 @@ export const MainView: FC<{}> = props => + diff --git a/src/components/furni-editor/FurniEditorView.tsx b/src/components/furni-editor/FurniEditorView.tsx index a013554..428035a 100644 --- a/src/components/furni-editor/FurniEditorView.tsx +++ b/src/components/furni-editor/FurniEditorView.tsx @@ -1,5 +1,6 @@ import { AddLinkEventTracker, ILinkEventTracker, RemoveLinkEventTracker } from '@nitrots/nitro-renderer'; import { FC, useCallback, useEffect, useState } from 'react'; +import { GetSessionDataManager } from '../../api'; import { NitroCardContentView, NitroCardHeaderView, NitroCardTabsItemView, NitroCardTabsView, NitroCardView } from '../../common'; import { useFurniEditor } from '../../hooks/furni-editor'; import { FurniEditorEditView } from './views/FurniEditorEditView'; @@ -15,13 +16,23 @@ export const FurniEditorView: FC<{}> = () => const { items, total, page, loading, error, clearError, - selectedItem, catalogItems, furniDataEntry, + selectedItem, setSelectedItem, furniDataEntry, interactions, searchItems, loadDetail, loadBySpriteId, updateItem, deleteItem, loadInteractions } = useFurniEditor(); + const isMod = GetSessionDataManager()?.isModerator; + + // Auto-switch to edit tab when an item is selected useEffect(() => { + if(selectedItem) setActiveTab(TAB_EDIT); + }, [ selectedItem ]); + + useEffect(() => + { + if(!isMod) return; + const linkTracker: ILinkEventTracker = { linkReceived: (url: string) => { @@ -48,47 +59,62 @@ export const FurniEditorView: FC<{}> = () => AddLinkEventTracker(linkTracker); return () => RemoveLinkEventTracker(linkTracker); - }, []); + }, [ isMod ]); useEffect(() => { if(isVisible) loadInteractions(); }, [ isVisible ]); + // Escape to close useEffect(() => { - const handler = async (e: CustomEvent<{ spriteId: number }>) => + if(!isVisible) return; + + const handler = (e: KeyboardEvent) => + { + if(e.key === 'Escape') setIsVisible(false); + }; + + window.addEventListener('keydown', handler); + + return () => window.removeEventListener('keydown', handler); + }, [ isVisible ]); + + useEffect(() => + { + if(!isMod) return; + + const handler = (e: CustomEvent<{ spriteId: number }>) => { const { spriteId } = e.detail; - const ok = await loadBySpriteId(spriteId); - - if(ok) setActiveTab(TAB_EDIT); + setIsVisible(true); + loadBySpriteId(spriteId); }; window.addEventListener('furni-editor:open', handler as EventListener); return () => window.removeEventListener('furni-editor:open', handler as EventListener); - }, [ loadBySpriteId ]); + }, [ isMod, loadBySpriteId ]); - const handleSelect = useCallback(async (id: number) => + const handleSelect = useCallback((id: number) => { - const ok = await loadDetail(id); - - if(ok) setActiveTab(TAB_EDIT); + loadDetail(id); }, [ loadDetail ]); const handleBack = useCallback(() => { + setSelectedItem(null); setActiveTab(TAB_SEARCH); - }, []); + }, [ setSelectedItem ]); const handleClose = useCallback(() => { setIsVisible(false); }, []); - if(!isVisible) return null; + if(!isVisible || !isMod) return null; return ( @@ -123,14 +149,12 @@ export const FurniEditorView: FC<{}> = () => { activeTab === TAB_EDIT && selectedItem && } diff --git a/src/components/furni-editor/views/FurniEditorEditView.tsx b/src/components/furni-editor/views/FurniEditorEditView.tsx index dc648ef..0d9fe2d 100644 --- a/src/components/furni-editor/views/FurniEditorEditView.tsx +++ b/src/components/furni-editor/views/FurniEditorEditView.tsx @@ -1,23 +1,72 @@ -import { FC, useCallback, useEffect, useState } from 'react'; -import { Button, Column, Flex, Text } from '../../../common'; -import { CatalogRef, FurniDetail } from '../../../hooks/furni-editor'; +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; - catalogItems: CatalogRef[]; furniDataEntry: Record | null; interactions: string[]; loading: boolean; - onUpdate: (id: number, fields: Record) => Promise; - onDelete: (id: number) => Promise; + onUpdate: (id: number, fields: Record) => void; + onDelete: (id: number) => void; onBack: () => void; - onRefresh: (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, catalogItems, furniDataEntry, interactions, loading, onUpdate, onDelete, onBack, onRefresh } = props; + const { item, furniDataEntry, interactions, loading, onUpdate, onDelete, onBack } = props; + const saveRef = useRef<() => void>(null); const [ form, setForm ] = useState({ itemName: '', @@ -41,7 +90,7 @@ export const FurniEditorEditView: FC = props => customparams: '', }); - const [ confirmDelete, setConfirmDelete ] = useState(false); + const [ showDeleteDialog, setShowDeleteDialog ] = useState(false); useEffect(() => { @@ -69,7 +118,7 @@ export const FurniEditorEditView: FC = props => customparams: item.customparams || '', }); - setConfirmDelete(false); + setShowDeleteDialog(false); }, [ item ]); const setField = useCallback((key: string, value: unknown) => @@ -77,113 +126,183 @@ export const FurniEditorEditView: FC = props => setForm(prev => ({ ...prev, [key]: value })); }, []); - const handleSave = useCallback(async () => + const isDirty = useMemo(() => { - const ok = await onUpdate(item.id, form); + if(!item) return false; - if(ok) onRefresh(item.id); - }, [ item, form, onUpdate, onRefresh ]); + 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 handleDelete = useCallback(async () => + const validation = useMemo(() => { - if(!confirmDelete) return setConfirmDelete(true); + const errors: Record = {}; - const ok = await onDelete(item.id); + 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'; - if(ok) onBack(); - }, [ confirmDelete, item, onDelete, onBack ]); + return errors; + }, [ form ]); - const inputClass = 'form-control form-control-sm'; - const labelClass = 'text-[11px] font-bold text-[#333] mb-0'; + 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-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'; return ( - - - - - - - { item.id } - | - - - - { item.spriteId } + { /* Header */ } + + +
+ +
+ + + ID: { item.id } + | + Sprite: { item.spriteId } + + ({ item.usageCount } in use) - ({ item.usageCount } in use) + { isDirty && Unsaved changes }
- { /* Basic Info */ } -
- Basic Info +
- setField('itemName', e.target.value) } /> + setField('itemName', e.target.value) } /> + { validation.itemName && { validation.itemName } }
- setField('publicName', e.target.value) } /> + setField('publicName', e.target.value) } /> + { validation.publicName && { validation.publicName } }
- setField('spriteId', Number(e.target.value)) } /> + setField('spriteId', Number(e.target.value)) } />
- setField('type', e.target.value) }>
-
+ - { /* Dimensions */ } -
- Dimensions +
- setField('width', Number(e.target.value)) } /> + setField('width', Number(e.target.value)) } /> + { validation.width && { validation.width } }
- setField('length', Number(e.target.value)) } /> + setField('length', Number(e.target.value)) } /> + { validation.length && { validation.length } }
- - setField('stackHeight', Number(e.target.value)) } /> + + setField('stackHeight', Number(e.target.value)) } /> + { validation.stackHeight && { validation.stackHeight } }
-
+ - { /* Permissions */ } -
- Permissions -
- { [ 'allowStack', 'allowWalk', 'allowSit', 'allowLay', 'allowGift', 'allowTrade', 'allowRecycle', 'allowMarketplaceSell', 'allowInventoryStack' ].map(key => ( - +
+
+ { PERM_GROUPS.map(group => ( +
+ { group.label } +
+ { group.keys.map(key => ( + + )) } +
+
)) }
-
+ - { /* Interaction */ } -
- Interaction +
- - setField('interactionType', e.target.value) }> { interactions.map(i => ( @@ -191,35 +310,18 @@ export const FurniEditorEditView: FC = props =>
- - setField('interactionModesCount', Number(e.target.value)) } /> + + setField('interactionModesCount', Number(e.target.value)) } />
- - setField('customparams', e.target.value) } /> + + setField('customparams', e.target.value) } />
-
+ - { /* Catalog References */ } - { catalogItems.length > 0 && -
- Catalog ({ catalogItems.length }) -
- { catalogItems.map(ci => ( -
- { ci.catalogName } (page: { ci.pageName }) - { ci.costCredits }c + { ci.costPoints }p -
- )) } -
-
- } - - { /* FurniData.json Entry */ } { furniDataEntry && -
- FurniData.json +
{ Object.entries(furniDataEntry).map(([ key, value ]) => (
@@ -228,22 +330,42 @@ export const FurniEditorEditView: FC = props =>
)) }
-
+ } { /* Actions */ } - - + + + + Ctrl+S + + + { /* Delete Confirmation Dialog */ } + { showDeleteDialog && +
setShowDeleteDialog(false) }> +
e.stopPropagation() }> + Delete Item? + + Are you sure you want to delete { item.publicName || item.itemName } (ID: { item.id })? + This action cannot be undone. + + + + + +
+
+ } ); }; diff --git a/src/components/furni-editor/views/FurniEditorSearchView.tsx b/src/components/furni-editor/views/FurniEditorSearchView.tsx index 7b3f8c6..4fb9af5 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, useState } from 'react'; -import { Button, Column, Flex, Text } from '../../../common'; +import { FC, useCallback, useEffect, useMemo, useState } from 'react'; +import { Button, Column, Flex, LayoutFurniIconImageView, Text } from '../../../common'; import { FurniItem } from '../../../hooks/furni-editor'; interface FurniEditorSearchViewProps @@ -12,11 +12,23 @@ interface FurniEditorSearchViewProps onSelect: (id: number) => void; } +type SortField = 'id' | 'spriteId' | 'itemName' | 'publicName' | 'type' | 'interactionType'; +type SortDir = 'asc' | 'desc'; + +const SortArrow: FC<{ field: SortField; active: SortField; dir: SortDir }> = ({ field, active, dir }) => +{ + if(field !== active) return ; + + return { dir === 'asc' ? '▲' : '▼' }; +}; + export const FurniEditorSearchView: FC = props => { const { items, total, page, loading, onSearch, onSelect } = props; const [ query, setQuery ] = useState(''); const [ typeFilter, setTypeFilter ] = useState(''); + const [ sortField, setSortField ] = useState('id'); + const [ sortDir, setSortDir ] = useState('asc'); useEffect(() => { @@ -33,6 +45,45 @@ export const FurniEditorSearchView: FC = props => if(e.key === 'Enter') handleSearch(); }, [ handleSearch ]); + 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 next = prev === type ? '' : type; + + onSearch(query, next, 1); + + return next; + }); + }, [ query, onSearch ]); + + const sortedItems = useMemo(() => + { + const sorted = [ ...items ]; + + sorted.sort((a, b) => + { + let va: string | number = a[sortField] ?? ''; + let vb: string | number = b[sortField] ?? ''; + + if(typeof va === 'string') va = va.toLowerCase(); + if(typeof vb === 'string') vb = vb.toLowerCase(); + + 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); return ( @@ -42,55 +93,80 @@ export const FurniEditorSearchView: FC = props => Search setQuery(e.target.value) } onKeyDown={ handleKeyDown } /> - - Type - - + + { [ '', 's', 'i' ].map(t => ( + + )) } +
+ { total > 0 && + + { total } items found + + } + - - - - - - - + + + + + + + + - { items.map(item => ( + { sortedItems.map(item => ( onSelect(item.id) } > + - - + + @@ -99,7 +175,7 @@ export const FurniEditorSearchView: FC = props => )) } { items.length === 0 && !loading && - + } @@ -109,7 +185,7 @@ export const FurniEditorSearchView: FC = props => { totalPages > 1 && - { total } items - Page { page }/{ totalPages } + Page { page }/{ totalPages } + { dropdownOpen &&
{ /* Left panel: position + rotation */ } diff --git a/src/components/toolbar/ToolbarView.tsx b/src/components/toolbar/ToolbarView.tsx index ea9fb16..e72c62a 100644 --- a/src/components/toolbar/ToolbarView.tsx +++ b/src/components/toolbar/ToolbarView.tsx @@ -96,6 +96,8 @@ export const ToolbarView: FC<{ isInRoom: boolean }> = props => CreateLinkEvent('camera/toggle') } /> } { isMod && CreateLinkEvent('mod-tools/toggle') } /> } + { isMod && + CreateLinkEvent('furni-editor/toggle') } /> } diff --git a/src/css/icons/icons.css b/src/css/icons/icons.css index 5c97f32..2feaedf 100644 --- a/src/css/icons/icons.css +++ b/src/css/icons/icons.css @@ -70,6 +70,12 @@ height: 34px; } +.nitro-icon.icon-furnieditor { + background-image: url("@/assets/images/toolbar/icons/furnieditor.png"); + width: 30px; + height: 34px; +} + .nitro-icon.icon-friendall { background-image: url("@/assets/images/toolbar/icons/friend_all.png"); width: 32px; diff --git a/src/hooks/furni-editor/index.ts b/src/hooks/furni-editor/index.ts new file mode 100644 index 0000000..47ce6ef --- /dev/null +++ b/src/hooks/furni-editor/index.ts @@ -0,0 +1 @@ +export * from './useFurniEditor'; diff --git a/src/hooks/furni-editor/useFurniEditor.ts b/src/hooks/furni-editor/useFurniEditor.ts new file mode 100644 index 0000000..936eb4f --- /dev/null +++ b/src/hooks/furni-editor/useFurniEditor.ts @@ -0,0 +1,257 @@ +import { FurniEditorBySpriteComposer, FurniEditorDeleteComposer, FurniEditorDetailComposer, FurniEditorDetailResultEvent, FurniEditorInteractionsComposer, FurniEditorInteractionsResultEvent, FurniEditorResultEvent, FurniEditorSearchComposer, FurniEditorSearchResultEvent, FurniEditorUpdateComposer } from '@nitrots/nitro-renderer'; +import { useCallback, useRef, useState } from 'react'; +import { NotificationAlertType, SendMessageComposer } from '../../api'; +import { useMessageEvent, useNotification } from '../../hooks'; + +export interface FurniItem +{ + id: number; + spriteId: number; + itemName: string; + publicName: string; + type: string; + width: number; + length: number; + stackHeight: number; + allowStack: boolean; + allowWalk: boolean; + allowSit: boolean; + allowLay: boolean; + interactionType: string; + interactionModesCount: number; +} + +export interface FurniDetail extends FurniItem +{ + allowGift: boolean; + allowTrade: boolean; + allowRecycle: boolean; + allowMarketplaceSell: boolean; + allowInventoryStack: boolean; + vendingIds: string; + customparams: string; + effectIdMale: number; + effectIdFemale: number; + clothingOnWalk: string; + multiheight: string; + description: string; + usageCount: number; +} + +export interface CatalogRef +{ + id: number; + catalogName: string; + costCredits: number; + costPoints: number; + pointsType: number; + pageId: number; + pageName: string; +} + +export const useFurniEditor = () => +{ + const [ items, setItems ] = useState([]); + const [ total, setTotal ] = useState(0); + const [ page, setPage ] = useState(1); + const [ loading, setLoading ] = useState(false); + const [ error, setError ] = useState(null); + const [ selectedItem, setSelectedItem ] = useState(null); + const [ catalogItems, setCatalogItems ] = useState([]); + const [ interactions, setInteractions ] = useState([]); + const [ furniDataEntry, setFurniDataEntry ] = useState | null>(null); + const pendingActionRef = useRef(null); + const { simpleAlert = null } = useNotification(); + + const clearError = useCallback(() => setError(null), []); + + // Handle search results + useMessageEvent(FurniEditorSearchResultEvent, (event: FurniEditorSearchResultEvent) => + { + const parser = event.getParser(); + + setLoading(false); + setItems(parser.items.map(item => ({ + id: item.id, + spriteId: item.spriteId, + itemName: item.itemName, + publicName: item.publicName, + type: item.type, + width: item.width, + length: item.length, + stackHeight: item.stackHeight, + allowStack: item.allowStack, + allowWalk: item.allowWalk, + allowSit: item.allowSit, + allowLay: item.allowLay, + interactionType: item.interactionType, + interactionModesCount: item.interactionModesCount + }))); + setTotal(parser.total); + setPage(parser.page); + }); + + // Handle detail results (for both detail and by-sprite lookups) + useMessageEvent(FurniEditorDetailResultEvent, (event: FurniEditorDetailResultEvent) => + { + const parser = event.getParser(); + const item = parser.item; + + setLoading(false); + setSelectedItem({ + id: item.id, + spriteId: item.spriteId, + itemName: item.itemName, + publicName: item.publicName, + type: item.type, + width: item.width, + length: item.length, + stackHeight: item.stackHeight, + allowStack: item.allowStack, + allowWalk: item.allowWalk, + allowSit: item.allowSit, + allowLay: item.allowLay, + interactionType: item.interactionType, + interactionModesCount: item.interactionModesCount, + allowGift: item.allowGift, + allowTrade: item.allowTrade, + allowRecycle: item.allowRecycle, + allowMarketplaceSell: item.allowMarketplaceSell, + allowInventoryStack: item.allowInventoryStack, + vendingIds: item.vendingIds, + customparams: item.customparams, + effectIdMale: item.effectIdMale, + effectIdFemale: item.effectIdFemale, + clothingOnWalk: item.clothingOnWalk, + multiheight: item.multiheight, + description: item.description, + usageCount: item.usageCount + }); + setCatalogItems(parser.catalogItems.map(ref => ({ + id: ref.id, + catalogName: ref.catalogName, + costCredits: ref.costCredits, + costPoints: ref.costPoints, + pointsType: ref.pointsType, + pageId: ref.pageId, + pageName: ref.pageName + }))); + + let furniData: Record | null = null; + + try + { + if(parser.furniDataJson && parser.furniDataJson !== '{}' && parser.furniDataJson !== '') + { + furniData = JSON.parse(parser.furniDataJson); + } + } + catch(e) {} + + setFurniDataEntry(furniData); + }); + + // Handle interaction types list + useMessageEvent(FurniEditorInteractionsResultEvent, (event: FurniEditorInteractionsResultEvent) => + { + setInteractions(event.getParser().interactions); + }); + + // Handle operation results (update, create, delete) + useMessageEvent(FurniEditorResultEvent, (event: FurniEditorResultEvent) => + { + const parser = event.getParser(); + const action = pendingActionRef.current; + + pendingActionRef.current = null; + setLoading(false); + + if(!parser.success) + { + setError(parser.message || 'Operation failed'); + + if(simpleAlert) + { + simpleAlert(parser.message || 'Operation failed', NotificationAlertType.ALERT, null, null, 'Furni Editor Error'); + } + + return; + } + + setError(null); + + if(action === 'update') + { + // Auto-reload detail after update + if(selectedItem) + { + SendMessageComposer(new FurniEditorDetailComposer(selectedItem.id)); + } + + if(simpleAlert) + { + simpleAlert('Item updated successfully', NotificationAlertType.DEFAULT, null, null, 'Furni Editor'); + } + } + else if(action === 'delete') + { + setSelectedItem(null); + setCatalogItems([]); + setFurniDataEntry(null); + + if(simpleAlert) + { + simpleAlert('Item deleted successfully', NotificationAlertType.DEFAULT, null, null, 'Furni Editor'); + } + } + }); + + const searchItems = useCallback((query: string, type: string, pg: number) => + { + setLoading(true); + setError(null); + SendMessageComposer(new FurniEditorSearchComposer(query, type, pg)); + }, []); + + const loadDetail = useCallback((id: number) => + { + setLoading(true); + setError(null); + SendMessageComposer(new FurniEditorDetailComposer(id)); + }, []); + + const loadBySpriteId = useCallback((spriteId: number) => + { + setLoading(true); + setError(null); + SendMessageComposer(new FurniEditorBySpriteComposer(spriteId)); + }, []); + + const updateItem = useCallback((id: number, fields: Record) => + { + setLoading(true); + setError(null); + pendingActionRef.current = 'update'; + SendMessageComposer(new FurniEditorUpdateComposer(id, JSON.stringify(fields))); + }, []); + + const deleteItem = useCallback((id: number) => + { + setLoading(true); + setError(null); + pendingActionRef.current = 'delete'; + SendMessageComposer(new FurniEditorDeleteComposer(id)); + }, []); + + const loadInteractions = useCallback(() => + { + SendMessageComposer(new FurniEditorInteractionsComposer()); + }, []); + + return { + items, total, page, loading, error, clearError, + selectedItem, setSelectedItem, catalogItems, furniDataEntry, + interactions, + searchItems, loadDetail, loadBySpriteId, updateItem, deleteItem, loadInteractions + }; +};
IDSpriteNamePublic NameTypeInteraction
handleSort('id') }> + ID + handleSort('spriteId') }> + Sprite + handleSort('itemName') }> + Name + handleSort('publicName') }> + Public Name + handleSort('type') }> + Type + handleSort('interactionType') }> + Interaction +
+ + { item.id } { item.spriteId }{ item.itemName }{ item.publicName }{ item.itemName }{ item.publicName } - + { item.type === 's' ? 'Floor' : 'Wall' }
No items foundNo items found