mirror of
https://github.com/duckietm/Nitro-V3.git
synced 2026-06-19 15:06:20 +00:00
8aa02249e1
Drop the SecurityLevel-named family (useIsModerator / useIsAdmin /
useIsCommunity / useIsPlayerSupport / useHasSecurityLevel /
useUserSecurityLevel) in favour of a rank-based family tied to the
operator's actual `permission_ranks` rows in the Arcturus DB:
- `useUserRank()` returns `{ id, name, level, badge, prefix,
prefixColor }` derived from the snapshot. Powered by the renderer's
extended IUserDataSnapshot (companion commit 87e67d5 on
feat/react19-event-bus).
- `useHasRankLevel(min)` replaces useHasSecurityLevel; consumers
pass a `permission_ranks.level` threshold from the deployment.
- `useIsRank(name)` matches `permission_ranks.rank_name` exactly.
To avoid bare integers in widget bodies, added a deployment-scoped
constants file at `src/api/nitro/session/RankLevels.ts`:
export const STAFF_LEVELS = {
MEMBER: 1, SUPPORT: 4, MOD: 5, SUPER_MOD: 6, ADMIN: 7
};
A deployment that re-numbers `permission_ranks` only edits this file.
Migrated all 11 consumer reads (same set as the earlier session's
useIsModerator migration plus the audit catch): ToolbarView,
CatalogClassicView, CatalogModernView, ChooserWidgetView,
CalendarView, YouTubePlayerView, FurniEditorView,
InfoStandWidgetFurniView, AvatarInfoWidgetPetView,
FurnitureMannequinView, NavigatorRoomInfoView. The
NavigatorRoomInfoView `staff_pick` permission was previously
`securityLevel >= COMMUNITY (7)` via the renderer-enum wrapper —
ported to `useHasRankLevel(STAFF_LEVELS.ADMIN)` because in the
default seed level 7 is Administrator, which is the actual rank that
gets the `acc_anyroomowner`-style permissions for staff-picking.
Tests refreshed under `useSessionSnapshots.test.tsx`:
- useUserRank surfaces the full metadata block;
- useHasRankLevel does `>=` against the threshold;
- useIsRank exact-matches against rank_name;
- a runtime promote (snapshot mutation + SESSION_DATA_UPDATED
dispatch) flips the result, locking in the reactive contract.
Mock extended only minimally — kept the SecurityLevel enum class for
any consumer outside the dropped family that still imports it.
Verification: yarn typecheck clean, yarn lint:hooks clean, yarn test
213/213. The Arcturus-side composer change (UserPermissionsComposer
appending the 5 extra fields) is staged but UNCOMMITTED on Arcturus
main (which has unrelated WIP); the wire is backward-compatible so
the React client works against both pre- and post-extension
emulators.
166 lines
5.3 KiB
TypeScript
166 lines
5.3 KiB
TypeScript
import { AddLinkEventTracker, ILinkEventTracker, RemoveLinkEventTracker } from '@nitrots/nitro-renderer';
|
|
import { FC, useCallback, useEffect, useState } from 'react';
|
|
import { STAFF_LEVELS } from '../../api';
|
|
import { NitroCardContentView, NitroCardHeaderView, NitroCardTabsItemView, NitroCardTabsView, NitroCardView } from '../../common';
|
|
import { useHasRankLevel } from '../../hooks';
|
|
import { useFurniEditor } from '../../hooks/furni-editor';
|
|
import { FurniEditorEditView } from './views/FurniEditorEditView';
|
|
import { FurniEditorSearchView } from './views/FurniEditorSearchView';
|
|
|
|
const TAB_SEARCH = 0;
|
|
const TAB_EDIT = 1;
|
|
|
|
export const FurniEditorView: FC<{}> = () =>
|
|
{
|
|
const [ isVisible, setIsVisible ] = useState(false);
|
|
const [ activeTab, setActiveTab ] = useState(TAB_SEARCH);
|
|
|
|
const {
|
|
items, total, page, loading, error, clearError,
|
|
selectedItem, setSelectedItem, furniDataEntry,
|
|
interactions,
|
|
searchItems, loadDetail, loadBySpriteId, updateItem, deleteItem, loadInteractions
|
|
} = useFurniEditor();
|
|
|
|
const isMod = useHasRankLevel(STAFF_LEVELS.MOD);
|
|
|
|
// 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) =>
|
|
{
|
|
const parts = url.split('/');
|
|
|
|
if(parts.length < 2) return;
|
|
|
|
switch(parts[1])
|
|
{
|
|
case 'show':
|
|
setIsVisible(true);
|
|
return;
|
|
case 'hide':
|
|
setIsVisible(false);
|
|
return;
|
|
case 'toggle':
|
|
setIsVisible(prev => !prev);
|
|
return;
|
|
}
|
|
},
|
|
eventUrlPrefix: 'furni-editor/'
|
|
};
|
|
|
|
AddLinkEventTracker(linkTracker);
|
|
|
|
return () => RemoveLinkEventTracker(linkTracker);
|
|
}, [ isMod ]);
|
|
|
|
useEffect(() =>
|
|
{
|
|
if(isVisible) loadInteractions();
|
|
}, [ isVisible ]);
|
|
|
|
// Escape to close
|
|
useEffect(() =>
|
|
{
|
|
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;
|
|
|
|
setIsVisible(true);
|
|
loadBySpriteId(spriteId);
|
|
};
|
|
|
|
window.addEventListener('furni-editor:open', handler);
|
|
|
|
return () => window.removeEventListener('furni-editor:open', handler);
|
|
}, [ isMod, loadBySpriteId ]);
|
|
|
|
const handleSelect = useCallback((id: number) =>
|
|
{
|
|
loadDetail(id);
|
|
}, [ loadDetail ]);
|
|
|
|
const handleBack = useCallback(() =>
|
|
{
|
|
setSelectedItem(null);
|
|
setActiveTab(TAB_SEARCH);
|
|
}, [ setSelectedItem ]);
|
|
|
|
const handleClose = useCallback(() =>
|
|
{
|
|
setIsVisible(false);
|
|
}, []);
|
|
|
|
if(!isVisible || !isMod) return null;
|
|
|
|
return (
|
|
<NitroCardView uniqueKey="furni-editor" className="w-[620px] h-[520px]">
|
|
<NitroCardHeaderView headerText="Furni Editor" onCloseClick={ handleClose } />
|
|
<NitroCardTabsView>
|
|
<NitroCardTabsItemView isActive={ activeTab === TAB_SEARCH } onClick={ () => setActiveTab(TAB_SEARCH) }>
|
|
Search
|
|
</NitroCardTabsItemView>
|
|
<NitroCardTabsItemView isActive={ activeTab === TAB_EDIT } onClick={ () => selectedItem && setActiveTab(TAB_EDIT) }>
|
|
Edit
|
|
</NitroCardTabsItemView>
|
|
</NitroCardTabsView>
|
|
<NitroCardContentView>
|
|
{ error &&
|
|
<div className="bg-[#f8d7da] border border-[#f5c6cb] rounded p-2 text-[#721c24] text-xs mb-1 flex justify-between items-center">
|
|
<span>{ error }</span>
|
|
<span className="cursor-pointer font-bold" onClick={ clearError }>x</span>
|
|
</div>
|
|
}
|
|
|
|
{ activeTab === TAB_SEARCH &&
|
|
<FurniEditorSearchView
|
|
items={ items }
|
|
total={ total }
|
|
page={ page }
|
|
loading={ loading }
|
|
onSearch={ searchItems }
|
|
onSelect={ handleSelect }
|
|
/>
|
|
}
|
|
|
|
{ activeTab === TAB_EDIT && selectedItem &&
|
|
<FurniEditorEditView
|
|
item={ selectedItem }
|
|
furniDataEntry={ furniDataEntry }
|
|
interactions={ interactions }
|
|
loading={ loading }
|
|
onUpdate={ updateItem }
|
|
onDelete={ deleteItem }
|
|
onBack={ handleBack }
|
|
/>
|
|
}
|
|
|
|
</NitroCardContentView>
|
|
</NitroCardView>
|
|
);
|
|
};
|