diff --git a/hac-client/src/pages/collection_viewer/collection_store.rs b/hac-client/src/pages/collection_viewer/collection_store.rs index 0156715..67c452e 100755 --- a/hac-client/src/pages/collection_viewer/collection_store.rs +++ b/hac-client/src/pages/collection_viewer/collection_store.rs @@ -26,6 +26,7 @@ pub struct CollectionStore { state: Option>>, } +#[derive(Debug)] pub enum CollectionStoreAction { SetSelectedRequest(Option>>), SetHoveredRequest(Option), @@ -199,10 +200,18 @@ impl CollectionStore { } fn maybe_hover_prev(&mut self) { - if self.get_hovered_request().is_some() && self.get_requests().is_some() { - let id = self.get_hovered_request().unwrap(); + if self.get_requests().is_some() { let requests = self.get_requests().unwrap(); + let Some(id) = self.get_hovered_request() else { + tracing::debug!("{:?}", self.get_hovered_request()); + self.dispatch(CollectionStoreAction::SetHoveredRequest( + requests.read().unwrap().first().map(|req| req.get_id()), + )); + tracing::debug!("{:?}", self.get_hovered_request()); + return; + }; + if let Some(next) = find_next_entry( &requests.read().unwrap(), VisitNode::Prev, @@ -217,10 +226,18 @@ impl CollectionStore { } fn maybe_hover_next(&mut self) { - if self.get_hovered_request().is_some() && self.get_requests().is_some() { - let id = self.get_hovered_request().unwrap(); + if self.get_requests().is_some() { let requests = self.get_requests().unwrap(); + let Some(id) = self.get_hovered_request() else { + tracing::debug!("{:?}", self.get_hovered_request()); + self.dispatch(CollectionStoreAction::SetHoveredRequest( + requests.read().unwrap().first().map(|req| req.get_id()), + )); + tracing::debug!("{:?}", self.get_hovered_request()); + return; + }; + if let Some(next) = find_next_entry( &requests.read().unwrap(), VisitNode::Next, @@ -241,6 +258,28 @@ impl CollectionStore { self.get_hovered_request().as_ref().unwrap(), ) } + + pub fn remove_item(&mut self, item_id: String) { + if let Some(request) = self.get_selected_request() { + if request.read().unwrap().id.eq(&item_id) { + self.dispatch(CollectionStoreAction::SetSelectedRequest(None)); + } + } + let mut requests = self.get_requests(); + let mut requests = requests.as_mut().unwrap().write().unwrap(); + requests.retain(|req| req.get_id().ne(&item_id)); + requests.iter_mut().for_each(|req| { + if let RequestKind::Nested(dir) = req { + dir.requests + .write() + .unwrap() + .retain(|child| child.get_id().ne(&item_id)); + } + }); + self.dispatch(CollectionStoreAction::SetHoveredRequest( + requests.first().map(|req| req.get_id()), + )); + } } #[derive(PartialEq)] @@ -276,6 +315,8 @@ fn traverse( // We are looking for the next and just found the current one, so we set the flag to // true in order to know when to return the next. (VisitNode::Next, true, false) => *found = true, + // we are looking for the current node and we found it, so we set the flag to true + // and return immediatly (VisitNode::Curr, true, _) => { path.push(current.clone()); *found = true; diff --git a/hac-client/src/pages/collection_viewer/collection_viewer.rs b/hac-client/src/pages/collection_viewer/collection_viewer.rs index e71b659..48c661c 100755 --- a/hac-client/src/pages/collection_viewer/collection_viewer.rs +++ b/hac-client/src/pages/collection_viewer/collection_viewer.rs @@ -31,7 +31,7 @@ pub struct ExplorerLayout { pub create_req_form: Rect, } -#[derive(Debug, Clone)] +#[derive(Debug, PartialEq, Eq, Clone)] pub enum CollectionViewerOverlay { None, CreateRequest, @@ -41,6 +41,7 @@ pub enum CollectionViewerOverlay { HeadersHelp, HeadersDelete, HeadersForm(usize), + DeleteSidebarItem(String), } #[derive(Debug, Clone, Copy, PartialEq)] @@ -79,6 +80,7 @@ pub struct CollectionViewer<'cv> { sidebar: Sidebar<'cv>, colors: &'cv hac_colors::Colors, + config: &'cv hac_config::Config, layout: ExplorerLayout, global_command_sender: Option>, collection_sync_timer: std::time::Instant, @@ -123,6 +125,7 @@ impl<'cv> CollectionViewer<'cv> { request_uri, colors, layout, + config, global_command_sender: None, collection_sync_timer: std::time::Instant::now(), responses_map: HashMap::default(), @@ -133,6 +136,27 @@ impl<'cv> CollectionViewer<'cv> { } } + fn rebuild_everything(&mut self) { + self.sidebar = sidebar::Sidebar::new(self.colors, self.collection_store.clone()); + self.request_editor = RequestEditor::new( + self.colors, + self.config, + self.collection_store.clone(), + self.layout.req_editor, + ); + self.response_viewer = ResponseViewer::new( + self.colors, + self.collection_store.clone(), + None, + self.layout.response_preview, + ); + self.request_uri = RequestUri::new( + self.colors, + self.collection_store.clone(), + self.layout.req_uri, + ); + } + // collect all pending responses from the channel. Here, I don't see a way we // may have more than one response on this channel at any point, but it shouldn't matter // if we have, so we can drain all the responses and update accordingly @@ -271,6 +295,9 @@ impl Renderable for CollectionViewer<'_> { CollectionViewerOverlay::EditRequest => { self.sidebar.draw_overlay(frame, overlay)?; } + CollectionViewerOverlay::DeleteSidebarItem(_) => { + self.sidebar.draw_overlay(frame, overlay)?; + } CollectionViewerOverlay::HeadersHelp => { self.request_editor.draw_overlay(frame, overlay)?; } @@ -409,9 +436,14 @@ impl Eventful for CollectionViewer<'_> { .collection_store .borrow_mut() .push_overlay(CollectionViewerOverlay::CreateDirectory), + Some(SidebarEvent::DeleteItem(item_id)) => self + .collection_store + .borrow_mut() + .push_overlay(CollectionViewerOverlay::DeleteSidebarItem(item_id)), Some(SidebarEvent::RemoveSelection) => self.update_selection(None), Some(SidebarEvent::SyncCollection) => self.sync_collection_changes(), Some(SidebarEvent::Quit) => return Ok(Some(Command::Quit)), + Some(SidebarEvent::RebuildView) => self.rebuild_everything(), // when theres no event we do nothing None => {} }, diff --git a/hac-client/src/pages/collection_viewer/request_editor.rs b/hac-client/src/pages/collection_viewer/request_editor.rs index 7cec024..078a672 100755 --- a/hac-client/src/pages/collection_viewer/request_editor.rs +++ b/hac-client/src/pages/collection_viewer/request_editor.rs @@ -99,7 +99,6 @@ pub struct RequestEditor<'re> { body_editor: BodyEditor<'re>, headers_editor: HeadersEditor<'re>, auth_editor: AuthEditor<'re>, - layout: ReqEditorLayout, curr_tab: ReqEditorTabs, } diff --git a/hac-client/src/pages/collection_viewer/request_editor/body_editor.rs b/hac-client/src/pages/collection_viewer/request_editor/body_editor.rs index b6c9f68..641939a 100755 --- a/hac-client/src/pages/collection_viewer/request_editor/body_editor.rs +++ b/hac-client/src/pages/collection_viewer/request_editor/body_editor.rs @@ -43,6 +43,7 @@ pub struct BodyEditor<'be> { /// Only KeyAction::Complex are stored here as any other kind of key action can be acted upon /// instantly keymap_buffer: Option, + collection_store: Rc>, } impl<'be> BodyEditor<'be> { @@ -52,25 +53,14 @@ impl<'be> BodyEditor<'be> { collection_store: Rc>, size: Rect, ) -> Self { - let (body, tree) = if let Some(request) = collection_store.borrow().get_selected_request() { - if let Some(body) = request.read().unwrap().body.as_ref() { - let mut highlighter = HIGHLIGHTER.write().unwrap(); - let tree = highlighter.parse(body); - - (TextObject::from(body).with_write(), tree) - } else { - Default::default() - } - } else { - Default::default() - }; - + let (body, tree) = make_body(&collection_store); let content = body.to_string(); let styled_display = build_syntax_highlighted_lines(&content, tree.as_ref(), colors); Self { body, tree, + collection_store, styled_display, cursor: Cursor::default(), editor_mode: EditorMode::Normal, @@ -662,3 +652,20 @@ fn get_visible_spans(line: &Line<'static>, scroll: usize) -> Line<'static> { Line::from(new_spans) } + +fn make_body(collection_store: &Rc>) -> (TextObject, Option) { + let (body, tree) = if let Some(request) = collection_store.borrow().get_selected_request() { + if let Some(body) = request.read().unwrap().body.as_ref() { + let mut highlighter = HIGHLIGHTER.write().unwrap(); + let tree = highlighter.parse(body); + + (TextObject::from(body).with_write(), tree) + } else { + Default::default() + } + } else { + Default::default() + }; + + (body, tree) +} diff --git a/hac-client/src/pages/collection_viewer/sidebar.rs b/hac-client/src/pages/collection_viewer/sidebar.rs index a57a8d5..51bb3bd 100755 --- a/hac-client/src/pages/collection_viewer/sidebar.rs +++ b/hac-client/src/pages/collection_viewer/sidebar.rs @@ -1,5 +1,6 @@ mod create_directory_form; mod create_request_form; +mod delete_item_prompt; mod edit_request_form; mod request_form; mod select_request_parent; @@ -7,6 +8,7 @@ mod select_request_parent; use hac_core::collection::types::{Request, RequestKind, RequestMethod}; use super::sidebar::create_directory_form::{CreateDirectoryForm, CreateDirectoryFormEvent}; +use super::sidebar::delete_item_prompt::{DeleteItemPrompt, DeleteItemPromptEvent}; use super::sidebar::request_form::RequestForm; use super::sidebar::request_form::RequestFormCreate; use super::sidebar::request_form::RequestFormEdit; @@ -28,7 +30,7 @@ use ratatui::widgets::{Block, Borders, Paragraph}; use ratatui::Frame; /// set of events Sidebar can emit to the caller when handling events. -#[derive(Debug)] +#[derive(Debug, PartialEq, Eq, Clone)] pub enum SidebarEvent { /// user pressed `CreateRequest (n)` hotkey, which should notify the caller to open /// the create_request_form and properly handle the creation of a new request @@ -42,9 +44,14 @@ pub enum SidebarEvent { /// user pressed `Esc` so we notify the caller to remove the selection from /// this pane, essentially bubbling the key handling scope to the caller RemoveSelection, + /// event to force a full rebuild of the view, when a request is deleted + RebuildView, /// this event is used when a request or directory is created, this notify the parent /// to sync changes with the file system. SyncCollection, + /// user pressed `DeleteItem (D)` hotkey, which should notify the caller to open the + /// delete_item_prompt to ask the user for confirmation + DeleteItem(String), /// user pressed a hotkey to quit the application, so we bubble up so the caller /// can do a few things before bubbling the quit request further up Quit, @@ -83,6 +90,7 @@ pub struct Sidebar<'sbar> { collection_store: Rc>, request_form: FormVariant<'sbar>, directory_form: CreateDirectoryForm<'sbar>, + delete_item_prompt: DeleteItemPrompt<'sbar>, } impl<'sbar> Sidebar<'sbar> { @@ -97,6 +105,7 @@ impl<'sbar> Sidebar<'sbar> { collection_store.clone(), )), directory_form: CreateDirectoryForm::new(colors, collection_store.clone()), + delete_item_prompt: DeleteItemPrompt::new(colors, collection_store.clone()), lines: vec![], collection_store, }; @@ -136,6 +145,9 @@ impl<'sbar> Sidebar<'sbar> { CollectionViewerOverlay::CreateDirectory => { self.directory_form.draw(frame, frame.size())?; } + CollectionViewerOverlay::DeleteSidebarItem(_) => { + self.delete_item_prompt.draw(frame, frame.size())?; + } _ => {} }; @@ -201,6 +213,8 @@ impl<'a> Eventful for Sidebar<'a> { let overlay = self.collection_store.borrow_mut().peek_overlay(); + tracing::debug!("{:?}", key_event.code); + match overlay { CollectionViewerOverlay::CreateRequest => { match self.request_form.inner().handle_key_event(key_event)? { @@ -268,6 +282,34 @@ impl<'a> Eventful for Sidebar<'a> { None => return Ok(None), } } + CollectionViewerOverlay::DeleteSidebarItem(item_id) => { + match self.delete_item_prompt.handle_key_event(key_event)? { + Some(DeleteItemPromptEvent::Confirm) => { + let mut store = self.collection_store.borrow_mut(); + let changed_selection = store + .get_selected_request() + .is_some_and(|req| req.read().unwrap().id.eq(&item_id)); + store.remove_item(item_id); + store.pop_overlay(); + drop(store); + self.rebuild_tree_view(); + + if changed_selection { + return Ok(Some(SidebarEvent::RebuildView)); + } else { + return Ok(None); + } + } + Some(DeleteItemPromptEvent::Cancel) => { + let mut store = self.collection_store.borrow_mut(); + store.pop_overlay(); + drop(store); + self.rebuild_tree_view(); + return Ok(None); + } + None => return Ok(None), + } + } _ => {} }; @@ -290,6 +332,7 @@ impl<'a> Eventful for Sidebar<'a> { } RequestKind::Single(req) => { store.dispatch(CollectionStoreAction::SetSelectedRequest(Some(req))); + return Ok(Some(SidebarEvent::RebuildView)); } } } @@ -306,6 +349,7 @@ impl<'a> Eventful for Sidebar<'a> { let RequestKind::Single(request) = store.find_hovered_request() else { return Ok(None); }; + drop(store); self.request_form = FormVariant::Edit(RequestForm::::new( self.colors, self.collection_store.clone(), @@ -313,6 +357,11 @@ impl<'a> Eventful for Sidebar<'a> { )); return Ok(Some(SidebarEvent::EditRequest)); } + KeyCode::Char('D') => { + if let Some(item_id) = store.get_hovered_request() { + return Ok(Some(SidebarEvent::DeleteItem(item_id))); + } + } KeyCode::Char('d') => return Ok(Some(SidebarEvent::CreateDirectory)), KeyCode::Esc => return Ok(Some(SidebarEvent::RemoveSelection)), _ => {} diff --git a/hac-client/src/pages/collection_viewer/sidebar/create_directory_form.rs b/hac-client/src/pages/collection_viewer/sidebar/create_directory_form.rs index f510383..4d22236 100644 --- a/hac-client/src/pages/collection_viewer/sidebar/create_directory_form.rs +++ b/hac-client/src/pages/collection_viewer/sidebar/create_directory_form.rs @@ -82,7 +82,7 @@ impl Renderable for CreateDirectoryForm<'_> { let logo = logo .iter() - .map(|line| Line::from(line.to_string().fg(self.colors.normal.red))) + .map(|line| Line::from(line.to_string().fg(self.colors.normal.red)).centered()) .collect::>(); let mut input = Input::new(self.colors, "Name".into()); diff --git a/hac-client/src/pages/collection_viewer/sidebar/create_request_form.rs b/hac-client/src/pages/collection_viewer/sidebar/create_request_form.rs index 7bd5196..890022f 100644 --- a/hac-client/src/pages/collection_viewer/sidebar/create_request_form.rs +++ b/hac-client/src/pages/collection_viewer/sidebar/create_request_form.rs @@ -53,6 +53,7 @@ impl<'rf> RequestForm<'rf, RequestFormCreate> { marker: std::marker::PhantomData, request: None, no_available_parent_timer: None, + previous_parent: None, } } } @@ -62,6 +63,40 @@ impl Eventful for RequestForm<'_, RequestFormCreate> { #[tracing::instrument(skip_all, err)] fn handle_key_event(&mut self, key_event: KeyEvent) -> anyhow::Result> { + let overlay = self.collection_store.borrow_mut().peek_overlay(); + + if overlay.eq(&CollectionViewerOverlay::SelectParentDir) { + match self.parent_selector.handle_key_event(key_event)? { + Some(SelectRequestParentEvent::Confirm(dir_id)) => { + let mut store = self.collection_store.borrow_mut(); + let collection = store + .get_collection() + .expect("tried attach a parent to a request without having a collection"); + let collection = collection.borrow(); + let requests = collection + .requests + .as_ref() + .expect("tried to attach a parent to a request with empty collection"); + let dir_name = requests + .read() + .unwrap() + .iter() + .find(|req| req.get_id().eq(&dir_id)) + .as_ref() + // its safe to unwrap here as to have an id we for sure have the directory + .unwrap() + .get_name(); + self.parent_dir = Some((dir_id, dir_name)); + store.pop_overlay(); + } + Some(SelectRequestParentEvent::Cancel) => { + self.collection_store.borrow_mut().pop_overlay(); + } + None => {} + } + return Ok(None); + } + if let KeyCode::Tab = key_event.code { self.focused_field = self.focused_field.next(); return Ok(None); @@ -88,16 +123,28 @@ impl Eventful for RequestForm<'_, RequestFormCreate> { self.request_name = String::from("unnamed request"); } - requests.push(RequestKind::Single(Arc::new(RwLock::new(Request { + let request = RequestKind::Single(Arc::new(RwLock::new(Request { id: uuid::Uuid::new_v4().to_string(), body: None, body_type: None, - parent: None, + parent: self.parent_dir.as_ref().map(|(id, _)| id.clone()), headers: None, method: self.request_method.clone(), name: self.request_name.clone(), uri: String::default(), - })))); + }))); + + if let Some((dir_id, _)) = self.parent_dir.as_ref() { + if let RequestKind::Nested(dir) = requests + .iter_mut() + .find(|req| req.get_id().eq(dir_id)) + .unwrap() + { + dir.requests.write().unwrap().push(request); + } + } else { + requests.push(request); + } drop(store); self.reset(); diff --git a/hac-client/src/pages/collection_viewer/sidebar/delete_item_prompt.rs b/hac-client/src/pages/collection_viewer/sidebar/delete_item_prompt.rs new file mode 100644 index 0000000..04e2400 --- /dev/null +++ b/hac-client/src/pages/collection_viewer/sidebar/delete_item_prompt.rs @@ -0,0 +1,136 @@ +use crate::ascii::LOGO_ASCII; +use crate::pages::collection_viewer::collection_store::CollectionStore; +use crate::pages::overlay::make_overlay; +use crate::pages::{Eventful, Renderable}; + +use std::cell::RefCell; +use std::ops::{Add, Div, Mul, Sub}; +use std::rc::Rc; + +use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; +use rand::Rng; +use ratatui::layout::Rect; +use ratatui::style::Stylize; +use ratatui::text::Line; +use ratatui::widgets::Paragraph; +use ratatui::Frame; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum DeleteItemPromptEvent { + Confirm, + Cancel, +} + +#[derive(Debug)] +pub struct DeleteItemPrompt<'dip> { + colors: &'dip hac_colors::Colors, + collection_store: Rc>, + logo_idx: usize, +} + +impl<'dip> DeleteItemPrompt<'dip> { + pub fn new( + colors: &'dip hac_colors::Colors, + collection_store: Rc>, + ) -> Self { + let logo_idx = rand::thread_rng().gen_range(0..LOGO_ASCII.len()); + DeleteItemPrompt { + colors, + logo_idx, + collection_store, + } + } +} + +impl Renderable for DeleteItemPrompt<'_> { + fn draw(&mut self, frame: &mut Frame, _: Rect) -> anyhow::Result<()> { + make_overlay(self.colors, self.colors.normal.black, 0.1, frame); + + let store = self.collection_store.borrow(); + let Some(hovered_id) = store.get_hovered_request().as_ref().cloned() else { + unreachable!(); + }; + let Some(collection) = store.get_collection() else { + unreachable!(); + }; + let collection = collection.borrow(); + let Some(ref requests) = collection.requests else { + unreachable!(); + }; + let is_dir = requests + .read() + .unwrap() + .iter() + .find(|req| req.get_id().eq(&hovered_id)) + .is_some_and(|req| req.is_dir()); + + let mut lines = if is_dir { + vec![ + Line::from( + "Are you sure you want to delete the directory?".fg(self.colors.normal.red), + ) + .centered(), + Line::from("This will delete all the requests inside".fg(self.colors.normal.red)) + .centered(), + Line::from(""), + ] + } else { + vec![ + Line::from( + "Are you sure you want to delete the request?".fg(self.colors.normal.red), + ) + .centered(), + Line::from(""), + ] + }; + + lines.push( + Line::from("[Confirm: Enter] [Cancel: Esc]".fg(self.colors.bright.black)).centered(), + ); + + let logo = LOGO_ASCII[self.logo_idx]; + let logo_size = logo.len() as u16; + let size = frame.size(); + + let size = Rect::new( + size.width.div(2).sub(25), + size.height + .div(2) + .saturating_sub(logo_size.div(2)) + .saturating_sub(3), + 50, + logo_size.add(7), + ); + + let logo_size = Rect::new(size.x, size.y, size.width, logo_size); + let logo = logo + .iter() + .map(|line| Line::from(line.fg(self.colors.normal.red)).centered()) + .collect::>(); + frame.render_widget(Paragraph::new(logo), logo_size); + + for (idx, line) in lines.into_iter().enumerate() { + let y_offset = logo_size.height.add(2).add(idx.mul(1) as u16); + let line_size = Rect::new(size.x, size.y.add(y_offset), size.width, 1); + frame.render_widget(Paragraph::new(line), line_size); + } + + Ok(()) + } +} + +impl Eventful for DeleteItemPrompt<'_> { + type Result = DeleteItemPromptEvent; + + fn handle_key_event(&mut self, key_event: KeyEvent) -> anyhow::Result> { + if let (KeyCode::Char('c'), KeyModifiers::CONTROL) = (key_event.code, key_event.modifiers) { + return Ok(Some(DeleteItemPromptEvent::Cancel)); + } + + match key_event.code { + KeyCode::Enter => Ok(Some(DeleteItemPromptEvent::Confirm)), + KeyCode::Esc => Ok(Some(DeleteItemPromptEvent::Cancel)), + _ => Ok(None), + } + } +} diff --git a/hac-client/src/pages/collection_viewer/sidebar/edit_request_form.rs b/hac-client/src/pages/collection_viewer/sidebar/edit_request_form.rs index 71a2816..da6ccb8 100644 --- a/hac-client/src/pages/collection_viewer/sidebar/edit_request_form.rs +++ b/hac-client/src/pages/collection_viewer/sidebar/edit_request_form.rs @@ -44,6 +44,30 @@ impl<'rf> RequestForm<'rf, RequestFormEdit> { let request_method = request.read().unwrap().method.clone(); let request_name = request.read().unwrap().name.clone(); + let parent_dir = if request.read().unwrap().parent.is_some() { + let store = collection_store.borrow(); + let Some(collection) = store.get_collection() else { + unreachable!(); + }; + let collection = collection.borrow(); + let Some(requests) = collection.requests.as_ref() else { + unreachable!(); + }; + + let dir_id = request.read().unwrap().parent.clone().unwrap(); + let dir_name = requests + .read() + .unwrap() + .iter() + .find(|req| req.get_id().eq(&dir_id)) + .unwrap() + .get_name(); + + Some((dir_id, dir_name)) + } else { + None + }; + RequestForm { colors, parent_selector: SelectRequestParent::new(colors, collection_store.clone()), @@ -51,11 +75,12 @@ impl<'rf> RequestForm<'rf, RequestFormEdit> { logo_idx, request_name, request_method, - parent_dir: None, + parent_dir, focused_field: FormField::Name, marker: std::marker::PhantomData, request: Some(request), no_available_parent_timer: None, + previous_parent: None, } } } @@ -68,7 +93,29 @@ impl Eventful for RequestForm<'_, RequestFormEdit> { let overlay = self.collection_store.borrow().peek_overlay(); if let CollectionViewerOverlay::SelectParentDir = overlay { match self.parent_selector.handle_key_event(key_event)? { - Some(SelectRequestParentEvent::Confirm(dir)) => {} + Some(SelectRequestParentEvent::Confirm(dir_id)) => { + let mut store = self.collection_store.borrow_mut(); + let collection = store + .get_collection() + .expect("tried attach a parent to a request without having a collection"); + let collection = collection.borrow(); + let requests = collection + .requests + .as_ref() + .expect("tried to attach a parent to a request with empty collection"); + let dir_name = requests + .read() + .unwrap() + .iter() + .find(|req| req.get_id().eq(&dir_id)) + .as_ref() + // its safe to unwrap here as to have an id we for sure have the directory + .unwrap() + .get_name(); + tracing::debug!("before: {:?}", self.parent_dir); + self.parent_dir = Some((dir_id, dir_name)); + store.pop_overlay(); + } Some(SelectRequestParentEvent::Cancel) => { self.collection_store.borrow_mut().pop_overlay(); } @@ -94,8 +141,45 @@ impl Eventful for RequestForm<'_, RequestFormEdit> { request.name.clone_from(&self.request_name); request.method.clone_from(&self.request_method); + request.parent = self.parent_dir.as_ref().map(|(id, _)| id.clone()); + let request_id = request.id.clone(); drop(request); + + let store = self.collection_store.borrow_mut(); + let collection = store + .get_collection() + .expect("tried to create a request without a collection"); + let mut collection = collection.borrow_mut(); + let requests = collection + .requests + .get_or_insert(Arc::new(RwLock::new(vec![]))); + let mut requests = requests.write().unwrap(); + + requests.iter_mut().for_each(|req| { + if let RequestKind::Nested(dir) = req { + dir.requests + .write() + .unwrap() + .retain(|req| req.get_id().ne(&request_id)); + } + }); + + if let Some((dir_id, _)) = self.parent_dir.as_ref() { + if let RequestKind::Nested(dir) = requests + .iter_mut() + .find(|req| req.get_id().eq(dir_id)) + .unwrap() + { + let request = self.request.as_ref().unwrap().clone(); + dir.requests + .write() + .unwrap() + .push(RequestKind::Single(request)); + } + } + + drop(store); self.reset(); return Ok(Some(RequestFormEvent::Confirm)); } diff --git a/hac-client/src/pages/collection_viewer/sidebar/request_form.rs b/hac-client/src/pages/collection_viewer/sidebar/request_form.rs index d67f2a4..cad1b5b 100644 --- a/hac-client/src/pages/collection_viewer/sidebar/request_form.rs +++ b/hac-client/src/pages/collection_viewer/sidebar/request_form.rs @@ -59,21 +59,35 @@ pub struct RequestFormEdit; pub struct RequestForm<'rf, State = RequestFormCreate> { pub colors: &'rf hac_colors::Colors, pub collection_store: Rc>, + + /// when we construct this component, we randomly chose one of our available + /// logo arts to display, for a little fun touch pub logo_idx: usize, + /// the name of the current request being edited or created pub request_name: String, + /// which method the request should have when finishing edition or creation pub request_method: RequestMethod, /// we store the parent dir uuid so its easier to find it and we dont need - /// lifetimes or to Rc our way to hell - pub parent_dir: Option, + /// lifetimes or to Rc our way to hell, along with it we also store the name + /// for displaying purposes + pub parent_dir: Option<(String, String)>, + /// the previous parent of the request, used when editing to remove the request + /// from the previous directory when changing parents + pub previous_parent: Option, + /// which form field is currently focused, so we can direct interactions + /// accordingly pub focused_field: FormField, - pub marker: std::marker::PhantomData, /// `request` is only used when editing a request so we can update it directly pub request: Option>>, + /// parent selector is the form used to select a parent for the current edited + /// or created request pub parent_selector: SelectRequestParent<'rf>, /// when the user tries to select a parent for a given request but there are /// no directories on the collection, we use this timer to show a message for /// a short duration, alerting the user pub no_available_parent_timer: Option, + + pub marker: std::marker::PhantomData, } impl<'rf, State> RequestForm<'rf, State> { @@ -149,24 +163,32 @@ impl<'rf, State> Renderable for RequestForm<'rf, State> { .constraints((0..5).map(|_| Constraint::Length(13))) .split(methods_size); - let parent_name = format!( - "{}None{}", - " ".repeat(parent_size.width.div(2).sub(2).into()), - " ".repeat(parent_size.width.div(2).sub(2).into()) - ); - let parent = Paragraph::new(parent_name) - .centered() - .block( - Block::default() - .title("Parent".fg(self.colors.normal.white)) - .borders(Borders::ALL) - .fg(if self.focused_field.eq(&FormField::Parent) { - self.colors.normal.red - } else { - self.colors.bright.black - }), + let parent_name = if self.parent_dir.is_none() { + format!( + "{}None{}", + " ".repeat(parent_size.width.div(2).sub(2).into()), + " ".repeat(parent_size.width.div(2).sub(2).into()) + ) + .fg(self.colors.bright.black) + } else { + format!( + "{}{}{}", + " ".repeat(parent_size.width.div(2).sub(2).into()), + self.parent_dir.as_ref().unwrap().1.clone(), + " ".repeat(parent_size.width.div(2).sub(2).into()) ) - .fg(self.colors.bright.black); + .fg(self.colors.normal.white) + }; + let parent = Paragraph::new(parent_name).centered().block( + Block::default() + .title("Parent".fg(self.colors.normal.white)) + .borders(Borders::ALL) + .fg(if self.focused_field.eq(&FormField::Parent) { + self.colors.normal.red + } else { + self.colors.bright.black + }), + ); for (idx, method) in RequestMethod::iter().enumerate() { let border_color = match (&self.request_method, &self.focused_field) { diff --git a/hac-client/src/pages/collection_viewer/sidebar/select_request_parent.rs b/hac-client/src/pages/collection_viewer/sidebar/select_request_parent.rs index f837e8d..3749a4a 100644 --- a/hac-client/src/pages/collection_viewer/sidebar/select_request_parent.rs +++ b/hac-client/src/pages/collection_viewer/sidebar/select_request_parent.rs @@ -4,7 +4,7 @@ use crate::pages::overlay::make_overlay; use crate::pages::{Eventful, Renderable}; use std::cell::RefCell; -use std::ops::{Add, Div, Sub}; +use std::ops::{Add, Div, Mul, Sub}; use std::rc::Rc; use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; @@ -28,10 +28,10 @@ pub enum SelectRequestParentEvent { #[derive(Debug)] pub struct SelectRequestParent<'srp> { colors: &'srp hac_colors::Colors, - dir_id: String, collection_store: Rc>, selected_dir: usize, logo_idx: usize, + scroll: usize, } impl<'srp> SelectRequestParent<'srp> { @@ -41,16 +41,12 @@ impl<'srp> SelectRequestParent<'srp> { ) -> Self { SelectRequestParent { colors, - dir_id: String::default(), collection_store, selected_dir: 0, logo_idx: rand::thread_rng().gen_range(0..LOGO_ASCII.len()), + scroll: 0, } } - - fn reset(&mut self) { - self.dir_id.clear(); - } } impl Renderable for SelectRequestParent<'_> { @@ -64,44 +60,71 @@ impl Renderable for SelectRequestParent<'_> { let mut directories = vec![]; if let Some(ref requests) = collection.borrow().requests { - directories.push( - requests - .read() - .unwrap() - .iter() - .filter(|req| req.is_dir()) - .map(|dir| dir.get_name()) - .collect::(), - ); + requests + .read() + .unwrap() + .iter() + .filter(|req| req.is_dir()) + .for_each(|dir| directories.push(dir.get_name())); }; let mut logo = LOGO_ASCII[self.logo_idx]; let size = frame.size(); - let logo_size = logo.len() as u16; - let mut total_size = logo_size.add(12); + let mut logo_size = logo.len() as u16; - if total_size.ge(&size.height) { + // if the logo makes the screen have 10 or less height, we hide it + if size.height.sub(logo_size).le(&10) { logo = &[]; - total_size = 12; + logo_size = 0; } let size = Rect::new( size.width.div(2).saturating_sub(25), - size.height.div(2).saturating_sub(total_size.div(2)), + size.y.add(4), 50, - total_size, + size.height, ); if !logo.is_empty() { + let logo_size = Rect::new(size.x, size.y, size.width, logo_size); let logo = logo .iter() .map(|line| Line::from(line.fg(self.colors.normal.red)).centered()) .collect::>(); - let logo_size = Rect::new(size.x, size.y, size.width, logo_size); frame.render_widget(Paragraph::new(logo), logo_size); } + let item_height = 2; + let remaining_space = size.height.sub(logo_size).sub(1); + let amount_on_view = remaining_space.div(item_height); + let dirs_start_y = logo_size.add(3); + + let header = Paragraph::new("Available directories".fg(self.colors.normal.yellow).bold()); + let header_size = Rect::new(size.x, size.y.add(logo_size).add(1), size.width, 2); + frame.render_widget(header, header_size); + + for (idx, dir) in directories + .into_iter() + .enumerate() + .skip(self.scroll) + .take(amount_on_view.into()) + { + let foreground = if self.selected_dir.eq(&idx) { + self.colors.normal.red + } else { + self.colors.normal.white + }; + let dir_size = Rect::new( + size.x, + size.y.add(dirs_start_y).add(idx.mul(2) as u16), + size.width, + 2, + ); + let dir = Paragraph::new(dir.fg(foreground)); + frame.render_widget(dir, dir_size); + } + Ok(()) } } @@ -111,7 +134,6 @@ impl Eventful for SelectRequestParent<'_> { fn handle_key_event(&mut self, key_event: KeyEvent) -> anyhow::Result> { if let (KeyCode::Char('c'), KeyModifiers::CONTROL) = (key_event.code, key_event.modifiers) { - self.reset(); return Ok(Some(SelectRequestParentEvent::Cancel)); } @@ -122,27 +144,25 @@ impl Eventful for SelectRequestParent<'_> { let mut directories = vec![]; if let Some(ref requests) = collection.borrow().requests { - directories.push( - requests - .read() - .unwrap() - .iter() - .filter(|req| req.is_dir()) - .map(|dir| dir.get_id()) - .collect::(), - ); + requests + .read() + .unwrap() + .iter() + .filter(|req| req.is_dir()) + .for_each(|dir| directories.push(dir.get_id())) }; let total_dirs = directories.len(); - drop(store); match key_event.code { KeyCode::Enter => { - let dir_id = self.dir_id.clone(); - self.reset(); + let (_, dir_id) = directories + .into_iter() + .enumerate() + .find(|(idx, _)| idx.eq(&self.selected_dir)) + .unwrap(); return Ok(Some(SelectRequestParentEvent::Confirm(dir_id))); } KeyCode::Esc => { - self.reset(); return Ok(Some(SelectRequestParentEvent::Cancel)); } KeyCode::Down | KeyCode::Tab | KeyCode::Char('j') => {