Move command metadata authority to server
This commit is contained in:
@@ -17,6 +17,22 @@ export type ItemPropertyMetadata = {
|
||||
};
|
||||
|
||||
type UiDefinitionsPayload = {
|
||||
commandMetadata?: {
|
||||
mainModeActions?: Array<{
|
||||
id: string;
|
||||
label?: string;
|
||||
tooltip?: string;
|
||||
}>;
|
||||
};
|
||||
itemManagement?: {
|
||||
actions?: Array<{
|
||||
id: string;
|
||||
label?: string;
|
||||
tooltip?: string;
|
||||
anyPermission?: string;
|
||||
ownPermission?: string;
|
||||
}>;
|
||||
};
|
||||
itemTypeOrder?: ItemType[];
|
||||
itemTypes?: Array<{
|
||||
type: ItemType;
|
||||
@@ -28,6 +44,17 @@ type UiDefinitionsPayload = {
|
||||
globalProperties?: Record<string, unknown>;
|
||||
}>;
|
||||
};
|
||||
|
||||
type ServerCommandMetadata = {
|
||||
label?: string;
|
||||
tooltip?: string;
|
||||
};
|
||||
|
||||
type ItemManagementActionMetadata = ServerCommandMetadata & {
|
||||
anyPermission?: string;
|
||||
ownPermission?: string;
|
||||
};
|
||||
|
||||
let itemTypeSequence: ItemType[] = [];
|
||||
let itemTypeLabels: Partial<Record<ItemType, string>> = {};
|
||||
let itemTypeTooltips: Partial<Record<ItemType, string>> = {};
|
||||
@@ -36,6 +63,8 @@ let itemTypeCapabilities: Partial<Record<ItemType, string[]>> = {};
|
||||
let itemTypeGlobalProperties: Partial<Record<ItemType, Record<string, string | number | boolean>>> = {};
|
||||
let itemTypePropertyMetadata: Partial<Record<ItemType, Record<string, ItemPropertyMetadata>>> = {};
|
||||
let propertyLabelByKey: Record<string, string> = {};
|
||||
let mainModeCommandMetadataById: Record<string, ServerCommandMetadata> = {};
|
||||
let itemManagementActionMetadataById: Record<string, ItemManagementActionMetadata> = {};
|
||||
|
||||
export let EDITABLE_ITEM_PROPERTY_KEYS = new Set<string>(
|
||||
Object.values(itemTypeEditableProperties).flatMap((keys) => keys ?? []),
|
||||
@@ -150,6 +179,16 @@ export function getItemTypeCapabilities(itemType: ItemType): string[] {
|
||||
return [...(itemTypeCapabilities[itemType] ?? [])];
|
||||
}
|
||||
|
||||
/** Returns server-authored metadata for one server-backed main-mode command id. */
|
||||
export function getServerMainModeCommandMetadata(commandId: string): ServerCommandMetadata | undefined {
|
||||
return mainModeCommandMetadataById[commandId];
|
||||
}
|
||||
|
||||
/** Returns server-authored metadata for one item-management action id. */
|
||||
export function getItemManagementActionMetadata(actionId: string): ItemManagementActionMetadata | undefined {
|
||||
return itemManagementActionMetadataById[actionId];
|
||||
}
|
||||
|
||||
/** Returns human-facing label for a property key. */
|
||||
export function itemPropertyLabel(key: string): string {
|
||||
const metadataLabel = propertyLabelByKey[key];
|
||||
@@ -237,6 +276,8 @@ export function applyServerItemUiDefinitions(uiDefinitions: UiDefinitionsPayload
|
||||
itemTypeGlobalProperties = {};
|
||||
itemTypePropertyMetadata = {};
|
||||
propertyLabelByKey = {};
|
||||
mainModeCommandMetadataById = {};
|
||||
itemManagementActionMetadataById = {};
|
||||
rebuildEditablePropertyKeySet();
|
||||
return false;
|
||||
}
|
||||
@@ -253,6 +294,8 @@ export function applyServerItemUiDefinitions(uiDefinitions: UiDefinitionsPayload
|
||||
const nextGlobals: Partial<Record<ItemType, Record<string, string | number | boolean>>> = {};
|
||||
const nextPropertyMetadata: Partial<Record<ItemType, Record<string, ItemPropertyMetadata>>> = {};
|
||||
const nextPropertyLabels: Record<string, string> = {};
|
||||
const nextMainModeCommandMetadata: Record<string, ServerCommandMetadata> = {};
|
||||
const nextItemManagementActionMetadata: Record<string, ItemManagementActionMetadata> = {};
|
||||
|
||||
for (const definition of uiDefinitions.itemTypes) {
|
||||
if (!definition || typeof definition.type !== 'string') continue;
|
||||
@@ -295,6 +338,28 @@ export function applyServerItemUiDefinitions(uiDefinitions: UiDefinitionsPayload
|
||||
discoveredOrder.push(definition.type as ItemType);
|
||||
}
|
||||
|
||||
for (const action of uiDefinitions.commandMetadata?.mainModeActions ?? []) {
|
||||
const id = String(action?.id ?? '').trim();
|
||||
if (!id) continue;
|
||||
nextMainModeCommandMetadata[id] = {
|
||||
label: typeof action?.label === 'string' && action.label.trim().length > 0 ? action.label.trim() : undefined,
|
||||
tooltip: typeof action?.tooltip === 'string' && action.tooltip.trim().length > 0 ? action.tooltip.trim() : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
for (const action of uiDefinitions.itemManagement?.actions ?? []) {
|
||||
const id = String(action?.id ?? '').trim();
|
||||
if (!id) continue;
|
||||
nextItemManagementActionMetadata[id] = {
|
||||
label: typeof action?.label === 'string' && action.label.trim().length > 0 ? action.label.trim() : undefined,
|
||||
tooltip: typeof action?.tooltip === 'string' && action.tooltip.trim().length > 0 ? action.tooltip.trim() : undefined,
|
||||
anyPermission:
|
||||
typeof action?.anyPermission === 'string' && action.anyPermission.trim().length > 0 ? action.anyPermission.trim() : undefined,
|
||||
ownPermission:
|
||||
typeof action?.ownPermission === 'string' && action.ownPermission.trim().length > 0 ? action.ownPermission.trim() : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
itemTypeLabels = nextLabels;
|
||||
itemTypeTooltips = nextTooltips;
|
||||
itemTypeEditableProperties = nextEditable;
|
||||
@@ -302,6 +367,8 @@ export function applyServerItemUiDefinitions(uiDefinitions: UiDefinitionsPayload
|
||||
itemTypeGlobalProperties = nextGlobals;
|
||||
itemTypePropertyMetadata = nextPropertyMetadata;
|
||||
propertyLabelByKey = nextPropertyLabels;
|
||||
mainModeCommandMetadataById = nextMainModeCommandMetadata;
|
||||
itemManagementActionMetadataById = nextItemManagementActionMetadata;
|
||||
itemTypeSequence = explicitOrder ?? discoveredOrder;
|
||||
rebuildEditablePropertyKeySet();
|
||||
return itemTypeSequence.length > 0;
|
||||
|
||||
@@ -47,6 +47,8 @@ import {
|
||||
} from './state/gameState';
|
||||
import {
|
||||
applyServerItemUiDefinitions,
|
||||
getItemManagementActionMetadata,
|
||||
getServerMainModeCommandMetadata,
|
||||
getItemTypeGlobalProperties,
|
||||
getItemTypeSequence,
|
||||
getEditableItemPropertyKeys,
|
||||
@@ -192,6 +194,7 @@ type AuthPolicy = {
|
||||
type AdminMenuAction = {
|
||||
id: string;
|
||||
label: string;
|
||||
tooltip?: string;
|
||||
};
|
||||
|
||||
type AdminRoleSummary = {
|
||||
@@ -220,6 +223,7 @@ type ItemManagementAction = 'delete' | 'transfer';
|
||||
type ItemManagementOption = {
|
||||
action: ItemManagementAction;
|
||||
label: string;
|
||||
tooltip?: string;
|
||||
};
|
||||
|
||||
type ItemManagementConfirmContext = {
|
||||
@@ -650,11 +654,12 @@ function applyAuthPermissions(role: string | null | undefined, permissions: stri
|
||||
}
|
||||
|
||||
/** Applies server-authored admin menu actions for current session. */
|
||||
function applyServerAdminMenuActions(actions: Array<{ id: string; label: string }> | null | undefined): void {
|
||||
function applyServerAdminMenuActions(actions: Array<{ id: string; label: string; tooltip?: string }> | null | undefined): void {
|
||||
serverAdminMenuActions = (actions || [])
|
||||
.map((entry) => ({
|
||||
id: String(entry.id || '').trim(),
|
||||
label: String(entry.label || '').trim(),
|
||||
tooltip: typeof entry.tooltip === 'string' && entry.tooltip.trim().length > 0 ? entry.tooltip.trim() : undefined,
|
||||
}))
|
||||
.filter((entry) => entry.id.length > 0 && entry.label.length > 0);
|
||||
}
|
||||
@@ -1064,24 +1069,36 @@ function beginItemSelection(
|
||||
|
||||
/** Returns whether the local user can delete the provided item. */
|
||||
function canManageDeleteItem(item: WorldItem): boolean {
|
||||
if (hasPermission('item.delete.any')) return true;
|
||||
return hasPermission('item.delete.own') && authUserId.length > 0 && item.createdBy === authUserId;
|
||||
const metadata = getItemManagementActionMetadata('delete');
|
||||
if (metadata?.anyPermission && hasPermission(metadata.anyPermission)) return true;
|
||||
return Boolean(metadata?.ownPermission) && hasPermission(metadata.ownPermission) && authUserId.length > 0 && item.createdBy === authUserId;
|
||||
}
|
||||
|
||||
/** Returns whether the local user can transfer the provided item. */
|
||||
function canManageTransferItem(item: WorldItem): boolean {
|
||||
if (hasPermission('item.transfer.any')) return true;
|
||||
return hasPermission('item.transfer.own') && authUserId.length > 0 && item.createdBy === authUserId;
|
||||
const metadata = getItemManagementActionMetadata('transfer');
|
||||
if (metadata?.anyPermission && hasPermission(metadata.anyPermission)) return true;
|
||||
return Boolean(metadata?.ownPermission) && hasPermission(metadata.ownPermission) && authUserId.length > 0 && item.createdBy === authUserId;
|
||||
}
|
||||
|
||||
/** Builds available item-management actions for one selected item. */
|
||||
function itemManagementOptionsFor(item: WorldItem): ItemManagementOption[] {
|
||||
const options: ItemManagementOption[] = [];
|
||||
const transferMetadata = getItemManagementActionMetadata('transfer');
|
||||
if (canManageTransferItem(item) && (state.player.id !== null || state.peers.size > 0)) {
|
||||
options.push({ action: 'transfer', label: 'Transfer item' });
|
||||
options.push({
|
||||
action: 'transfer',
|
||||
label: transferMetadata?.label ?? 'Transfer item',
|
||||
tooltip: transferMetadata?.tooltip,
|
||||
});
|
||||
}
|
||||
const deleteMetadata = getItemManagementActionMetadata('delete');
|
||||
if (canManageDeleteItem(item)) {
|
||||
options.push({ action: 'delete', label: 'Delete item' });
|
||||
options.push({
|
||||
action: 'delete',
|
||||
label: deleteMetadata?.label ?? 'Delete item',
|
||||
tooltip: deleteMetadata?.tooltip,
|
||||
});
|
||||
}
|
||||
return options;
|
||||
}
|
||||
@@ -2570,6 +2587,8 @@ function getAvailableCommandPaletteEntriesForMode(mode: GameMode): Array<Command
|
||||
});
|
||||
return descriptors.map((descriptor) => ({
|
||||
...descriptor,
|
||||
label: getServerMainModeCommandMetadata(descriptor.id)?.label ?? descriptor.label,
|
||||
tooltip: getServerMainModeCommandMetadata(descriptor.id)?.tooltip ?? descriptor.tooltip,
|
||||
run: mainModeCommandHandlers[descriptor.id],
|
||||
}));
|
||||
}
|
||||
@@ -3054,6 +3073,11 @@ function handleItemManageOptionsModeInput(code: string, key: string): void {
|
||||
audio.sfxUiBlip();
|
||||
return;
|
||||
}
|
||||
if (code === 'Space') {
|
||||
updateStatus(itemManagementOptions[itemManagementOptionIndex]?.tooltip ?? 'No tooltip available.');
|
||||
audio.sfxUiBlip();
|
||||
return;
|
||||
}
|
||||
if (control.type === 'select') {
|
||||
const option = itemManagementOptions[itemManagementOptionIndex];
|
||||
if (option.action === 'delete') {
|
||||
@@ -3176,6 +3200,11 @@ function handleAdminMenuModeInput(code: string, key: string): void {
|
||||
audio.sfxUiBlip();
|
||||
return;
|
||||
}
|
||||
if (code === 'Space') {
|
||||
updateStatus(adminMenuActions[adminMenuIndex]?.tooltip ?? 'No tooltip available.');
|
||||
audio.sfxUiBlip();
|
||||
return;
|
||||
}
|
||||
if (control.type === 'select') {
|
||||
const selected = adminMenuActions[adminMenuIndex];
|
||||
if (!selected) return;
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
const actionMetadataSchema = z.object({
|
||||
id: z.string(),
|
||||
label: z.string(),
|
||||
tooltip: z.string().optional(),
|
||||
});
|
||||
|
||||
export const itemSchema = z.object({
|
||||
id: z.string(),
|
||||
type: z.string().min(1),
|
||||
@@ -59,7 +65,7 @@ export const welcomeMessageSchema = z.object({
|
||||
username: z.string().nullable().optional(),
|
||||
role: z.string().nullable().optional(),
|
||||
permissions: z.array(z.string()).optional(),
|
||||
adminMenuActions: z.array(z.object({ id: z.string(), label: z.string() })).optional(),
|
||||
adminMenuActions: z.array(actionMetadataSchema).optional(),
|
||||
policy: z
|
||||
.object({
|
||||
usernameMinLength: z.number().int().positive(),
|
||||
@@ -105,7 +111,22 @@ export const welcomeMessageSchema = z.object({
|
||||
),
|
||||
adminMenu: z
|
||||
.object({
|
||||
actions: z.array(z.object({ id: z.string(), label: z.string() })),
|
||||
actions: z.array(actionMetadataSchema),
|
||||
})
|
||||
.optional(),
|
||||
commandMetadata: z
|
||||
.object({
|
||||
mainModeActions: z.array(actionMetadataSchema).optional(),
|
||||
})
|
||||
.optional(),
|
||||
itemManagement: z
|
||||
.object({
|
||||
actions: z.array(
|
||||
actionMetadataSchema.extend({
|
||||
anyPermission: z.string().optional(),
|
||||
ownPermission: z.string().optional(),
|
||||
}),
|
||||
),
|
||||
})
|
||||
.optional(),
|
||||
})
|
||||
@@ -133,7 +154,7 @@ export const authResultSchema = z.object({
|
||||
username: z.string().optional(),
|
||||
role: z.string().optional(),
|
||||
permissions: z.array(z.string()).optional(),
|
||||
adminMenuActions: z.array(z.object({ id: z.string(), label: z.string() })).optional(),
|
||||
adminMenuActions: z.array(actionMetadataSchema).optional(),
|
||||
nickname: z.string().optional(),
|
||||
authPolicy: z
|
||||
.object({
|
||||
@@ -289,7 +310,7 @@ export const authPermissionsSchema = z.object({
|
||||
type: z.literal('auth_permissions'),
|
||||
role: z.string(),
|
||||
permissions: z.array(z.string()),
|
||||
adminMenuActions: z.array(z.object({ id: z.string(), label: z.string() })).optional(),
|
||||
adminMenuActions: z.array(actionMetadataSchema).optional(),
|
||||
});
|
||||
|
||||
const adminRoleSummarySchema = z.object({
|
||||
|
||||
Reference in New Issue
Block a user