feat: adds main loop

fix: some logic bugs
This commit is contained in:
itsscb 2024-08-12 22:26:24 +02:00
parent 786ab80474
commit 6f8b865e33
9 changed files with 136 additions and 37 deletions

View File

@ -9,6 +9,7 @@ serde = {version = "1.0.204", features = ["derive"]}
serde_json = "1.0.122"
ellipse = "0.2.0"
itertools = "0.13.0"
clearscreen = "3.0.0"
[dev-dependencies]
tempfile = "3.11.0"

5
db.json Normal file
View File

@ -0,0 +1,5 @@
{
"last_item_id": 3,
"epics": {},
"stories": {}
}

View File

@ -1,4 +1,5 @@
use std::fs;
use std::fs::{self, OpenOptions};
use std::path::Path;
use anyhow::{anyhow, Result};
@ -9,10 +10,23 @@ pub struct JiraDatabase {
}
impl JiraDatabase {
pub fn new(file_path: String) -> Self {
Self {
database: Box::new(JSONFileDatabase { file_path }),
pub fn new(file_path: &str) -> Result<Self> {
let path = file_path.to_owned();
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> {
@ -138,6 +152,7 @@ pub mod test_utils {
}
impl MockDB {
#[allow(dead_code)]
pub fn new() -> Self {
Self {
last_written_state: RefCell::new(DBState {

View File

@ -5,5 +5,9 @@ pub fn get_user_input() -> String {
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();
}

View File

@ -1,9 +1,59 @@
mod db;
mod io_utils;
use std::rc::Rc;
mod models;
mod navigator;
mod db;
use anyhow::Result;
use db::*;
mod ui;
fn main() {
println!("Welcome scrumtask-cli!");
mod io_utils;
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);
}
}
}
}

View File

@ -1,7 +1,7 @@
use serde::{Deserialize, Serialize};
use std::{collections::HashMap, fmt::Display};
#[derive(Debug, PartialEq, Eq)]
#[derive(Debug, PartialEq, Eq, Clone)]
pub enum Action {
NavigateToEpicDetail { epic_id: u32 },
NavigateToStoryDetail { epic_id: u32, story_id: u32 },
@ -15,7 +15,7 @@ pub enum Action {
Exit,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub enum Status {
Open,
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 name: 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 name: String,
pub description: String,
@ -76,3 +76,13 @@ pub struct DBState {
pub epics: HashMap<u32, Epic>,
pub stories: HashMap<u32, Story>,
}
impl DBState {
pub fn new() -> Self {
Self {
last_item_id: 0,
epics: HashMap::new(),
stories: HashMap::new(),
}
}
}

View File

@ -1,3 +1,4 @@
#[allow(unused_imports)]
use anyhow::{anyhow, Context, Ok, Result};
use std::rc::Rc;
@ -45,7 +46,9 @@ impl Navigator {
}
Action::NavigateToPreviousPage => {
// remove the last page from the pages vector
let _ = self.pages.pop();
if !self.pages.is_empty() {
self.pages.pop();
}
}
Action::CreateEpic => {
// prompt the user to create a new epic and persist it in the database
@ -61,9 +64,12 @@ impl Navigator {
}
Action::DeleteEpic { epic_id } => {
// prompt the user to delete the epic and persist it in the database
self.db
.delete_epic(epic_id)
.with_context(|| format!("failed to delete epic: {epic_id}"))?;
if (self.prompts.delete_epic)() {
self.db
.delete_epic(epic_id)
.with_context(|| format!("failed to delete epic: {epic_id}"))?;
self.pages.pop();
}
}
Action::CreateStory { epic_id } => {
// prompt the user to create a new story and persist it in the database
@ -73,9 +79,12 @@ impl Navigator {
}
Action::UpdateStoryStatus { story_id } => {
// prompt the user to update status and persist it in the database
let status = (self.prompts.update_status)()
.with_context(|| format!("invalid status: {story_id}"))?;
self.db.update_story_status(story_id, status)?;
if let Some(status) = (self.prompts.update_status)() {
let s = status.clone();
self.db
.update_story_status(story_id, status)
.with_context(|| format!("invalid status: {s}"))?;
}
}
Action::DeleteStory { epic_id, story_id } => {
// prompt the user to delete the story and persist it in the database
@ -83,6 +92,7 @@ impl Navigator {
self.db
.delete_story(epic_id, story_id)
.with_context(|| format!("failed to delete story: {story_id}"))?;
self.pages.pop();
}
}
Action::Exit => {
@ -95,11 +105,11 @@ impl Navigator {
}
// Private functions used for testing
#[allow(dead_code)]
fn get_page_count(&self) -> usize {
self.pages.len()
}
#[allow(dead_code)]
fn set_prompts(&mut self, prompts: Prompts) {
self.prompts = prompts;
}

View File

@ -14,6 +14,7 @@ use page_helpers::*;
pub trait Page {
fn draw_page(&self) -> Result<()>;
fn handle_input(&self, input: &str) -> Result<Option<Action>>;
#[allow(dead_code)]
fn as_any(&self) -> &dyn Any;
}
@ -31,11 +32,16 @@ impl Page for HomePage {
println!("----------------------------- EPICS -----------------------------");
println!(" id | name | status ");
self.db.read_db()?.epics.iter().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));
});
self.db
.read_db()?
.epics
.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!();
@ -95,7 +101,7 @@ impl Page for EpicDetail {
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(&e.name, 33));
print!("{}", get_column_string(&e.status.to_string(), 17));

View File

@ -46,7 +46,7 @@ fn create_story_prompt() -> Story {
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]:";
println!("{DELIMITER}");
print!("{QUESTION}");
println!("{QUESTION}");
let decision = matches!(get_user_input().as_str(), "y" | "Y");
println!();
decision
@ -55,7 +55,7 @@ fn delete_epic_prompt() -> bool {
fn delete_story_prompt() -> bool {
static QUESTION: &str = "Are you sure you want to delete this story? [Y/n]:";
println!("{DELIMITER}");
print!("{QUESTION}");
println!("{QUESTION}");
let decision = matches!(get_user_input().as_str(), "y" | "Y");
println!();
decision
@ -64,14 +64,12 @@ fn delete_story_prompt() -> bool {
fn update_status_prompt() -> Option<Status> {
static QUESTION: &str = "New Status (1 - OPEN, 2 - IN-PROGRESS, 3 - RESOLVED, 4 - CLOSED):";
println!("{DELIMITER}");
print!("{QUESTION}");
let decision = match get_user_input().as_str() {
println!("{QUESTION}");
match get_user_input().as_str() {
"1" => Some(Status::Open),
"2" => Some(Status::InProgress),
"3" => Some(Status::Resolved),
"4" => Some(Status::Closed),
_ => None,
};
println!();
decision
}
}