mirror of
https://github.com/duckietm/Nitro-V3.git
synced 2026-06-19 15:06:20 +00:00
refactor(hooks/rooms): collapse usePetPackageWidget 5 useStates into useReducer
The hook tracked five related useState fields driving the pet-package
naming dialog (isVisible / objectId / objectType / petName / errorResult).
They transitioned in lockstep on the two RoomSessionPetPackageEvent
types and the inline change handler — textbook state-machine territory.
Collapse into a single useReducer with four explicit transitions:
- 'open' → REQUESTED event lands; flips visible, records target
- 'close' → REQUESTED-result success OR user dismiss; resets to INITIAL
- 'set-name' → input change; updates petName AND clears any error
(the previous code had this side effect inlined in
onChangePetName as `if(errorResult.length) setErrorResult('')`,
now it's part of the reducer contract)
- 'set-error' → REQUESTED-result with validation failure; sets the label
Plus extract `getPetPackageNameError(code)` to a top-level exported
pure function (was an inline closure named getErrorResultForCode).
The mapping is server-protocol contract, not UI state — moving it out
of the hook means it's testable, reusable, and won't be recreated on
every render.
Public API of usePetPackageWidget is unchanged — the one consumer
(PetPackageWidgetView) reads the same destructured fields. Verified
via grep.
Tests: 4 new cases on getPetPackageNameError covering code 0 / 1-4 /
falsy / unknown-fallback. Suite: 207/207 (was 203/203).
This commit is contained in:
@@ -0,0 +1,36 @@
|
|||||||
|
import { describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
|
vi.mock('../../../api', () => ({
|
||||||
|
LocalizeText: (key: string) => key,
|
||||||
|
SendMessageComposer: vi.fn()
|
||||||
|
}));
|
||||||
|
|
||||||
|
import { getPetPackageNameError } from './usePetPackageWidget';
|
||||||
|
|
||||||
|
describe('getPetPackageNameError', () =>
|
||||||
|
{
|
||||||
|
it('returns empty string for code 0 (no error)', () =>
|
||||||
|
{
|
||||||
|
expect(getPetPackageNameError(0)).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns empty string for falsy values (null/undefined coerced)', () =>
|
||||||
|
{
|
||||||
|
expect(getPetPackageNameError(null as never)).toBe('');
|
||||||
|
expect(getPetPackageNameError(undefined as never)).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('maps the four documented error codes to their localization keys', () =>
|
||||||
|
{
|
||||||
|
expect(getPetPackageNameError(1)).toBe('catalog.alert.petname.long');
|
||||||
|
expect(getPetPackageNameError(2)).toBe('catalog.alert.petname.short');
|
||||||
|
expect(getPetPackageNameError(3)).toBe('catalog.alert.petname.chars');
|
||||||
|
expect(getPetPackageNameError(4)).toBe('catalog.alert.petname.bobba');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('falls back to the bobba label for unknown error codes (defensive default)', () =>
|
||||||
|
{
|
||||||
|
expect(getPetPackageNameError(99)).toBe('catalog.alert.petname.bobba');
|
||||||
|
expect(getPetPackageNameError(-1)).toBe('catalog.alert.petname.bobba');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,53 +1,74 @@
|
|||||||
import { GetRoomEngine, OpenPetPackageMessageComposer, RoomObjectCategory, RoomSessionPetPackageEvent } from '@nitrots/nitro-renderer';
|
import { GetRoomEngine, OpenPetPackageMessageComposer, RoomObjectCategory, RoomSessionPetPackageEvent } from '@nitrots/nitro-renderer';
|
||||||
import { useState } from 'react';
|
import { useReducer } from 'react';
|
||||||
import { LocalizeText, SendMessageComposer } from '../../../api';
|
import { LocalizeText, SendMessageComposer } from '../../../api';
|
||||||
import { useNitroEvent } from '../../events';
|
import { useNitroEvent } from '../../events';
|
||||||
|
|
||||||
|
interface PetPackageState
|
||||||
|
{
|
||||||
|
isVisible: boolean;
|
||||||
|
objectId: number;
|
||||||
|
objectType: string;
|
||||||
|
petName: string;
|
||||||
|
errorResult: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type PetPackageAction =
|
||||||
|
| { type: 'open'; objectId: number; objectType: string }
|
||||||
|
| { type: 'close' }
|
||||||
|
| { type: 'set-name'; petName: string }
|
||||||
|
| { type: 'set-error'; errorResult: string };
|
||||||
|
|
||||||
|
const INITIAL_STATE: PetPackageState = {
|
||||||
|
isVisible: false,
|
||||||
|
objectId: -1,
|
||||||
|
objectType: '',
|
||||||
|
petName: '',
|
||||||
|
errorResult: ''
|
||||||
|
};
|
||||||
|
|
||||||
|
const petPackageReducer = (state: PetPackageState, action: PetPackageAction): PetPackageState =>
|
||||||
|
{
|
||||||
|
switch(action.type)
|
||||||
|
{
|
||||||
|
case 'open':
|
||||||
|
return { ...INITIAL_STATE, isVisible: true, objectId: action.objectId, objectType: action.objectType };
|
||||||
|
case 'close':
|
||||||
|
return INITIAL_STATE;
|
||||||
|
case 'set-name':
|
||||||
|
// Typing into the input always clears any previous error label.
|
||||||
|
return { ...state, petName: action.petName, errorResult: '' };
|
||||||
|
case 'set-error':
|
||||||
|
return { ...state, errorResult: action.errorResult };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps the pet-package name-validation error code returned by the
|
||||||
|
* server to a localized error label. Exported for testability — the
|
||||||
|
* mapping is server-protocol contract, not UI state.
|
||||||
|
*/
|
||||||
|
export const getPetPackageNameError = (errorCode: number): string =>
|
||||||
|
{
|
||||||
|
if(!errorCode) return '';
|
||||||
|
|
||||||
|
switch(errorCode)
|
||||||
|
{
|
||||||
|
case 1: return LocalizeText('catalog.alert.petname.long');
|
||||||
|
case 2: return LocalizeText('catalog.alert.petname.short');
|
||||||
|
case 3: return LocalizeText('catalog.alert.petname.chars');
|
||||||
|
case 4:
|
||||||
|
default:
|
||||||
|
return LocalizeText('catalog.alert.petname.bobba');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const usePetPackageWidgetState = () =>
|
const usePetPackageWidgetState = () =>
|
||||||
{
|
{
|
||||||
const [ isVisible, setIsVisible ] = useState<boolean>(false);
|
const [ state, dispatch ] = useReducer(petPackageReducer, INITIAL_STATE);
|
||||||
const [ objectId, setObjectId ] = useState<number>(-1);
|
|
||||||
const [ objectType, setObjectType ] = useState<string>('');
|
|
||||||
const [ petName, setPetName ] = useState<string>('');
|
|
||||||
const [ errorResult, setErrorResult ] = useState<string>('');
|
|
||||||
|
|
||||||
const onClose = () =>
|
const onClose = () => dispatch({ type: 'close' });
|
||||||
{
|
const onConfirm = () => SendMessageComposer(new OpenPetPackageMessageComposer(state.objectId, state.petName));
|
||||||
setErrorResult('');
|
const onChangePetName = (petName: string) => dispatch({ type: 'set-name', petName });
|
||||||
setPetName('');
|
|
||||||
setObjectType('');
|
|
||||||
setObjectId(-1);
|
|
||||||
setIsVisible(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onConfirm = () =>
|
|
||||||
{
|
|
||||||
SendMessageComposer(new OpenPetPackageMessageComposer(objectId, petName));
|
|
||||||
};
|
|
||||||
|
|
||||||
const onChangePetName = (petName: string) =>
|
|
||||||
{
|
|
||||||
setPetName(petName);
|
|
||||||
if(errorResult.length > 0) setErrorResult('');
|
|
||||||
};
|
|
||||||
|
|
||||||
const getErrorResultForCode = (errorCode: number) =>
|
|
||||||
{
|
|
||||||
if(!errorCode || errorCode === 0) return;
|
|
||||||
|
|
||||||
switch(errorCode)
|
|
||||||
{
|
|
||||||
case 1:
|
|
||||||
return LocalizeText('catalog.alert.petname.long');
|
|
||||||
case 2:
|
|
||||||
return LocalizeText('catalog.alert.petname.short');
|
|
||||||
case 3:
|
|
||||||
return LocalizeText('catalog.alert.petname.chars');
|
|
||||||
case 4:
|
|
||||||
default:
|
|
||||||
return LocalizeText('catalog.alert.petname.bobba');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useNitroEvent<RoomSessionPetPackageEvent>(RoomSessionPetPackageEvent.RSOPPE_OPEN_PET_PACKAGE_REQUESTED, event =>
|
useNitroEvent<RoomSessionPetPackageEvent>(RoomSessionPetPackageEvent.RSOPPE_OPEN_PET_PACKAGE_REQUESTED, event =>
|
||||||
{
|
{
|
||||||
@@ -55,21 +76,31 @@ const usePetPackageWidgetState = () =>
|
|||||||
|
|
||||||
const roomObject = GetRoomEngine().getRoomObject(event.session.roomId, event.objectId, RoomObjectCategory.FLOOR);
|
const roomObject = GetRoomEngine().getRoomObject(event.session.roomId, event.objectId, RoomObjectCategory.FLOOR);
|
||||||
|
|
||||||
setObjectId(event.objectId);
|
dispatch({ type: 'open', objectId: event.objectId, objectType: roomObject.type });
|
||||||
setObjectType(roomObject.type);
|
|
||||||
setIsVisible(true);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
useNitroEvent<RoomSessionPetPackageEvent>(RoomSessionPetPackageEvent.RSOPPE_OPEN_PET_PACKAGE_RESULT, event =>
|
useNitroEvent<RoomSessionPetPackageEvent>(RoomSessionPetPackageEvent.RSOPPE_OPEN_PET_PACKAGE_RESULT, event =>
|
||||||
{
|
{
|
||||||
if(!event) return;
|
if(!event) return;
|
||||||
|
|
||||||
if(event.nameValidationStatus === 0) onClose();
|
if(event.nameValidationStatus === 0)
|
||||||
|
{
|
||||||
|
dispatch({ type: 'close' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if(event.nameValidationStatus !== 0) setErrorResult(getErrorResultForCode(event.nameValidationStatus));
|
dispatch({ type: 'set-error', errorResult: getPetPackageNameError(event.nameValidationStatus) });
|
||||||
});
|
});
|
||||||
|
|
||||||
return { isVisible, errorResult, petName, objectType, onChangePetName, onConfirm, onClose };
|
return {
|
||||||
|
isVisible: state.isVisible,
|
||||||
|
errorResult: state.errorResult,
|
||||||
|
petName: state.petName,
|
||||||
|
objectType: state.objectType,
|
||||||
|
onChangePetName,
|
||||||
|
onConfirm,
|
||||||
|
onClose
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const usePetPackageWidget = usePetPackageWidgetState;
|
export const usePetPackageWidget = usePetPackageWidgetState;
|
||||||
|
|||||||
Reference in New Issue
Block a user