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.
|
/// The loose prop ID to unlock.
|
||||||
loose_prop_id: Uuid,
|
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.
|
/// Server-to-client WebSocket messages.
|
||||||
|
|
@ -251,6 +257,12 @@ pub enum ServerMessage {
|
||||||
prop_id: Uuid,
|
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.
|
/// A prop was updated (scale changed) - clients should update their local copy.
|
||||||
PropRefresh {
|
PropRefresh {
|
||||||
/// The updated prop with all current values.
|
/// The updated prop with all current values.
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ use uuid::Uuid;
|
||||||
|
|
||||||
use chattyness_db::{
|
use chattyness_db::{
|
||||||
models::{ActionType, AvatarRenderData, ChannelMemberWithAvatar, EmotionState, ForcedAvatarReason, User},
|
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},
|
ws_messages::{close_codes, ClientMessage, DisconnectReason, ServerMessage, WsConfig},
|
||||||
};
|
};
|
||||||
use chattyness_error::AppError;
|
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 } => {
|
ClientMessage::PickUpProp { loose_prop_id } => {
|
||||||
// Check if prop is locked
|
// Check if prop is locked
|
||||||
let is_mod = match memberships::is_moderator(&pool, user_id, realm_id).await {
|
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")]
|
#[cfg(feature = "hydrate")]
|
||||||
use chattyness_db::ws_messages::ClientMessage;
|
use chattyness_db::ws_messages::ClientMessage;
|
||||||
|
|
||||||
use super::modals::{GuestLockedOverlay, Modal};
|
use super::modals::{ConfirmModal, GuestLockedOverlay, Modal};
|
||||||
use super::tabs::{Tab, TabBar};
|
use super::tabs::{Tab, TabBar};
|
||||||
use super::ws_client::WsSender;
|
use super::ws_client::WsSender;
|
||||||
|
|
||||||
|
|
@ -45,6 +45,8 @@ pub fn InventoryPopup(
|
||||||
let (error, set_error) = signal(Option::<String>::None);
|
let (error, set_error) = signal(Option::<String>::None);
|
||||||
let (selected_item, set_selected_item) = signal(Option::<Uuid>::None);
|
let (selected_item, set_selected_item) = signal(Option::<Uuid>::None);
|
||||||
let (dropping, set_dropping) = signal(false);
|
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)
|
// Server props state (with acquisition info for authenticated users)
|
||||||
let (server_props, set_server_props) = signal(Vec::<PropAcquisitionInfo>::new());
|
let (server_props, set_server_props) = signal(Vec::<PropAcquisitionInfo>::new());
|
||||||
|
|
@ -244,6 +246,33 @@ pub fn InventoryPopup(
|
||||||
#[cfg(not(feature = "hydrate"))]
|
#[cfg(not(feature = "hydrate"))]
|
||||||
let handle_drop = |_item_id: Uuid| {};
|
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! {
|
view! {
|
||||||
<Modal
|
<Modal
|
||||||
open=open
|
open=open
|
||||||
|
|
@ -277,6 +306,10 @@ pub fn InventoryPopup(
|
||||||
set_selected_item=set_selected_item
|
set_selected_item=set_selected_item
|
||||||
dropping=dropping
|
dropping=dropping
|
||||||
on_drop=Callback::new(handle_drop)
|
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>
|
</Show>
|
||||||
|
|
||||||
|
|
@ -324,6 +357,25 @@ pub fn InventoryPopup(
|
||||||
<Show when=move || is_guest.get()>
|
<Show when=move || is_guest.get()>
|
||||||
<GuestLockedOverlay />
|
<GuestLockedOverlay />
|
||||||
</Show>
|
</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>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
}
|
}
|
||||||
|
|
@ -339,6 +391,8 @@ fn MyInventoryTab(
|
||||||
set_selected_item: WriteSignal<Option<Uuid>>,
|
set_selected_item: WriteSignal<Option<Uuid>>,
|
||||||
#[prop(into)] dropping: Signal<bool>,
|
#[prop(into)] dropping: Signal<bool>,
|
||||||
#[prop(into)] on_drop: Callback<Uuid>,
|
#[prop(into)] on_drop: Callback<Uuid>,
|
||||||
|
#[prop(into)] deleting: Signal<bool>,
|
||||||
|
#[prop(into)] on_delete_request: Callback<(Uuid, String)>,
|
||||||
) -> impl IntoView {
|
) -> impl IntoView {
|
||||||
view! {
|
view! {
|
||||||
// Loading state
|
// Loading state
|
||||||
|
|
@ -423,8 +477,11 @@ fn MyInventoryTab(
|
||||||
let item_id = selected_item.get()?;
|
let item_id = selected_item.get()?;
|
||||||
let item = items.get().into_iter().find(|i| i.id == item_id)?;
|
let item = items.get().into_iter().find(|i| i.id == item_id)?;
|
||||||
let on_drop = on_drop.clone();
|
let on_drop = on_drop.clone();
|
||||||
|
let on_delete_request = on_delete_request.clone();
|
||||||
let is_dropping = dropping.get();
|
let is_dropping = dropping.get();
|
||||||
|
let is_deleting = deleting.get();
|
||||||
let is_droppable = item.is_droppable;
|
let is_droppable = item.is_droppable;
|
||||||
|
let item_name = item.prop_name.clone();
|
||||||
|
|
||||||
Some(view! {
|
Some(view! {
|
||||||
<div class="mt-4 pt-4 border-t border-gray-700">
|
<div class="mt-4 pt-4 border-t border-gray-700">
|
||||||
|
|
@ -452,10 +509,25 @@ fn MyInventoryTab(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
disabled=is_dropping || !is_droppable
|
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" }}
|
{if is_dropping { "Dropping..." } else { "Drop" }}
|
||||||
</button>
|
</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)
|
// Transfer button (disabled for now)
|
||||||
<Show when=move || item.is_transferable>
|
<Show when=move || item.is_transferable>
|
||||||
<button
|
<button
|
||||||
|
|
|
||||||
|
|
@ -662,6 +662,11 @@ fn handle_server_message(
|
||||||
// Treat expired props the same as picked up (remove from display)
|
// Treat expired props the same as picked up (remove from display)
|
||||||
PostAction::PropPickedUp(prop_id)
|
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 } => {
|
ServerMessage::PropRefresh { prop } => {
|
||||||
PostAction::PropRefresh(prop)
|
PostAction::PropRefresh(prop)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue