feat: add delete form inventory
This commit is contained in:
parent
6e637a29cd
commit
73f9c95e37
4 changed files with 123 additions and 3 deletions
|
|
@ -134,6 +134,12 @@ pub enum ClientMessage {
|
|||
/// The loose prop ID to unlock.
|
||||
loose_prop_id: Uuid,
|
||||
},
|
||||
|
||||
/// Permanently delete a prop from inventory (does not drop to scene).
|
||||
DeleteProp {
|
||||
/// Inventory item ID to delete.
|
||||
inventory_item_id: Uuid,
|
||||
},
|
||||
}
|
||||
|
||||
/// Server-to-client WebSocket messages.
|
||||
|
|
@ -251,6 +257,12 @@ pub enum ServerMessage {
|
|||
prop_id: Uuid,
|
||||
},
|
||||
|
||||
/// A prop was permanently deleted from inventory.
|
||||
PropDeleted {
|
||||
/// ID of the deleted inventory item.
|
||||
inventory_item_id: Uuid,
|
||||
},
|
||||
|
||||
/// A prop was updated (scale changed) - clients should update their local copy.
|
||||
PropRefresh {
|
||||
/// The updated prop with all current values.
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ use uuid::Uuid;
|
|||
|
||||
use chattyness_db::{
|
||||
models::{ActionType, AvatarRenderData, ChannelMemberWithAvatar, EmotionState, ForcedAvatarReason, User},
|
||||
queries::{avatars, channel_members, loose_props, memberships, moderation, realm_avatars, realms, scenes, server_avatars, users},
|
||||
queries::{avatars, channel_members, inventory, loose_props, memberships, moderation, realm_avatars, realms, scenes, server_avatars, users},
|
||||
ws_messages::{close_codes, ClientMessage, DisconnectReason, ServerMessage, WsConfig},
|
||||
};
|
||||
use chattyness_error::AppError;
|
||||
|
|
@ -711,6 +711,37 @@ async fn handle_socket(
|
|||
}
|
||||
}
|
||||
}
|
||||
ClientMessage::DeleteProp { inventory_item_id } => {
|
||||
match inventory::drop_inventory_item(
|
||||
&mut *recv_conn,
|
||||
user_id,
|
||||
inventory_item_id,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(()) => {
|
||||
let _ = direct_tx.send(ServerMessage::PropDeleted {
|
||||
inventory_item_id,
|
||||
}).await;
|
||||
}
|
||||
Err(e) => {
|
||||
let (code, message) = match &e {
|
||||
chattyness_error::AppError::Forbidden(msg) => (
|
||||
"PROP_NOT_DELETABLE".to_string(),
|
||||
msg.clone(),
|
||||
),
|
||||
chattyness_error::AppError::NotFound(msg) => {
|
||||
("PROP_NOT_FOUND".to_string(), msg.clone())
|
||||
}
|
||||
_ => (
|
||||
"DELETE_FAILED".to_string(),
|
||||
format!("{:?}", e),
|
||||
),
|
||||
};
|
||||
let _ = direct_tx.send(ServerMessage::Error { code, message }).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
ClientMessage::PickUpProp { loose_prop_id } => {
|
||||
// Check if prop is locked
|
||||
let is_mod = match memberships::is_moderator(&pool, user_id, realm_id).await {
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ use chattyness_db::models::{InventoryItem, PropAcquisitionInfo};
|
|||
#[cfg(feature = "hydrate")]
|
||||
use chattyness_db::ws_messages::ClientMessage;
|
||||
|
||||
use super::modals::{GuestLockedOverlay, Modal};
|
||||
use super::modals::{ConfirmModal, GuestLockedOverlay, Modal};
|
||||
use super::tabs::{Tab, TabBar};
|
||||
use super::ws_client::WsSender;
|
||||
|
||||
|
|
@ -45,6 +45,8 @@ pub fn InventoryPopup(
|
|||
let (error, set_error) = signal(Option::<String>::None);
|
||||
let (selected_item, set_selected_item) = signal(Option::<Uuid>::None);
|
||||
let (dropping, set_dropping) = signal(false);
|
||||
let (deleting, set_deleting) = signal(false);
|
||||
let (delete_confirm_item, set_delete_confirm_item) = signal(Option::<(Uuid, String)>::None);
|
||||
|
||||
// Server props state (with acquisition info for authenticated users)
|
||||
let (server_props, set_server_props) = signal(Vec::<PropAcquisitionInfo>::new());
|
||||
|
|
@ -244,6 +246,33 @@ pub fn InventoryPopup(
|
|||
#[cfg(not(feature = "hydrate"))]
|
||||
let handle_drop = |_item_id: Uuid| {};
|
||||
|
||||
// Handle delete action via WebSocket (permanent deletion)
|
||||
#[cfg(feature = "hydrate")]
|
||||
let handle_delete = {
|
||||
move |item_id: Uuid| {
|
||||
set_deleting.set(true);
|
||||
ws_sender.with_value(|sender| {
|
||||
if let Some(send_fn) = sender {
|
||||
send_fn(ClientMessage::DeleteProp {
|
||||
inventory_item_id: item_id,
|
||||
});
|
||||
// Optimistically remove from local list
|
||||
set_items.update(|items| {
|
||||
items.retain(|i| i.id != item_id);
|
||||
});
|
||||
set_selected_item.set(None);
|
||||
set_delete_confirm_item.set(None);
|
||||
} else {
|
||||
set_error.set(Some("Not connected to server".to_string()));
|
||||
}
|
||||
});
|
||||
set_deleting.set(false);
|
||||
}
|
||||
};
|
||||
|
||||
#[cfg(not(feature = "hydrate"))]
|
||||
let handle_delete = |_item_id: Uuid| {};
|
||||
|
||||
view! {
|
||||
<Modal
|
||||
open=open
|
||||
|
|
@ -277,6 +306,10 @@ pub fn InventoryPopup(
|
|||
set_selected_item=set_selected_item
|
||||
dropping=dropping
|
||||
on_drop=Callback::new(handle_drop)
|
||||
deleting=deleting
|
||||
on_delete_request=Callback::new(move |(id, name)| {
|
||||
set_delete_confirm_item.set(Some((id, name)));
|
||||
})
|
||||
/>
|
||||
</Show>
|
||||
|
||||
|
|
@ -324,6 +357,25 @@ pub fn InventoryPopup(
|
|||
<Show when=move || is_guest.get()>
|
||||
<GuestLockedOverlay />
|
||||
</Show>
|
||||
|
||||
// Delete confirmation modal
|
||||
{move || {
|
||||
delete_confirm_item.get().map(|(item_id, item_name)| {
|
||||
view! {
|
||||
<ConfirmModal
|
||||
open=Signal::derive(|| true)
|
||||
title="Delete Prop?"
|
||||
message=format!("Permanently delete '{}'? This cannot be undone.", item_name)
|
||||
confirm_text="Delete"
|
||||
cancel_text="Cancel"
|
||||
destructive=true
|
||||
pending=Signal::derive(move || deleting.get())
|
||||
on_confirm=Callback::new(move |_| handle_delete(item_id))
|
||||
on_cancel=Callback::new(move |_| set_delete_confirm_item.set(None))
|
||||
/>
|
||||
}
|
||||
})
|
||||
}}
|
||||
</div>
|
||||
</Modal>
|
||||
}
|
||||
|
|
@ -339,6 +391,8 @@ fn MyInventoryTab(
|
|||
set_selected_item: WriteSignal<Option<Uuid>>,
|
||||
#[prop(into)] dropping: Signal<bool>,
|
||||
#[prop(into)] on_drop: Callback<Uuid>,
|
||||
#[prop(into)] deleting: Signal<bool>,
|
||||
#[prop(into)] on_delete_request: Callback<(Uuid, String)>,
|
||||
) -> impl IntoView {
|
||||
view! {
|
||||
// Loading state
|
||||
|
|
@ -423,8 +477,11 @@ fn MyInventoryTab(
|
|||
let item_id = selected_item.get()?;
|
||||
let item = items.get().into_iter().find(|i| i.id == item_id)?;
|
||||
let on_drop = on_drop.clone();
|
||||
let on_delete_request = on_delete_request.clone();
|
||||
let is_dropping = dropping.get();
|
||||
let is_deleting = deleting.get();
|
||||
let is_droppable = item.is_droppable;
|
||||
let item_name = item.prop_name.clone();
|
||||
|
||||
Some(view! {
|
||||
<div class="mt-4 pt-4 border-t border-gray-700">
|
||||
|
|
@ -452,10 +509,25 @@ fn MyInventoryTab(
|
|||
}
|
||||
}
|
||||
disabled=is_dropping || !is_droppable
|
||||
title=if is_droppable { "" } else { "Essential prop cannot be dropped" }
|
||||
title=if is_droppable { "Drop prop to scene canvas" } else { "Essential prop cannot be dropped" }
|
||||
>
|
||||
{if is_dropping { "Dropping..." } else { "Drop" }}
|
||||
</button>
|
||||
// Delete button - only shown for droppable props
|
||||
<Show when=move || is_droppable>
|
||||
<button
|
||||
type="button"
|
||||
class="px-4 py-2 bg-red-800 hover:bg-red-900 text-white rounded-lg transition-colors disabled:opacity-50"
|
||||
on:click={
|
||||
let name = item_name.clone();
|
||||
move |_| on_delete_request.run((item_id, name.clone()))
|
||||
}
|
||||
disabled=is_dropping || is_deleting
|
||||
title="Permanently delete this prop"
|
||||
>
|
||||
"Delete"
|
||||
</button>
|
||||
</Show>
|
||||
// Transfer button (disabled for now)
|
||||
<Show when=move || item.is_transferable>
|
||||
<button
|
||||
|
|
|
|||
|
|
@ -662,6 +662,11 @@ fn handle_server_message(
|
|||
// Treat expired props the same as picked up (remove from display)
|
||||
PostAction::PropPickedUp(prop_id)
|
||||
}
|
||||
ServerMessage::PropDeleted { inventory_item_id: _ } => {
|
||||
// Inventory deletion is handled optimistically in the UI
|
||||
// No scene state change needed
|
||||
PostAction::None
|
||||
}
|
||||
ServerMessage::PropRefresh { prop } => {
|
||||
PostAction::PropRefresh(prop)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue