use crate::backlog::{BacklogItem, ItemId};
use crate::error::{Error, Result};
use crate::service::relations::validate_parent;
use crate::service::{open_board, open_board_locked, validate_sprint_assignment};
use crate::storage::BacklogItemRepository;
use chrono::Utc;
use std::path::{Path, PathBuf};
#[derive(Debug, Default, Clone)]
pub struct ItemEdit {
pub title: Option<String>,
pub points: Option<u32>,
pub labels: Option<Vec<String>>,
pub assignee: Option<String>,
pub sprint: Option<String>,
pub body: Option<String>,
pub parent: Option<Option<ItemId>>,
}
impl ItemEdit {
fn is_empty(&self) -> bool {
self.title.is_none()
&& self.points.is_none()
&& self.labels.is_none()
&& self.assignee.is_none()
&& self.sprint.is_none()
&& self.body.is_none()
&& self.parent.is_none()
}
}
pub async fn edit_item(project_dir: &Path, id: &ItemId, edit: ItemEdit) -> Result<BacklogItem> {
let (_board_dir, repo, _config, _lock) = open_board_locked(project_dir).await?;
if edit.is_empty() {
return Err(Error::NothingToUpdate);
}
if let Some(title) = &edit.title
&& title.trim().is_empty()
{
return Err(Error::EmptyTitle);
}
let validated_sprint = match edit.sprint.as_deref() {
Some(raw) => Some(validate_sprint_assignment(&repo, raw).await?),
None => None,
};
let mut item = repo.load(id).await?;
if let Some(Some(parent)) = &edit.parent {
let items = repo.list().await?;
validate_parent(&items, id, parent)?;
}
if let Some(title) = edit.title {
item.title = title;
}
if let Some(points) = edit.points {
item.points = Some(points);
}
if let Some(labels) = edit.labels {
item.labels = labels;
}
if let Some(assignee) = edit.assignee {
item.assignee = Some(assignee);
}
if let Some(sprint) = validated_sprint {
item.sprint = Some(sprint.to_string());
}
if let Some(body) = edit.body {
item.body = body;
}
if let Some(parent) = edit.parent {
item.parent = parent;
}
item.updated = Utc::now();
repo.save(&item).await?;
repo.commit(&format!("pinto: update {}", item.id)).await?;
Ok(item)
}
pub async fn item_edit_template(project_dir: &Path, id: &ItemId) -> Result<String> {
let (_board_dir, repo, _config) = open_board(project_dir).await?;
let item = repo.load(id).await?;
let markdown = crate::storage::item_to_markdown(&item)?;
Ok(with_edit_guidance(&markdown))
}
fn with_edit_guidance(markdown: &str) -> String {
let guide = crate::i18n::current().text(crate::i18n::Message::EditGuidance);
match markdown.split_once('\n') {
Some((first, rest)) if first == "+++" => format!("{first}\n{guide}\n{rest}"),
_ => markdown.to_string(),
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum EditOutcome {
Updated(Box<BacklogItem>),
Unchanged,
}
pub async fn apply_item_edit(project_dir: &Path, id: &ItemId, edited: &str) -> Result<EditOutcome> {
let (_board_dir, repo, _config, _lock) = open_board_locked(project_dir).await?;
let stored = repo.load(id).await?;
let display_path = PathBuf::from(format!("{id}.md"));
let parsed = crate::storage::item_from_markdown(edited, &display_path).map_err(|e| {
Error::EditorInvalid {
message: e.to_string(),
}
})?;
let validated_sprint = match parsed.sprint.as_deref() {
Some(raw) => Some(
validate_sprint_assignment(&repo, raw)
.await
.map_err(|error| Error::EditorInvalid {
message: error.to_string(),
})?,
),
None => None,
};
let mut updated = stored.clone();
updated.title = parsed.title;
updated.points = parsed.points;
updated.labels = parsed.labels;
updated.assignee = parsed.assignee;
updated.sprint = validated_sprint.map(|sprint| sprint.to_string());
updated.body = parsed.body;
if updated.parent != parsed.parent {
if let Some(parent) = &parsed.parent {
let items = repo.list().await?;
validate_parent(&items, id, parent)?;
}
updated.parent = parsed.parent;
}
if updated == stored {
return Ok(EditOutcome::Unchanged);
}
updated.updated = Utc::now();
repo.save(&updated).await?;
repo.commit(&format!("pinto: update {}", updated.id))
.await?;
Ok(EditOutcome::Updated(Box::new(updated)))
}