kanban-tui 0.8.0

Terminal user interface for the kanban project management tool
Documentation
//! Snapshot functionality - uses domain Snapshot with TUI extensions.
//!
//! The core Snapshot type is in kanban-domain. This module provides
//! TUI-specific extension methods for App integration.
//! Serialization (to_json_bytes/from_json_bytes) lives on domain Snapshot directly.

use crate::app::App;

use kanban_domain::Snapshot;

/// Extension trait for App-specific snapshot operations.
///
/// These methods bridge between the domain Snapshot and the TUI App.
pub trait TuiSnapshot {
    /// Create a snapshot from current app state.
    fn from_app(app: &App) -> Self;

    /// Apply snapshot to app state (overwrites).
    fn apply_to_app(&self, app: &mut App) -> kanban_domain::KanbanResult<()>;
}

impl TuiSnapshot for Snapshot {
    fn from_app(app: &App) -> Self {
        app.ctx.snapshot().unwrap_or_default()
    }

    fn apply_to_app(&self, app: &mut App) -> kanban_domain::KanbanResult<()> {
        app.ctx.apply_snapshot(self.clone())?;

        // Sync sort field/order from active board to preserve user's selection
        // after reload. The snapshot's `boards` carries every head (live and
        // archived), so resolving by id works for either.
        if let Some(board_id) = app.selection.active_board_id {
            if let Some(board) = self.boards.iter().find(|b| b.id == board_id) {
                app.filter.current_sort_field = Some(board.task_sort_field);
                app.filter.current_sort_order = Some(board.task_sort_order);
            }
        }
        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use kanban_domain::{Board, DependencyGraph, SortField};

    #[test]
    fn test_snapshot_serialization() {
        use kanban_persistence::{snapshot_from_json_bytes, snapshot_to_json_bytes};

        let snapshot = Snapshot {
            archived_boards: Vec::new(),
            boards: vec![],
            columns: vec![],
            cards: vec![],
            archived_cards: vec![],
            sprints: vec![],
            graph: DependencyGraph::new(),
        };

        let bytes = snapshot_to_json_bytes(&snapshot).unwrap();
        let restored = snapshot_from_json_bytes(&bytes).unwrap();

        assert_eq!(restored.boards.len(), 0);
    }

    #[test]
    fn test_apply_to_app_syncs_sort_field_from_board() {
        // Create a board with Position sort field
        let mut board = Board::new("Test", None::<String>);
        board.update_task_sort(SortField::Position, kanban_domain::SortOrder::Ascending);
        let board_id = board.id;

        let snapshot = Snapshot {
            archived_boards: Vec::new(),
            boards: vec![board],
            columns: vec![],
            cards: vec![],
            archived_cards: vec![],
            sprints: vec![],
            graph: DependencyGraph::new(),
        };

        // Create a minimal app with the active board set by id.
        let mut app = App::test_default();
        app.selection.active_board_id = Some(board_id);
        app.filter.current_sort_field = Some(SortField::Default);

        // Apply snapshot - should sync sort field from board
        snapshot.apply_to_app(&mut app).unwrap();

        // After apply, current_sort_field should match the board's task_sort_field
        assert_eq!(
            app.filter.current_sort_field,
            Some(SortField::Position),
            "apply_to_app should sync current_sort_field from active board"
        );
    }
}