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"
|
||||
ellipse = "0.2.0"
|
||||
itertools = "0.13.0"
|
||||
clearscreen = "3.0.0"
|
||||
|
||||
[dev-dependencies]
|
||||
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};
|
||||
|
||||
@ -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 {
|
||||
|
@ -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();
|
||||
}
|
||||
|
60
src/main.rs
60
src/main.rs
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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));
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user