metis_docs_tui/app/operations/
document_ops.rs1use crate::app::state::BacklogCategory;
2use crate::app::App;
3use crate::models::{kanban::DocumentObject, AppState, BoardType};
4use anyhow::Result;
5use metis_core::{domain::documents::types::DocumentType, Document};
6
7impl App {
8 fn handle_creation_error(&mut self, message: String) {
12 self.add_error_message(message);
13 self.ui_state.set_app_state(AppState::Normal);
14 self.ui_state.reset_input();
15 }
16
17 async fn handle_creation_success(
19 &mut self,
20 doc_type: DocumentType,
21 doc_id: Option<String>,
22 ) -> Result<()> {
23 if let Some(id) = doc_id {
25 if self.ui_state.current_board == BoardType::Backlog {
26 if let Some(tag) = self.ui_state.selected_backlog_category.as_tag() {
27 if let Some(sync_service) = &self.sync_service {
28 let _ = sync_service.sync_database().await;
29 }
30 let _ = self.add_tag_to_document(&id, tag).await;
31 }
32 }
33 }
34
35 self.add_success_message(format!("{} created successfully", doc_type));
36 self.ui_state.set_app_state(AppState::Normal);
37 self.ui_state.reset_input();
38
39 if let Some(sync_service) = &self.sync_service {
41 let _ = sync_service.sync_database().await;
42 }
43 self.load_documents().await?;
44 Ok(())
45 }
46
47 fn get_document_type_for_board(&self) -> DocumentType {
49 match self.ui_state.current_board {
50 BoardType::Strategy => DocumentType::Strategy,
51 BoardType::Initiative => DocumentType::Initiative,
52 BoardType::Task => DocumentType::Task,
53 BoardType::Adr => DocumentType::Adr,
54 BoardType::Backlog => DocumentType::Task,
55 }
56 }
57
58 fn find_parent_id(&mut self, doc_type: DocumentType) -> Option<String> {
60 match doc_type {
61 DocumentType::Initiative => {
62 if self.core_state.flight_config.strategies_enabled {
64 let strategy_items: Vec<_> = self
65 .ui_state
66 .strategy_board
67 .columns
68 .iter()
69 .flat_map(|col| &col.items)
70 .collect();
71
72 if let Some(strategy) = strategy_items.first() {
73 Some(strategy.id())
74 } else {
75 self.handle_creation_error(
76 "Cannot create initiative: No strategy available as parent".to_string(),
77 );
78 None
79 }
80 } else {
81 None
83 }
84 }
85 DocumentType::Task => {
86 if self.ui_state.current_board == BoardType::Task {
87 if self.core_state.flight_config.initiatives_enabled {
89 let initiative_items: Vec<_> = self
90 .ui_state
91 .initiative_board
92 .columns
93 .iter()
94 .flat_map(|col| &col.items)
95 .collect();
96
97 if let Some(initiative) = initiative_items.first() {
98 Some(initiative.id())
99 } else {
100 self.handle_creation_error(
101 "Cannot create task: No initiative available as parent".to_string(),
102 );
103 None
104 }
105 } else {
106 None
108 }
109 } else {
110 None }
112 }
113 _ => None,
114 }
115 }
116
117 async fn create_child_task_from_initiative(&self, title: String) -> Result<String> {
119 if let Some(document_service) = &self.document_service {
120 let initiative_items: Vec<_> = self
121 .ui_state
122 .initiative_board
123 .columns
124 .iter()
125 .flat_map(|col| &col.items)
126 .collect();
127
128 if let Some(initiative) = initiative_items.first() {
129 let strategy_id = match &initiative.document {
130 DocumentObject::Initiative(init) => init.parent_id(),
131 _ => None,
132 };
133
134 let effective_strategy_id = if let Some(strategy_id) = strategy_id {
135 strategy_id.to_string()
136 } else {
137 "NULL".to_string()
139 };
140
141 document_service
142 .create_child_task(title, effective_strategy_id, initiative.id())
143 .await
144 } else {
145 Err(anyhow::anyhow!("No initiative found for task creation"))
146 }
147 } else {
148 Err(anyhow::anyhow!("Document service not available"))
149 }
150 }
151
152 pub async fn create_new_document(&mut self) -> Result<()> {
153 let title = self.ui_state.input_title.value().to_string();
154 if title.trim().is_empty() {
155 self.handle_creation_error("Title cannot be empty".to_string());
156 return Ok(());
157 }
158
159 let doc_type = self.get_document_type_for_board();
160
161 let parent_id = self.find_parent_id(doc_type);
163
164 let needs_parent = match doc_type {
166 DocumentType::Strategy | DocumentType::Adr => false, DocumentType::Initiative => self.core_state.flight_config.strategies_enabled, DocumentType::Task => {
169 if self.ui_state.current_board == BoardType::Backlog {
171 false
172 } else {
173 self.core_state.flight_config.initiatives_enabled }
175 }
176 _ => false,
177 };
178
179 if needs_parent && parent_id.is_none() {
180 return Ok(()); }
182
183 let result = if doc_type == DocumentType::Task
185 && parent_id.is_some()
186 && self.ui_state.current_board == BoardType::Task
187 {
188 self.create_child_task_from_initiative(title).await
189 } else if let Some(document_service) = &self.document_service {
190 document_service
191 .create_document(doc_type, title, None, parent_id)
192 .await
193 } else {
194 Err(anyhow::anyhow!("Document service not available"))
195 };
196
197 match result {
198 Ok(doc_id) => {
199 self.handle_creation_success(doc_type, Some(doc_id)).await?;
200 }
201 Err(e) => {
202 self.handle_creation_error(format!("Failed to create {}: {}", doc_type, e));
203 }
204 }
205
206 Ok(())
207 }
208
209 pub async fn create_child_document(&mut self) -> Result<()> {
210 let title = self.ui_state.input_title.value().to_string();
211 if title.trim().is_empty() {
212 self.add_error_message("Title cannot be empty".to_string());
213 self.ui_state.set_app_state(AppState::Normal);
214 self.ui_state.reset_input();
215 return Ok(());
216 }
217
218 if let Some(parent_item) = self.get_selected_item() {
220 if let Some(document_service) = &self.document_service {
221 let child_doc_type = match parent_item.doc_type() {
223 DocumentType::Strategy => DocumentType::Initiative,
224 DocumentType::Initiative => DocumentType::Task,
225 _ => {
226 self.add_error_message(
227 "Cannot create child for this document type".to_string(),
228 );
229 self.ui_state.set_app_state(AppState::Normal);
230 self.ui_state.reset_input();
231 return Ok(());
232 }
233 };
234
235 let parent_id = parent_item.id();
236
237 match child_doc_type {
239 DocumentType::Task => {
240 if let DocumentObject::Initiative(ref initiative) = &parent_item.document {
243 let effective_strategy_id =
244 if let Some(strategy_id) = initiative.parent_id() {
245 strategy_id.to_string()
246 } else {
247 "NULL".to_string()
249 };
250
251 match document_service
252 .create_child_task(
253 title,
254 effective_strategy_id,
255 parent_id.to_string(),
256 )
257 .await
258 {
259 Ok(_) => {
260 self.ui_state.set_app_state(AppState::Normal);
261 self.ui_state.reset_input();
262 if let Some(sync_service) = &self.sync_service {
264 let _ = sync_service.sync_database().await;
265 }
266 self.load_documents().await?;
267 }
268 Err(e) => {
269 self.add_error_message(format!("Failed to create task: {}", e));
270 self.ui_state.set_app_state(AppState::Normal);
271 self.ui_state.reset_input();
272 }
273 }
274 } else {
275 self.add_error_message(
276 "Selected item is not an initiative".to_string(),
277 );
278 self.ui_state.set_app_state(AppState::Normal);
279 self.ui_state.reset_input();
280 }
281 }
282 _ => {
283 match document_service
285 .create_document(
286 child_doc_type,
287 title,
288 None, Some(parent_id.to_string()),
290 )
291 .await
292 {
293 Ok(_) => {
294 self.add_success_message(format!(
295 "{} created successfully",
296 child_doc_type
297 ));
298 self.ui_state.set_app_state(AppState::Normal);
299 self.ui_state.reset_input();
300 if let Some(sync_service) = &self.sync_service {
302 let _ = sync_service.sync_database().await;
303 }
304 self.load_documents().await?;
305 }
306 Err(e) => {
307 self.add_error_message(format!(
308 "Failed to create {}: {}",
309 child_doc_type, e
310 ));
311 self.ui_state.set_app_state(AppState::Normal);
312 self.ui_state.reset_input();
313 }
314 }
315 }
316 }
317 }
318 } else {
319 let error_msg = match self.ui_state.current_board {
321 BoardType::Strategy => {
322 "Please select a strategy first to create an initiative under it"
323 }
324 BoardType::Initiative => {
325 "Please select an initiative first to create a task under it"
326 }
327 _ => "Please select a parent document first",
328 };
329 self.add_error_message(error_msg.to_string());
330 self.ui_state.set_app_state(AppState::Normal);
331 self.ui_state.reset_input();
332 }
333
334 Ok(())
335 }
336
337 pub async fn create_adr_from_ticket(&mut self) -> Result<()> {
338 let title = self.ui_state.input_title.value().to_string();
340 if title.trim().is_empty() {
341 self.add_error_message("ADR title cannot be empty".to_string());
342 self.ui_state.set_app_state(AppState::Normal);
343 self.ui_state.reset_input();
344 return Ok(());
345 }
346
347 let context = if let Some(selected_item) = self.get_selected_item() {
349 match &selected_item.document {
351 DocumentObject::Strategy(strategy) => Some(format!(
352 "Context from strategy '{}': {}",
353 strategy.title(),
354 strategy.content().full_content()
355 )),
356 DocumentObject::Initiative(initiative) => Some(format!(
357 "Context from initiative '{}': {}",
358 initiative.title(),
359 initiative.content().full_content()
360 )),
361 DocumentObject::Task(task) => Some(format!(
362 "Context from task '{}': {}",
363 task.title(),
364 task.content().full_content()
365 )),
366 DocumentObject::Adr(_) => None, }
368 } else {
369 None };
371
372 if let Some(document_service) = &self.document_service {
373 match document_service.create_adr(title, context).await {
374 Ok(_file_path) => {
375 self.add_success_message("ADR created successfully".to_string());
376 self.ui_state.set_app_state(AppState::Normal);
377 self.ui_state.reset_input();
378 if let Some(sync_service) = &self.sync_service {
380 let _ = sync_service.sync_database().await;
381 }
382 self.load_documents().await?;
383 }
384 Err(e) => {
385 self.add_error_message(format!("Failed to create ADR: {}", e));
386 self.ui_state.set_app_state(AppState::Normal);
387 self.ui_state.reset_input();
388 }
389 }
390 }
391
392 Ok(())
393 }
394
395 pub fn move_category_selection_up(&mut self) {
397 if self.ui_state.backlog_category_selection > 0 {
398 self.ui_state.backlog_category_selection -= 1;
399 self.update_selected_category();
400 }
401 }
402
403 pub fn move_category_selection_down(&mut self) {
404 if self.ui_state.backlog_category_selection < 3 {
405 self.ui_state.backlog_category_selection += 1;
407 self.update_selected_category();
408 }
409 }
410
411 pub fn confirm_category_selection(&mut self) {
412 self.ui_state.set_app_state(AppState::CreatingDocument);
414 }
415
416 fn update_selected_category(&mut self) {
417 self.ui_state.selected_backlog_category = match self.ui_state.backlog_category_selection {
418 0 => BacklogCategory::General,
419 1 => BacklogCategory::Bug,
420 2 => BacklogCategory::Feature,
421 3 => BacklogCategory::TechDebt,
422 _ => BacklogCategory::General,
423 };
424 }
425
426 async fn add_tag_to_document(&self, doc_id: &str, tag: &str) -> Result<()> {
427 if let Some(document_service) = &self.document_service {
428 let docs = document_service.load_documents_from_database().await?;
430
431 if let Some(doc) = docs.iter().find(|d| d.id == doc_id) {
432 let content = std::fs::read_to_string(&doc.filepath)?;
434
435 let updated_content = if content.contains("tags:") {
437 let lines: Vec<&str> = content.lines().collect();
439 let mut new_lines = Vec::new();
440 let mut in_tags_section = false;
441 let mut tags_section_ended = false;
442 let tag_line = format!(" - \"{}\"", tag);
443
444 for line in lines {
445 if line.trim() == "tags:" {
446 in_tags_section = true;
447 new_lines.push(line.to_string());
448 } else if in_tags_section && !tags_section_ended {
449 if line.trim().starts_with("- \"#") {
450 new_lines.push(line.to_string());
452 } else if line.trim().is_empty() {
453 new_lines.push(tag_line.clone());
455 new_lines.push(line.to_string());
456 tags_section_ended = true;
457 } else {
458 new_lines.push(tag_line.clone());
460 new_lines.push(line.to_string());
461 tags_section_ended = true;
462 }
463 } else {
464 new_lines.push(line.to_string());
465 }
466 }
467
468 new_lines.join("\n")
469 } else {
470 content
472 };
473
474 std::fs::write(&doc.filepath, updated_content)?;
476 }
477 }
478 Ok(())
479 }
480}