feat: adds main loop
fix: some logic bugs
This commit is contained in:
parent
786ab80474
commit
6f8b865e33
@ -9,6 +9,7 @@ serde = {version = "1.0.204", features = ["derive"]}
|
|||||||
serde_json = "1.0.122"
|
serde_json = "1.0.122"
|
||||||
ellipse = "0.2.0"
|
ellipse = "0.2.0"
|
||||||
itertools = "0.13.0"
|
itertools = "0.13.0"
|
||||||
|
clearscreen = "3.0.0"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tempfile = "3.11.0"
|
tempfile = "3.11.0"
|
||||||
|
23
src/db.rs
23
src/db.rs
@ -1,4 +1,5 @@
|
|||||||
use std::fs;
|
use std::fs::{self, OpenOptions};
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
|
|
||||||
@ -9,10 +10,23 @@ pub struct JiraDatabase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl JiraDatabase {
|
impl JiraDatabase {
|
||||||
pub fn new(file_path: String) -> Self {
|
pub fn new(file_path: &str) -> Result<Self> {
|
||||||
Self {
|
let path = file_path.to_owned();
|
||||||
database: Box::new(JSONFileDatabase { file_path }),
|
let db = Self {
|
||||||
|
database: Box::new(JSONFileDatabase { file_path: path }),
|
||||||
|
};
|
||||||
|
|
||||||
|
if !Path::new(file_path).exists() {
|
||||||
|
match OpenOptions::new().create(true).write(true).open(file_path) {
|
||||||
|
Err(e) => return Err(anyhow!("failed to open/create database file: {e}")),
|
||||||
|
Ok(_) => {
|
||||||
|
db.database.write_db(&DBState::new())?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// .with_context(|| format!("failed to create epic"))?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Ok(db)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn read_db(&self) -> Result<DBState> {
|
pub fn read_db(&self) -> Result<DBState> {
|
||||||
@ -138,6 +152,7 @@ pub mod test_utils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl MockDB {
|
impl MockDB {
|
||||||
|
#[allow(dead_code)]
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
last_written_state: RefCell::new(DBState {
|
last_written_state: RefCell::new(DBState {
|
||||||
|
@ -5,5 +5,9 @@ pub fn get_user_input() -> String {
|
|||||||
|
|
||||||
io::stdin().read_line(&mut user_input).unwrap();
|
io::stdin().read_line(&mut user_input).unwrap();
|
||||||
|
|
||||||
user_input
|
user_input.trim().to_owned()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn wait_for_key_press() {
|
||||||
|
io::stdin().read_line(&mut String::new()).unwrap();
|
||||||
|
}
|
||||||
|
60
src/main.rs
60
src/main.rs
@ -1,9 +1,59 @@
|
|||||||
mod db;
|
use std::rc::Rc;
|
||||||
mod io_utils;
|
|
||||||
mod models;
|
mod models;
|
||||||
mod navigator;
|
|
||||||
|
mod db;
|
||||||
|
use anyhow::Result;
|
||||||
|
use db::*;
|
||||||
|
|
||||||
mod ui;
|
mod ui;
|
||||||
|
|
||||||
fn main() {
|
mod io_utils;
|
||||||
println!("Welcome scrumtask-cli!");
|
use io_utils::*;
|
||||||
|
|
||||||
|
mod navigator;
|
||||||
|
use navigator::*;
|
||||||
|
|
||||||
|
fn main() -> Result<()> {
|
||||||
|
// TODO: create database and navigator
|
||||||
|
let db = Rc::new(JiraDatabase::new("./db.json")?);
|
||||||
|
let mut nav = Navigator::new(db);
|
||||||
|
|
||||||
|
loop {
|
||||||
|
// clearscreen::clear().unwrap();
|
||||||
|
|
||||||
|
// 1. get current page from navigator. If there is no current page exit the loop.
|
||||||
|
let page = match nav.get_current_page() {
|
||||||
|
Some(p) => p,
|
||||||
|
None => {
|
||||||
|
break Ok(());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// 2. render page
|
||||||
|
if let Err(e) = page.draw_page() {
|
||||||
|
eprintln!("failed to render page: {e}");
|
||||||
|
wait_for_key_press();
|
||||||
|
break Err(e);
|
||||||
|
}
|
||||||
|
// 3. get user input
|
||||||
|
let input = io_utils::get_user_input();
|
||||||
|
// 4. pass input to page's input handler
|
||||||
|
let action = match page.handle_input(&input.trim()) {
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("failed to handle input '{input}': {e}");
|
||||||
|
wait_for_key_press();
|
||||||
|
break Err(e);
|
||||||
|
}
|
||||||
|
Ok(a) => a,
|
||||||
|
};
|
||||||
|
// 5. if the page's input handler returns an action let the navigator process the action
|
||||||
|
if let Some(a) = action {
|
||||||
|
let action = a.clone();
|
||||||
|
if let Err(e) = nav.handle_action(a) {
|
||||||
|
eprintln!("failed to handle action '{action:?}': {e}");
|
||||||
|
wait_for_key_press();
|
||||||
|
break Err(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::{collections::HashMap, fmt::Display};
|
use std::{collections::HashMap, fmt::Display};
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq)]
|
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||||
pub enum Action {
|
pub enum Action {
|
||||||
NavigateToEpicDetail { epic_id: u32 },
|
NavigateToEpicDetail { epic_id: u32 },
|
||||||
NavigateToStoryDetail { epic_id: u32, story_id: u32 },
|
NavigateToStoryDetail { epic_id: u32, story_id: u32 },
|
||||||
@ -15,7 +15,7 @@ pub enum Action {
|
|||||||
Exit,
|
Exit,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
|
||||||
pub enum Status {
|
pub enum Status {
|
||||||
Open,
|
Open,
|
||||||
InProgress,
|
InProgress,
|
||||||
@ -34,7 +34,7 @@ impl Display for Status {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
|
||||||
pub struct Epic {
|
pub struct Epic {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub description: String,
|
pub description: String,
|
||||||
@ -53,7 +53,7 @@ impl Epic {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
|
||||||
pub struct Story {
|
pub struct Story {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub description: String,
|
pub description: String,
|
||||||
@ -76,3 +76,13 @@ pub struct DBState {
|
|||||||
pub epics: HashMap<u32, Epic>,
|
pub epics: HashMap<u32, Epic>,
|
||||||
pub stories: HashMap<u32, Story>,
|
pub stories: HashMap<u32, Story>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl DBState {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
last_item_id: 0,
|
||||||
|
epics: HashMap::new(),
|
||||||
|
stories: HashMap::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
#[allow(unused_imports)]
|
||||||
use anyhow::{anyhow, Context, Ok, Result};
|
use anyhow::{anyhow, Context, Ok, Result};
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
@ -45,7 +46,9 @@ impl Navigator {
|
|||||||
}
|
}
|
||||||
Action::NavigateToPreviousPage => {
|
Action::NavigateToPreviousPage => {
|
||||||
// remove the last page from the pages vector
|
// remove the last page from the pages vector
|
||||||
let _ = self.pages.pop();
|
if !self.pages.is_empty() {
|
||||||
|
self.pages.pop();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Action::CreateEpic => {
|
Action::CreateEpic => {
|
||||||
// prompt the user to create a new epic and persist it in the database
|
// prompt the user to create a new epic and persist it in the database
|
||||||
@ -61,9 +64,12 @@ impl Navigator {
|
|||||||
}
|
}
|
||||||
Action::DeleteEpic { epic_id } => {
|
Action::DeleteEpic { epic_id } => {
|
||||||
// prompt the user to delete the epic and persist it in the database
|
// prompt the user to delete the epic and persist it in the database
|
||||||
self.db
|
if (self.prompts.delete_epic)() {
|
||||||
.delete_epic(epic_id)
|
self.db
|
||||||
.with_context(|| format!("failed to delete epic: {epic_id}"))?;
|
.delete_epic(epic_id)
|
||||||
|
.with_context(|| format!("failed to delete epic: {epic_id}"))?;
|
||||||
|
self.pages.pop();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Action::CreateStory { epic_id } => {
|
Action::CreateStory { epic_id } => {
|
||||||
// prompt the user to create a new story and persist it in the database
|
// prompt the user to create a new story and persist it in the database
|
||||||
@ -73,9 +79,12 @@ impl Navigator {
|
|||||||
}
|
}
|
||||||
Action::UpdateStoryStatus { story_id } => {
|
Action::UpdateStoryStatus { story_id } => {
|
||||||
// prompt the user to update status and persist it in the database
|
// prompt the user to update status and persist it in the database
|
||||||
let status = (self.prompts.update_status)()
|
if let Some(status) = (self.prompts.update_status)() {
|
||||||
.with_context(|| format!("invalid status: {story_id}"))?;
|
let s = status.clone();
|
||||||
self.db.update_story_status(story_id, status)?;
|
self.db
|
||||||
|
.update_story_status(story_id, status)
|
||||||
|
.with_context(|| format!("invalid status: {s}"))?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Action::DeleteStory { epic_id, story_id } => {
|
Action::DeleteStory { epic_id, story_id } => {
|
||||||
// prompt the user to delete the story and persist it in the database
|
// prompt the user to delete the story and persist it in the database
|
||||||
@ -83,6 +92,7 @@ impl Navigator {
|
|||||||
self.db
|
self.db
|
||||||
.delete_story(epic_id, story_id)
|
.delete_story(epic_id, story_id)
|
||||||
.with_context(|| format!("failed to delete story: {story_id}"))?;
|
.with_context(|| format!("failed to delete story: {story_id}"))?;
|
||||||
|
self.pages.pop();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Action::Exit => {
|
Action::Exit => {
|
||||||
@ -95,11 +105,11 @@ impl Navigator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Private functions used for testing
|
// Private functions used for testing
|
||||||
|
#[allow(dead_code)]
|
||||||
fn get_page_count(&self) -> usize {
|
fn get_page_count(&self) -> usize {
|
||||||
self.pages.len()
|
self.pages.len()
|
||||||
}
|
}
|
||||||
|
#[allow(dead_code)]
|
||||||
fn set_prompts(&mut self, prompts: Prompts) {
|
fn set_prompts(&mut self, prompts: Prompts) {
|
||||||
self.prompts = prompts;
|
self.prompts = prompts;
|
||||||
}
|
}
|
||||||
|
@ -14,6 +14,7 @@ use page_helpers::*;
|
|||||||
pub trait Page {
|
pub trait Page {
|
||||||
fn draw_page(&self) -> Result<()>;
|
fn draw_page(&self) -> Result<()>;
|
||||||
fn handle_input(&self, input: &str) -> Result<Option<Action>>;
|
fn handle_input(&self, input: &str) -> Result<Option<Action>>;
|
||||||
|
#[allow(dead_code)]
|
||||||
fn as_any(&self) -> &dyn Any;
|
fn as_any(&self) -> &dyn Any;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -31,11 +32,16 @@ impl Page for HomePage {
|
|||||||
println!("----------------------------- EPICS -----------------------------");
|
println!("----------------------------- EPICS -----------------------------");
|
||||||
println!(" id | name | status ");
|
println!(" id | name | status ");
|
||||||
|
|
||||||
self.db.read_db()?.epics.iter().for_each(|(id, e)| {
|
self.db
|
||||||
print!("{}| ", get_column_string(format!("{id}").as_str(), 12));
|
.read_db()?
|
||||||
print!("{}| ", get_column_string(&e.name, 33));
|
.epics
|
||||||
print!("{}", get_column_string(&e.status.to_string(), 17));
|
.iter()
|
||||||
});
|
.sorted()
|
||||||
|
.for_each(|(id, e)| {
|
||||||
|
print!("{}| ", get_column_string(format!("{id}").as_str(), 12));
|
||||||
|
print!("{}| ", get_column_string(&e.name, 33));
|
||||||
|
print!("{}", get_column_string(&e.status.to_string(), 17));
|
||||||
|
});
|
||||||
|
|
||||||
println!();
|
println!();
|
||||||
println!();
|
println!();
|
||||||
@ -95,7 +101,7 @@ impl Page for EpicDetail {
|
|||||||
|
|
||||||
let stories = &db_state.stories;
|
let stories = &db_state.stories;
|
||||||
|
|
||||||
for (id, e) in stories {
|
for (id, e) in stories.iter().sorted() {
|
||||||
print!("{}| ", get_column_string(format!("{id}").as_str(), 12));
|
print!("{}| ", get_column_string(format!("{id}").as_str(), 12));
|
||||||
print!("{}| ", get_column_string(&e.name, 33));
|
print!("{}| ", get_column_string(&e.name, 33));
|
||||||
print!("{}", get_column_string(&e.status.to_string(), 17));
|
print!("{}", get_column_string(&e.status.to_string(), 17));
|
||||||
|
@ -46,7 +46,7 @@ fn create_story_prompt() -> Story {
|
|||||||
fn delete_epic_prompt() -> bool {
|
fn delete_epic_prompt() -> bool {
|
||||||
static QUESTION: &str = "Are you sure you want to delete this epic? All stories in this epic will also be deleted [Y/n]:";
|
static QUESTION: &str = "Are you sure you want to delete this epic? All stories in this epic will also be deleted [Y/n]:";
|
||||||
println!("{DELIMITER}");
|
println!("{DELIMITER}");
|
||||||
print!("{QUESTION}");
|
println!("{QUESTION}");
|
||||||
let decision = matches!(get_user_input().as_str(), "y" | "Y");
|
let decision = matches!(get_user_input().as_str(), "y" | "Y");
|
||||||
println!();
|
println!();
|
||||||
decision
|
decision
|
||||||
@ -55,7 +55,7 @@ fn delete_epic_prompt() -> bool {
|
|||||||
fn delete_story_prompt() -> bool {
|
fn delete_story_prompt() -> bool {
|
||||||
static QUESTION: &str = "Are you sure you want to delete this story? [Y/n]:";
|
static QUESTION: &str = "Are you sure you want to delete this story? [Y/n]:";
|
||||||
println!("{DELIMITER}");
|
println!("{DELIMITER}");
|
||||||
print!("{QUESTION}");
|
println!("{QUESTION}");
|
||||||
let decision = matches!(get_user_input().as_str(), "y" | "Y");
|
let decision = matches!(get_user_input().as_str(), "y" | "Y");
|
||||||
println!();
|
println!();
|
||||||
decision
|
decision
|
||||||
@ -64,14 +64,12 @@ fn delete_story_prompt() -> bool {
|
|||||||
fn update_status_prompt() -> Option<Status> {
|
fn update_status_prompt() -> Option<Status> {
|
||||||
static QUESTION: &str = "New Status (1 - OPEN, 2 - IN-PROGRESS, 3 - RESOLVED, 4 - CLOSED):";
|
static QUESTION: &str = "New Status (1 - OPEN, 2 - IN-PROGRESS, 3 - RESOLVED, 4 - CLOSED):";
|
||||||
println!("{DELIMITER}");
|
println!("{DELIMITER}");
|
||||||
print!("{QUESTION}");
|
println!("{QUESTION}");
|
||||||
let decision = match get_user_input().as_str() {
|
match get_user_input().as_str() {
|
||||||
"1" => Some(Status::Open),
|
"1" => Some(Status::Open),
|
||||||
"2" => Some(Status::InProgress),
|
"2" => Some(Status::InProgress),
|
||||||
"3" => Some(Status::Resolved),
|
"3" => Some(Status::Resolved),
|
||||||
"4" => Some(Status::Closed),
|
"4" => Some(Status::Closed),
|
||||||
_ => None,
|
_ => None,
|
||||||
};
|
}
|
||||||
println!();
|
|
||||||
decision
|
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user