diff --git a/hac-client/benches/collection_viewer_bench.rs b/hac-client/benches/collection_viewer_bench.rs index 6c427fc..8c40cb5 100755 --- a/hac-client/benches/collection_viewer_bench.rs +++ b/hac-client/benches/collection_viewer_bench.rs @@ -31,6 +31,7 @@ fn create_sample_collection() -> Collection { id: "any id".to_string(), headers: None, name: "testing".to_string(), + parent: None, uri: "https://jsonplaceholder.typicode.com/users".to_string(), method: RequestMethod::Get, body: Some("[\r\n {\r\n \"id\": 1,\r\n \"name\": \"Leanne Graham\",\r\n \"username\": \"Bret\",\r\n \"email\": \"Sincere@april.biz\",\r\n \"address\": {\r\n \"street\": \"Kulas Light\",\r\n \"suite\": \"Apt. 556\",\r\n \"city\": \"Gwenborough\",\r\n \"zipcode\": \"92998-3874\",\r\n \"geo\": {\r\n \"lat\": \"-37.3159\",\r\n \"lng\": \"81.1496\"\r\n }\r\n },\r\n \"phone\": \"1-770-736-8031 x56442\",\r\n \"website\": \"hildegard.org\",\r\n \"company\": {\r\n \"name\": \"Romaguera-Crona\",\r\n \"catchPhrase\": \"Multi-layered client-server neural-net\",\r\n \"bs\": \"harness real-time e-markets\"\r\n }\r\n },\r\n {\r\n \"id\": 2,\r\n \"name\": \"Ervin Howell\",\r\n \"username\": \"Antonette\",\r\n \"email\": \"Shanna@melissa.tv\",\r\n \"address\": {\r\n \"street\": \"Victor Plains\",\r\n \"suite\": \"Suite 879\",\r\n \"city\": \"Wisokyburgh\",\r\n \"zipcode\": \"90566-7771\",\r\n \"geo\": {\r\n \"lat\": \"-43.9509\",\r\n \"lng\": \"-34.4618\"\r\n }\r\n },\r\n \"phone\": \"010-692-6593 x09125\",\r\n \"website\": \"anastasia.net\",\r\n \"company\": {\r\n \"name\": \"Deckow-Crist\",\r\n \"catchPhrase\": \"Proactive didactic contingency\",\r\n \"bs\": \"synergize scalable supply-chains\"\r\n }\r\n },\r\n {\r\n \"id\": 3,\r\n \"name\": \"Clementine Bauch\",\r\n \"username\": \"Samantha\",\r\n \"email\": \"Nathan@yesenia.net\",\r\n \"address\": {\r\n \"street\": \"Douglas Extension\",\r\n \"suite\": \"Suite 847\",\r\n \"city\": \"McKenziehaven\",\r\n \"zipcode\": \"59590-4157\",\r\n \"geo\": {\r\n \"lat\": \"-68.6102\",\r\n \"lng\": \"-47.0653\"\r\n }\r\n },\r\n \"phone\": \"1-463-123-4447\",\r\n \"website\": \"ramiro.info\",\r\n \"company\": {\r\n \"name\": \"Romaguera-Jacobson\",\r\n \"catchPhrase\": \"Face to face bifurcated interface\",\r\n \"bs\": \"e-enable strategic applications\"\r\n }\r\n },\r\n {\r\n \"id\": 4,\r\n \"name\": \"Patricia Lebsack\",\r\n \"username\": \"Karianne\",\r\n \"email\": \"Julianne.OConner@kory.org\",\r\n \"address\": {\r\n \"street\": \"Hoeger Mall\",\r\n \"suite\": \"Apt. 692\",\r\n \"city\": \"South Elvis\",\r\n \"zipcode\": \"53919-4257\",\r\n \"geo\": {\r\n \"lat\": \"29.4572\",\r\n \"lng\": \"-164.2990\"\r\n }\r\n },\r\n \"phone\": \"493-170-9623 x156\",\r\n \"website\": \"kale.biz\",\r\n \"company\": {\r\n \"name\": \"Robel-Corkery\",\r\n \"catchPhrase\": \"Multi-tiered zero tolerance productivity\",\r\n \"bs\": \"transition cutting-edge web services\"\r\n }\r\n },\r\n {\r\n \"id\": 5,\r\n \"name\": \"Chelsey Dietrich\",\r\n \"username\": \"Kamren\",\r\n \"email\": \"Lucio_Hettinger@annie.ca\",\r\n \"address\": {\r\n \"street\": \"Skiles Walks\",\r\n \"suite\": \"Suite 351\",\r\n \"city\": \"Roscoeview\",\r\n \"zipcode\": \"33263\",\r\n \"geo\": {\r\n \"lat\": \"-31.8129\",\r\n \"lng\": \"62.5342\"\r\n }\r\n },\r\n \"phone\": \"(254)954-1289\",\r\n \"website\": \"demarco.info\",\r\n \"company\": {\r\n \"name\": \"Keebler LLC\",\r\n \"catchPhrase\": \"User-centric fault-tolerant solution\",\r\n \"bs\": \"revolutionize end-to-end systems\"\r\n }\r\n },\r\n {\r\n \"id\": 6,\r\n \"name\": \"Mrs. Dennis Schulist\",\r\n \"username\": \"Leopoldo_Corkery\",\r\n \"email\": \"Karley_Dach@jasper.info\",\r\n \"address\": {\r\n \"street\": \"Norberto Crossing\",\r\n \"suite\": \"Apt. 950\",\r\n \"city\": \"South Christy\",\r\n \"zipcode\": \"23505-1337\",\r\n \"geo\": {\r\n \"lat\": \"-71.4197\",\r\n \"lng\": \"71.7478\"\r\n }\r\n },\r\n \"phone\": \"1-477-935-8478 x6430\",\r\n \"website\": \"ola.org\",\r\n \"company\": {\r\n \"name\": \"Considine-Lockman\",\r\n \"catchPhrase\": \"Synchronised bottom-line interface\",\r\n \"bs\": \"e-enable innovative applications\"\r\n }\r\n },\r\n {\r\n \"id\": 7,\r\n \"name\": \"Kurtis Weissnat\",\r\n \"username\": \"Elwyn.Skiles\",\r\n \"email\": \"Telly.Hoeger@billy.biz\",\r\n \"address\": {\r\n \"street\": \"Rex Trail\",\r\n \"suite\": \"Suite 280\",\r\n \"city\": \"Howemouth\",\r\n \"zipcode\": \"58804-1099\",\r\n \"geo\": {\r\n \"lat\": \"24.8918\",\r\n \"lng\": \"21.8984\"\r\n }\r\n },\r\n \"phone\": \"210.067.6132\",\r\n \"website\": \"elvis.io\",\r\n \"company\": {\r\n \"name\": \"Johns Group\",\r\n \"catchPhrase\": \"Configurable multimedia task-force\",\r\n \"bs\": \"generate enterprise e-tailers\"\r\n }\r\n },\r\n {\r\n \"id\": 8,\r\n \"name\": \"Nicholas Runolfsdottir V\",\r\n \"username\": \"Maxime_Nienow\",\r\n \"email\": \"Sherwood@rosamond.me\",\r\n \"address\": {\r\n \"street\": \"Ellsworth Summit\",\r\n \"suite\": \"Suite 729\",\r\n \"city\": \"Aliyaview\",\r\n \"zipcode\": \"45169\",\r\n \"geo\": {\r\n \"lat\": \"-14.3990\",\r\n \"lng\": \"-120.7677\"\r\n }\r\n },\r\n \"phone\": \"586.493.6943 x140\",\r\n \"website\": \"jacynthe.com\",\r\n \"company\": {\r\n \"name\": \"Abernathy Group\",\r\n \"catchPhrase\": \"Implemented secondary concept\",\r\n \"bs\": \"e-enable extensible e-tailers\"\r\n }\r\n },\r\n {\r\n \"id\": 9,\r\n \"name\": \"Glenna Reichert\",\r\n \"username\": \"Delphine\",\r\n \"email\": \"Chaim_McDermott@dana.io\",\r\n \"address\": {\r\n \"street\": \"Dayna Park\",\r\n \"suite\": \"Suite 449\",\r\n \"city\": \"Bartholomebury\",\r\n \"zipcode\": \"76495-3109\",\r\n \"geo\": {\r\n \"lat\": \"24.6463\",\r\n \"lng\": \"-168.8889\"\r\n }\r\n },\r\n \"phone\": \"(775)976-6794 x41206\",\r\n \"website\": \"conrad.com\",\r\n \"company\": {\r\n \"name\": \"Yost and Sons\",\r\n \"catchPhrase\": \"Switchable contextually-based project\",\r\n \"bs\": \"aggregate real-time technologies\"\r\n }\r\n },\r\n {\r\n \"id\": 10,\r\n \"name\": \"Clementina DuBuque\",\r\n \"username\": \"Moriah.Stanton\",\r\n \"email\": \"Rey.Padberg@karina.biz\",\r\n \"address\": {\r\n \"street\": \"Kattie Turnpike\",\r\n \"suite\": \"Suite 198\",\r\n \"city\": \"Lebsackbury\",\r\n \"zipcode\": \"31428-2261\",\r\n \"geo\": {\r\n \"lat\": \"-38.2386\",\r\n \"lng\": \"57.2232\"\r\n }\r\n },\r\n \"phone\": \"024-648-3804\",\r\n \"website\": \"ambrose.net\",\r\n \"company\": {\r\n \"name\": \"Hoeger LLC\",\r\n \"catchPhrase\": \"Centralized empowering task-force\",\r\n \"bs\": \"target end-to-end models\"\r\n }\r\n }\r\n]".to_string()), @@ -41,6 +42,7 @@ fn create_sample_collection() -> Collection { name: "testing".to_string(), uri: "https://jsonplaceholder.typicode.com/users".to_string(), method: RequestMethod::Get, + parent: None, headers: None, body: Some("[\r\n {\r\n \"id\": 1,\r\n \"name\": \"Leanne Graham\",\r\n \"username\": \"Bret\",\r\n \"email\": \"Sincere@april.biz\",\r\n \"address\": {\r\n \"street\": \"Kulas Light\",\r\n \"suite\": \"Apt. 556\",\r\n \"city\": \"Gwenborough\",\r\n \"zipcode\": \"92998-3874\",\r\n \"geo\": {\r\n \"lat\": \"-37.3159\",\r\n \"lng\": \"81.1496\"\r\n }\r\n },\r\n \"phone\": \"1-770-736-8031 x56442\",\r\n \"website\": \"hildegard.org\",\r\n \"company\": {\r\n \"name\": \"Romaguera-Crona\",\r\n \"catchPhrase\": \"Multi-layered client-server neural-net\",\r\n \"bs\": \"harness real-time e-markets\"\r\n }\r\n },\r\n {\r\n \"id\": 2,\r\n \"name\": \"Ervin Howell\",\r\n \"username\": \"Antonette\",\r\n \"email\": \"Shanna@melissa.tv\",\r\n \"address\": {\r\n \"street\": \"Victor Plains\",\r\n \"suite\": \"Suite 879\",\r\n \"city\": \"Wisokyburgh\",\r\n \"zipcode\": \"90566-7771\",\r\n \"geo\": {\r\n \"lat\": \"-43.9509\",\r\n \"lng\": \"-34.4618\"\r\n }\r\n },\r\n \"phone\": \"010-692-6593 x09125\",\r\n \"website\": \"anastasia.net\",\r\n \"company\": {\r\n \"name\": \"Deckow-Crist\",\r\n \"catchPhrase\": \"Proactive didactic contingency\",\r\n \"bs\": \"synergize scalable supply-chains\"\r\n }\r\n },\r\n {\r\n \"id\": 3,\r\n \"name\": \"Clementine Bauch\",\r\n \"username\": \"Samantha\",\r\n \"email\": \"Nathan@yesenia.net\",\r\n \"address\": {\r\n \"street\": \"Douglas Extension\",\r\n \"suite\": \"Suite 847\",\r\n \"city\": \"McKenziehaven\",\r\n \"zipcode\": \"59590-4157\",\r\n \"geo\": {\r\n \"lat\": \"-68.6102\",\r\n \"lng\": \"-47.0653\"\r\n }\r\n },\r\n \"phone\": \"1-463-123-4447\",\r\n \"website\": \"ramiro.info\",\r\n \"company\": {\r\n \"name\": \"Romaguera-Jacobson\",\r\n \"catchPhrase\": \"Face to face bifurcated interface\",\r\n \"bs\": \"e-enable strategic applications\"\r\n }\r\n },\r\n {\r\n \"id\": 4,\r\n \"name\": \"Patricia Lebsack\",\r\n \"username\": \"Karianne\",\r\n \"email\": \"Julianne.OConner@kory.org\",\r\n \"address\": {\r\n \"street\": \"Hoeger Mall\",\r\n \"suite\": \"Apt. 692\",\r\n \"city\": \"South Elvis\",\r\n \"zipcode\": \"53919-4257\",\r\n \"geo\": {\r\n \"lat\": \"29.4572\",\r\n \"lng\": \"-164.2990\"\r\n }\r\n },\r\n \"phone\": \"493-170-9623 x156\",\r\n \"website\": \"kale.biz\",\r\n \"company\": {\r\n \"name\": \"Robel-Corkery\",\r\n \"catchPhrase\": \"Multi-tiered zero tolerance productivity\",\r\n \"bs\": \"transition cutting-edge web services\"\r\n }\r\n },\r\n {\r\n \"id\": 5,\r\n \"name\": \"Chelsey Dietrich\",\r\n \"username\": \"Kamren\",\r\n \"email\": \"Lucio_Hettinger@annie.ca\",\r\n \"address\": {\r\n \"street\": \"Skiles Walks\",\r\n \"suite\": \"Suite 351\",\r\n \"city\": \"Roscoeview\",\r\n \"zipcode\": \"33263\",\r\n \"geo\": {\r\n \"lat\": \"-31.8129\",\r\n \"lng\": \"62.5342\"\r\n }\r\n },\r\n \"phone\": \"(254)954-1289\",\r\n \"website\": \"demarco.info\",\r\n \"company\": {\r\n \"name\": \"Keebler LLC\",\r\n \"catchPhrase\": \"User-centric fault-tolerant solution\",\r\n \"bs\": \"revolutionize end-to-end systems\"\r\n }\r\n },\r\n {\r\n \"id\": 6,\r\n \"name\": \"Mrs. Dennis Schulist\",\r\n \"username\": \"Leopoldo_Corkery\",\r\n \"email\": \"Karley_Dach@jasper.info\",\r\n \"address\": {\r\n \"street\": \"Norberto Crossing\",\r\n \"suite\": \"Apt. 950\",\r\n \"city\": \"South Christy\",\r\n \"zipcode\": \"23505-1337\",\r\n \"geo\": {\r\n \"lat\": \"-71.4197\",\r\n \"lng\": \"71.7478\"\r\n }\r\n },\r\n \"phone\": \"1-477-935-8478 x6430\",\r\n \"website\": \"ola.org\",\r\n \"company\": {\r\n \"name\": \"Considine-Lockman\",\r\n \"catchPhrase\": \"Synchronised bottom-line interface\",\r\n \"bs\": \"e-enable innovative applications\"\r\n }\r\n },\r\n {\r\n \"id\": 7,\r\n \"name\": \"Kurtis Weissnat\",\r\n \"username\": \"Elwyn.Skiles\",\r\n \"email\": \"Telly.Hoeger@billy.biz\",\r\n \"address\": {\r\n \"street\": \"Rex Trail\",\r\n \"suite\": \"Suite 280\",\r\n \"city\": \"Howemouth\",\r\n \"zipcode\": \"58804-1099\",\r\n \"geo\": {\r\n \"lat\": \"24.8918\",\r\n \"lng\": \"21.8984\"\r\n }\r\n },\r\n \"phone\": \"210.067.6132\",\r\n \"website\": \"elvis.io\",\r\n \"company\": {\r\n \"name\": \"Johns Group\",\r\n \"catchPhrase\": \"Configurable multimedia task-force\",\r\n \"bs\": \"generate enterprise e-tailers\"\r\n }\r\n },\r\n {\r\n \"id\": 8,\r\n \"name\": \"Nicholas Runolfsdottir V\",\r\n \"username\": \"Maxime_Nienow\",\r\n \"email\": \"Sherwood@rosamond.me\",\r\n \"address\": {\r\n \"street\": \"Ellsworth Summit\",\r\n \"suite\": \"Suite 729\",\r\n \"city\": \"Aliyaview\",\r\n \"zipcode\": \"45169\",\r\n \"geo\": {\r\n \"lat\": \"-14.3990\",\r\n \"lng\": \"-120.7677\"\r\n }\r\n },\r\n \"phone\": \"586.493.6943 x140\",\r\n \"website\": \"jacynthe.com\",\r\n \"company\": {\r\n \"name\": \"Abernathy Group\",\r\n \"catchPhrase\": \"Implemented secondary concept\",\r\n \"bs\": \"e-enable extensible e-tailers\"\r\n }\r\n },\r\n {\r\n \"id\": 9,\r\n \"name\": \"Glenna Reichert\",\r\n \"username\": \"Delphine\",\r\n \"email\": \"Chaim_McDermott@dana.io\",\r\n \"address\": {\r\n \"street\": \"Dayna Park\",\r\n \"suite\": \"Suite 449\",\r\n \"city\": \"Bartholomebury\",\r\n \"zipcode\": \"76495-3109\",\r\n \"geo\": {\r\n \"lat\": \"24.6463\",\r\n \"lng\": \"-168.8889\"\r\n }\r\n },\r\n \"phone\": \"(775)976-6794 x41206\",\r\n \"website\": \"conrad.com\",\r\n \"company\": {\r\n \"name\": \"Yost and Sons\",\r\n \"catchPhrase\": \"Switchable contextually-based project\",\r\n \"bs\": \"aggregate real-time technologies\"\r\n }\r\n },\r\n {\r\n \"id\": 10,\r\n \"name\": \"Clementina DuBuque\",\r\n \"username\": \"Moriah.Stanton\",\r\n \"email\": \"Rey.Padberg@karina.biz\",\r\n \"address\": {\r\n \"street\": \"Kattie Turnpike\",\r\n \"suite\": \"Suite 198\",\r\n \"city\": \"Lebsackbury\",\r\n \"zipcode\": \"31428-2261\",\r\n \"geo\": {\r\n \"lat\": \"-38.2386\",\r\n \"lng\": \"57.2232\"\r\n }\r\n },\r\n \"phone\": \"024-648-3804\",\r\n \"website\": \"ambrose.net\",\r\n \"company\": {\r\n \"name\": \"Hoeger LLC\",\r\n \"catchPhrase\": \"Centralized empowering task-force\",\r\n \"bs\": \"target end-to-end models\"\r\n }\r\n }\r\n]".to_string()), body_type: Some(BodyType::Json), diff --git a/hac-client/src/pages/collection_viewer/collection_store.rs b/hac-client/src/pages/collection_viewer/collection_store.rs index c79299c..0156715 100755 --- a/hac-client/src/pages/collection_viewer/collection_store.rs +++ b/hac-client/src/pages/collection_viewer/collection_store.rs @@ -358,6 +358,7 @@ mod tests { id: "root".to_string(), method: RequestMethod::Get, name: "Root1".to_string(), + parent: None, headers: None, uri: "/root1".to_string(), body_type: None, @@ -368,6 +369,7 @@ mod tests { fn create_child_one() -> RequestKind { RequestKind::Single(Arc::new(RwLock::new(Request { id: "child_one".to_string(), + parent: Some(String::from("dir")), method: RequestMethod::Post, name: "Child1".to_string(), uri: "/nested1/child1".to_string(), @@ -383,6 +385,7 @@ mod tests { method: RequestMethod::Put, name: "Child2".to_string(), headers: None, + parent: Some(String::from("dir")), uri: "/nested1/child2".to_string(), body_type: None, body: None, @@ -394,6 +397,7 @@ mod tests { id: "not_used".to_string(), method: RequestMethod::Put, name: "NotUsed".to_string(), + parent: None, headers: None, uri: "/not/used".to_string(), body_type: None, @@ -418,6 +422,7 @@ mod tests { id: "root_two".to_string(), method: RequestMethod::Delete, headers: None, + parent: None, name: "Root2".to_string(), uri: "/root2".to_string(), body_type: None, diff --git a/hac-client/src/pages/collection_viewer/sidebar.rs b/hac-client/src/pages/collection_viewer/sidebar.rs index c413a6c..62a4a7a 100755 --- a/hac-client/src/pages/collection_viewer/sidebar.rs +++ b/hac-client/src/pages/collection_viewer/sidebar.rs @@ -1,15 +1,17 @@ +mod create_directory_form; mod create_request_form; mod edit_request_form; mod request_form; use hac_core::collection::types::{Request, RequestKind, RequestMethod}; +use super::sidebar::create_directory_form::{CreateDirectoryForm, CreateDirectoryFormEvent}; +use super::sidebar::request_form::RequestForm; +use super::sidebar::request_form::RequestFormCreate; +use super::sidebar::request_form::RequestFormEdit; +use super::sidebar::request_form::RequestFormEvent; use crate::pages::collection_viewer::collection_store::{CollectionStore, CollectionStoreAction}; use crate::pages::collection_viewer::collection_viewer::{CollectionViewerOverlay, PaneFocus}; -use crate::pages::collection_viewer::sidebar::request_form::RequestForm; -use crate::pages::collection_viewer::sidebar::request_form::RequestFormCreate; -use crate::pages::collection_viewer::sidebar::request_form::RequestFormEdit; -use crate::pages::collection_viewer::sidebar::request_form::RequestFormEvent; use crate::pages::{Eventful, Renderable}; use std::cell::RefCell; @@ -75,6 +77,7 @@ pub struct Sidebar<'sbar> { lines: Vec>, collection_store: Rc>, request_form: FormVariant<'sbar>, + directory_form: CreateDirectoryForm<'sbar>, } impl<'sbar> Sidebar<'sbar> { @@ -88,6 +91,7 @@ impl<'sbar> Sidebar<'sbar> { colors, collection_store.clone(), )), + directory_form: CreateDirectoryForm::new(colors, collection_store.clone()), lines: vec![], collection_store, }; @@ -121,7 +125,9 @@ impl<'sbar> Sidebar<'sbar> { CollectionViewerOverlay::EditRequest => { self.request_form.inner().draw(frame, frame.size())?; } - CollectionViewerOverlay::CreateDirectory => {} + CollectionViewerOverlay::CreateDirectory => { + self.directory_form.draw(frame, frame.size())?; + } _ => {} }; @@ -207,7 +213,25 @@ impl<'a> Eventful for Sidebar<'a> { None => return Ok(None), } } - CollectionViewerOverlay::CreateDirectory => todo!(), + CollectionViewerOverlay::CreateDirectory => { + match self.directory_form.handle_key_event(key_event)? { + Some(CreateDirectoryFormEvent::Confirm) => { + let mut store = self.collection_store.borrow_mut(); + store.pop_overlay(); + drop(store); + self.rebuild_tree_view(); + return Ok(Some(SidebarEvent::SyncCollection)); + } + Some(CreateDirectoryFormEvent::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), + } + } CollectionViewerOverlay::EditRequest => { // when editing, we setup the form to display the current header information. match self.request_form.inner().handle_key_event(key_event)? { 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 new file mode 100644 index 0000000..7945bc7 --- /dev/null +++ b/hac-client/src/pages/collection_viewer/sidebar/create_directory_form.rs @@ -0,0 +1,164 @@ +use hac_core::collection::types::*; + +use crate::ascii::LOGO_ASCII; +use crate::pages::collection_viewer::collection_store::CollectionStore; +use crate::pages::input::Input; +use crate::pages::overlay::make_overlay; +use crate::pages::{Eventful, Renderable}; + +use std::cell::RefCell; +use std::ops::{Add, Div, Sub}; +use std::rc::Rc; +use std::sync::{Arc, RwLock}; + +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; + +/// set of events `CreateDirectoryForm` can send the parent to +/// handle +#[derive(Debug, PartialEq, Eq, Clone)] +pub enum CreateDirectoryFormEvent { + /// when user confirms the directory creation we should notify + /// the parent to properly handle the event + Confirm, + /// when the user cancels the creation, we should also notify + /// the parent to properly clean up things + Cancel, +} + +#[derive(Debug)] +pub struct CreateDirectoryForm<'cdf> { + colors: &'cdf hac_colors::Colors, + dir_name: String, + collection_store: Rc>, + logo_idx: usize, +} + +impl<'cdf> CreateDirectoryForm<'cdf> { + pub fn new( + colors: &'cdf hac_colors::Colors, + collection_store: Rc>, + ) -> Self { + let logo_idx = rand::thread_rng().gen_range(0..LOGO_ASCII.len()); + + CreateDirectoryForm { + colors, + dir_name: String::default(), + collection_store, + logo_idx, + } + } + + fn reset(&mut self) { + self.dir_name.clear(); + } +} + +impl Renderable for CreateDirectoryForm<'_> { + fn draw( + &mut self, + frame: &mut ratatui::prelude::Frame, + _: ratatui::prelude::Rect, + ) -> anyhow::Result<()> { + make_overlay(self.colors, self.colors.normal.black, 0.1, frame); + + 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(2), + 50, + logo_size.add(4), + ); + + let logo = logo + .iter() + .map(|line| Line::from(line.to_string().fg(self.colors.normal.red))) + .collect::>(); + + let mut input = Input::new(self.colors, "Name".into()); + input.focus(); + + let hint = Line::from("[Confirm: Enter] [Cancel: Esc]") + .fg(self.colors.bright.black) + .centered(); + + let logo_size = Rect::new(size.x, size.y, size.width, logo_size); + let input_size = Rect::new( + size.x, + logo_size.y.add(logo_size.height).add(1), + size.width, + 3, + ); + let hint_size = Rect::new(size.x, input_size.y.add(4), size.width, 1); + + frame.render_widget(Paragraph::new(logo), logo_size); + frame.render_stateful_widget(input, input_size, &mut self.dir_name); + frame.render_widget(hint, hint_size); + + frame.set_cursor( + input_size.x.add(self.dir_name.len() as u16).add(1), + input_size.y.add(1), + ); + + Ok(()) + } +} + +impl Eventful for CreateDirectoryForm<'_> { + type Result = CreateDirectoryFormEvent; + + 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(CreateDirectoryFormEvent::Cancel)); + } + + match key_event.code { + KeyCode::Esc => { + self.reset(); + return Ok(Some(CreateDirectoryFormEvent::Cancel)); + } + KeyCode::Enter => { + 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(); + + if self.dir_name.is_empty() { + self.dir_name = "unnamed directory".into(); + } + + requests.push(RequestKind::Nested(Directory { + id: uuid::Uuid::new_v4().to_string(), + name: self.dir_name.clone(), + requests: Arc::new(RwLock::new(vec![])), + })); + + drop(store); + self.reset(); + return Ok(Some(CreateDirectoryFormEvent::Confirm)); + } + KeyCode::Char(c) => self.dir_name.push(c), + KeyCode::Backspace => _ = self.dir_name.pop(), + _ => {} + } + + Ok(None) + } +} 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 94971d9..eb2bcb8 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 @@ -71,6 +71,7 @@ impl Eventful for RequestForm<'_, RequestFormCreate> { id: uuid::Uuid::new_v4().to_string(), body: None, body_type: None, + parent: None, headers: None, method: self.request_method.clone(), name: self.request_name.clone(), diff --git a/hac-core/src/collection/types.rs b/hac-core/src/collection/types.rs index fd4abf5..4cc37ce 100755 --- a/hac-core/src/collection/types.rs +++ b/hac-core/src/collection/types.rs @@ -4,20 +4,27 @@ use std::sync::{Arc, RwLock}; use serde::{Deserialize, Serialize}; +/// a collection is represented as a file on the file system and holds every +/// request and metadata #[derive(Debug, Serialize, Deserialize, Clone)] pub struct Collection { + /// basic information about the collection such as name and description pub info: Info, + /// maybe a vector of `RequestKind` that are part of the collection pub requests: Option>>>, + /// path is a virtual field used only during runtime to know where to + /// sync the file, this will be the absolute path to the file on the + /// users computer #[serde(skip)] pub path: PathBuf, } -impl AsRef for Collection { - fn as_ref(&self) -> &Collection { - self - } -} - +/// we store requests on a collection and on directories as a enum that could +/// be either an request or a directory. This enables us to have nested +/// directories, although we don't support that now and might not ever support. +/// +/// Single means its a request +/// Nested means its a directory #[derive(Debug, Serialize, Deserialize, Clone)] #[serde(untagged)] pub enum RequestKind { @@ -26,6 +33,8 @@ pub enum RequestKind { } impl RequestKind { + /// helper method to get the name of either a request or a directory without + /// needing to narrow the type pub fn get_name(&self) -> String { match self { RequestKind::Single(req) => req.read().unwrap().name.to_string(), @@ -33,6 +42,8 @@ impl RequestKind { } } + /// helper method to get the id of either a request or a directory without + /// needing to narrow the type pub fn get_id(&self) -> String { match self { RequestKind::Single(req) => req.read().unwrap().id.to_string(), @@ -41,12 +52,17 @@ impl RequestKind { } } +/// we store headers as a simple struct which is composed by a pair which +/// represents name/value of a header, and wether it is enabled or not. +/// +/// disabled headers should not be sent on requests #[derive(Debug, Deserialize, Serialize, Clone)] pub struct HeaderMap { pub pair: (String, String), pub enabled: bool, } +/// set of methods we currently support on HTTP requests #[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Hash, Clone)] #[serde(rename_all = "UPPERCASE")] pub enum RequestMethod { @@ -72,6 +88,8 @@ impl TryFrom for RequestMethod { } } +/// this is a cyclic implementation of `next` and `prev` that are used +/// by the UI to cycle through elements, usually by using tab impl RequestMethod { pub fn next(&self) -> Self { match self { @@ -106,6 +124,11 @@ impl std::fmt::Display for RequestMethod { } } +// custom iterator implementation for RequestMethod to be able to map over +// its variants without writing a lot of boilerplate everytime +// +// NOTE: if this kind of behavior repeats a lot we might want to introduce +// a helper crate like strum impl RequestMethod { pub fn iter() -> std::slice::Iter<'static, RequestMethod> { [ @@ -119,39 +142,64 @@ impl RequestMethod { } } +/// This is how we store a request on the system, basically this stores all +/// needed information about a request to be able to perform any actions we +/// allow. #[derive(Debug, Serialize, Deserialize, Clone)] pub struct Request { + /// we store an uuid on each request to be able to easily identify them + /// as identifying by name is pub id: String, + /// each request has to have a method, this is the HTTP method that will + /// be used when realizing requests pub method: RequestMethod, + /// name of the request that will be displayed on the sidebar pub name: String, + /// uri that the request will be sent against pub uri: String, + /// all headers used on given request, sometimes, we may include additional + /// headers if required to make a request pub headers: Option>, + /// if this request lives as a children of a directory, the uuid of given + /// directory will be stored here, this is mainly used to know where to + /// insert or move the request + pub parent: Option, + /// body of the request, this will only be sent in methods that accept a + /// body, like POST or PUT, for example pub body: Option, #[serde(rename = "bodyType")] + /// the type of the body to be used, like `application/json` or any other + /// accepted body type pub body_type: Option, } +/// a collection of all available body types we support. #[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] pub enum BodyType { #[serde(rename = "json")] Json, } -impl Hash for Request { - fn hash(&self, state: &mut H) { - state.write(format!("{}{}{}", self.method, self.name, self.uri).as_bytes()); - } -} - +/// a directory can hold a vector of requests, which will be +/// displayed as a tree-like view in the sidebar #[derive(Debug, Default, Serialize, Deserialize, Clone)] pub struct Directory { + /// as it is the same with requests, directories have an id to make + /// easier to know relations without having to rely on references + /// and lifetimes pub id: String, + /// name of the directory that will be used in the display on the + /// sidebar pub name: String, + /// vector of requests that are children of this directory pub requests: Arc>>, } +/// basic information about a colleciton #[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] pub struct Info { + /// name of the collection that will be displayed onscreen pub name: String, + /// a optional description in case it is useful pub description: Option, }