Skip to main content

communitas_ui_api/
kanban.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! Kanban DTOs for board, column, card, and related views.
4//!
5//! These types are designed for UI rendering and MCP tool responses.
6//! They provide a simplified view of the underlying CRDT-based kanban data.
7
8use serde::{Deserialize, Serialize};
9
10use crate::SyncState;
11
12/// Summary of a board for list views.
13#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
14pub struct BoardSummary {
15    /// Unique board identifier.
16    pub id: String,
17    /// Display name of the board.
18    pub name: String,
19    /// Entity (project/group) that owns this board.
20    pub entity_id: String,
21    /// Number of columns in the board.
22    pub column_count: u32,
23    /// Total number of cards across all columns.
24    pub card_count: u32,
25    /// Unix timestamp (ms) of last activity, if any.
26    pub last_activity: Option<i64>,
27}
28
29/// Full board view with columns and cards.
30#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
31pub struct BoardView {
32    /// Unique board identifier.
33    pub id: String,
34    /// Display name of the board.
35    pub name: String,
36    /// Entity (project/group) that owns this board.
37    pub entity_id: String,
38    /// Optional description of the board.
39    pub description: Option<String>,
40    /// Columns in display order.
41    pub columns: Vec<ColumnView>,
42    /// Board configuration settings.
43    pub settings: BoardSettings,
44    /// Unix timestamp (ms) when the board was created.
45    pub created_at: i64,
46    /// Unix timestamp (ms) of last update.
47    pub updated_at: i64,
48}
49
50/// A column within a board.
51#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
52pub struct ColumnView {
53    /// Unique column identifier.
54    pub id: String,
55    /// Display name of the column.
56    pub name: String,
57    /// Position within the board (0-indexed).
58    pub position: u32,
59    /// Cards in this column in display order.
60    pub cards: Vec<CardView>,
61    /// Optional WIP (work-in-progress) limit.
62    pub wip_limit: Option<u32>,
63}
64
65/// A card (work item) in a column.
66#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
67pub struct CardView {
68    /// Unique card identifier.
69    pub id: String,
70    /// Card title.
71    pub title: String,
72    /// Optional description (may be truncated for list views).
73    pub description: Option<String>,
74    /// Current workflow state.
75    pub state: CardState,
76    /// Priority level for the card.
77    pub priority: Option<PriorityView>,
78    /// Four-word identities of assigned users.
79    pub assignees: Vec<String>,
80    /// Tags attached to this card.
81    pub tags: Vec<TagView>,
82    /// Optional due date as Unix timestamp (ms).
83    pub due_date: Option<i64>,
84    /// Checklist progress summary, if the card has steps.
85    pub checklist_progress: Option<ChecklistProgress>,
86    /// Position within the column (0-indexed).
87    pub position: u32,
88    /// Linked message thread ID for discussions.
89    pub linked_thread_id: Option<String>,
90    /// Display name of the linked thread (if any).
91    pub linked_thread_name: Option<String>,
92    /// Sync state of this card.
93    pub sync_state: SyncState,
94}
95
96/// Detailed card view with full content.
97///
98/// Extends [`CardView`] with steps, comments, attachments, and activity.
99#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
100pub struct CardDetail {
101    /// Unique card identifier.
102    pub id: String,
103    /// Card title.
104    pub title: String,
105    /// Full description content.
106    pub description: Option<String>,
107    /// Current workflow state.
108    pub state: CardState,
109    /// Priority level for the card.
110    pub priority: Option<PriorityView>,
111    /// Four-word identities of assigned users.
112    pub assignees: Vec<String>,
113    /// Tags attached to this card.
114    pub tags: Vec<TagView>,
115    /// Optional due date as Unix timestamp (ms).
116    pub due_date: Option<i64>,
117    /// Checklist progress summary.
118    pub checklist_progress: Option<ChecklistProgress>,
119    /// Position within the column (0-indexed).
120    pub position: u32,
121    /// Checklist steps.
122    pub steps: Vec<StepView>,
123    /// Comments on this card.
124    pub comments: Vec<CommentView>,
125    /// File attachments.
126    pub attachments: Vec<AttachmentView>,
127    /// Activity log entries.
128    pub activity: Vec<ActivityEntry>,
129    /// Linked message thread ID for discussions.
130    pub linked_thread_id: Option<String>,
131    /// Display name of the linked thread (if any).
132    pub linked_thread_name: Option<String>,
133    /// Sync state of this card.
134    pub sync_state: SyncState,
135}
136
137/// A tag for categorizing cards.
138#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
139pub struct TagView {
140    /// Unique tag identifier.
141    pub id: String,
142    /// Display name of the tag.
143    pub name: String,
144    /// Hex color code (e.g., "#FF5733").
145    pub color: String,
146}
147
148/// A checklist step within a card.
149#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
150pub struct StepView {
151    /// Unique step identifier.
152    pub id: String,
153    /// Step text/title.
154    pub title: String,
155    /// Whether the step is completed.
156    pub completed: bool,
157}
158
159/// A comment on a card.
160#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
161pub struct CommentView {
162    /// Unique comment identifier.
163    pub id: String,
164    /// Four-word identity of the author.
165    pub author_id: String,
166    /// Display name of the author.
167    pub author_name: String,
168    /// Comment text content.
169    pub text: String,
170    /// Unix timestamp (ms) when the comment was created.
171    pub created_at: i64,
172}
173
174/// Summary of checklist completion progress.
175#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
176pub struct ChecklistProgress {
177    /// Number of completed steps.
178    pub completed: u32,
179    /// Total number of steps.
180    pub total: u32,
181}
182
183/// Priority level for a card.
184///
185/// Used by UI layers to display visual indicators and support filtering/sorting.
186#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
187pub enum PriorityView {
188    /// Highest priority - requires immediate attention.
189    Urgent,
190    /// High priority - should be addressed soon.
191    High,
192    /// Normal priority - standard work items.
193    #[default]
194    Normal,
195    /// Low priority - can be deferred.
196    Low,
197}
198
199impl PriorityView {
200    /// Get a human-readable label for the priority.
201    #[must_use]
202    pub fn label(&self) -> &'static str {
203        match self {
204            PriorityView::Urgent => "Urgent",
205            PriorityView::High => "High",
206            PriorityView::Normal => "Normal",
207            PriorityView::Low => "Low",
208        }
209    }
210
211    /// Get the hex color code for the priority.
212    #[must_use]
213    pub fn color(&self) -> &'static str {
214        match self {
215            PriorityView::Urgent => "#DC2626",
216            PriorityView::High => "#EA580C",
217            PriorityView::Normal => "#2563EB",
218            PriorityView::Low => "#6B7280",
219        }
220    }
221
222    /// Get sort order (lower = higher priority).
223    #[must_use]
224    pub fn sort_order(&self) -> u8 {
225        match self {
226            PriorityView::Urgent => 0,
227            PriorityView::High => 1,
228            PriorityView::Normal => 2,
229            PriorityView::Low => 3,
230        }
231    }
232
233    /// Returns all priority levels in order of importance.
234    #[must_use]
235    pub fn all() -> &'static [PriorityView] {
236        &[
237            PriorityView::Urgent,
238            PriorityView::High,
239            PriorityView::Normal,
240            PriorityView::Low,
241        ]
242    }
243}
244
245impl std::fmt::Display for PriorityView {
246    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
247        write!(f, "{}", self.label())
248    }
249}
250
251/// Card workflow state.
252///
253/// These states represent the card's position in a typical workflow.
254/// UI layers may map these to column positions or visual indicators.
255#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
256pub enum CardState {
257    /// Card is waiting to be started.
258    #[default]
259    Todo,
260    /// Card is actively being worked on.
261    InProgress,
262    /// Card is in review/QA.
263    InReview,
264    /// Card is complete.
265    Done,
266    /// Card is archived (hidden from active views).
267    Archived,
268}
269
270impl CardState {
271    /// Get a human-readable label for the state.
272    #[must_use]
273    pub fn label(&self) -> &'static str {
274        match self {
275            CardState::Todo => "To Do",
276            CardState::InProgress => "In Progress",
277            CardState::InReview => "In Review",
278            CardState::Done => "Done",
279            CardState::Archived => "Archived",
280        }
281    }
282}
283
284impl std::fmt::Display for CardState {
285    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
286        write!(f, "{}", self.label())
287    }
288}
289
290/// Board configuration settings.
291#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
292pub struct BoardSettings {
293    /// Whether WIP limits are enforced.
294    pub enable_wip_limits: bool,
295    /// Whether due dates are enabled.
296    pub enable_due_dates: bool,
297    /// Whether checklists (steps) are enabled.
298    pub enable_checklists: bool,
299    /// Default column for new cards.
300    pub default_column_id: Option<String>,
301}
302
303/// A file attachment on a card.
304#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
305pub struct AttachmentView {
306    /// Unique attachment identifier.
307    pub id: String,
308    /// File name.
309    pub name: String,
310    /// MIME type of the file.
311    pub mime_type: String,
312    /// File size in bytes.
313    pub size_bytes: u64,
314}
315
316/// An entry in the card activity log.
317#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
318pub struct ActivityEntry {
319    /// Unique activity entry identifier.
320    pub id: String,
321    /// Type of action (e.g., "created", "moved", "commented").
322    pub action_type: String,
323    /// Four-word identity of the actor.
324    pub actor_id: String,
325    /// Display name of the actor.
326    pub actor_name: String,
327    /// Human-readable description of the action.
328    pub description: String,
329    /// Unix timestamp (ms) when the action occurred.
330    pub timestamp: i64,
331}
332
333#[cfg(test)]
334mod tests {
335    use super::*;
336
337    #[test]
338    fn board_summary_equality() {
339        let b1 = BoardSummary {
340            id: "board-1".to_string(),
341            name: "Sprint Board".to_string(),
342            entity_id: "proj-1".to_string(),
343            column_count: 4,
344            card_count: 12,
345            last_activity: Some(1705500000000),
346        };
347        let b2 = b1.clone();
348        assert_eq!(b1, b2);
349    }
350
351    #[test]
352    fn card_state_default() {
353        let state = CardState::default();
354        assert_eq!(state, CardState::Todo);
355    }
356
357    #[test]
358    fn card_state_display() {
359        assert_eq!(format!("{}", CardState::Todo), "To Do");
360        assert_eq!(format!("{}", CardState::InProgress), "In Progress");
361        assert_eq!(format!("{}", CardState::InReview), "In Review");
362        assert_eq!(format!("{}", CardState::Done), "Done");
363        assert_eq!(format!("{}", CardState::Archived), "Archived");
364    }
365
366    #[test]
367    fn checklist_progress_construction() {
368        let progress = ChecklistProgress {
369            completed: 3,
370            total: 5,
371        };
372        assert_eq!(progress.completed, 3);
373        assert_eq!(progress.total, 5);
374    }
375
376    #[test]
377    fn card_view_with_tags() {
378        let card = CardView {
379            id: "card-1".to_string(),
380            title: "Implement feature".to_string(),
381            description: Some("Add the new feature".to_string()),
382            state: CardState::InProgress,
383            priority: Some(PriorityView::High),
384            assignees: vec!["alice-beta-charlie-delta".to_string()],
385            tags: vec![TagView {
386                id: "tag-1".to_string(),
387                name: "urgent".to_string(),
388                color: "#FF0000".to_string(),
389            }],
390            due_date: Some(1705600000000),
391            checklist_progress: Some(ChecklistProgress {
392                completed: 2,
393                total: 4,
394            }),
395            position: 0,
396            linked_thread_id: None,
397            linked_thread_name: None,
398            sync_state: SyncState::Synced,
399        };
400        assert_eq!(card.tags.len(), 1);
401        assert_eq!(card.tags[0].name, "urgent");
402        assert_eq!(card.priority, Some(PriorityView::High));
403    }
404
405    #[test]
406    fn board_settings_default() {
407        let settings = BoardSettings::default();
408        assert!(!settings.enable_wip_limits);
409        assert!(!settings.enable_due_dates);
410        assert!(!settings.enable_checklists);
411        assert!(settings.default_column_id.is_none());
412    }
413
414    #[test]
415    fn activity_entry_construction() {
416        let entry = ActivityEntry {
417            id: "act-1".to_string(),
418            action_type: "moved".to_string(),
419            actor_id: "user-1".to_string(),
420            actor_name: "Alice".to_string(),
421            description: "Moved card to In Progress".to_string(),
422            timestamp: 1705500000000,
423        };
424        assert_eq!(entry.action_type, "moved");
425    }
426}
427
428/// Swimlane view modes for grouping cards.
429#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
430pub enum SwimlaneMode {
431    /// Standard column view (no swimlanes).
432    #[default]
433    None,
434    /// Group cards by assignee.
435    ByAssignee,
436    /// Group cards by tag.
437    ByTag,
438    /// Group cards by state.
439    ByState,
440}
441
442impl SwimlaneMode {
443    /// Returns a human-readable label for the mode.
444    #[must_use]
445    pub fn label(&self) -> &'static str {
446        match self {
447            SwimlaneMode::None => "Standard",
448            SwimlaneMode::ByAssignee => "By Assignee",
449            SwimlaneMode::ByTag => "By Tag",
450            SwimlaneMode::ByState => "By State",
451        }
452    }
453}
454
455impl std::fmt::Display for SwimlaneMode {
456    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
457        write!(f, "{}", self.label())
458    }
459}
460
461#[cfg(test)]
462mod swimlane_tests {
463    use super::*;
464
465    #[test]
466    fn swimlane_mode_default() {
467        let mode = SwimlaneMode::default();
468        assert_eq!(mode, SwimlaneMode::None);
469    }
470
471    #[test]
472    fn swimlane_mode_labels() {
473        assert_eq!(SwimlaneMode::None.label(), "Standard");
474        assert_eq!(SwimlaneMode::ByAssignee.label(), "By Assignee");
475        assert_eq!(SwimlaneMode::ByTag.label(), "By Tag");
476        assert_eq!(SwimlaneMode::ByState.label(), "By State");
477    }
478
479    #[test]
480    fn swimlane_mode_display() {
481        assert_eq!(format!("{}", SwimlaneMode::None), "Standard");
482        assert_eq!(format!("{}", SwimlaneMode::ByAssignee), "By Assignee");
483        assert_eq!(format!("{}", SwimlaneMode::ByTag), "By Tag");
484        assert_eq!(format!("{}", SwimlaneMode::ByState), "By State");
485    }
486
487    #[test]
488    fn swimlane_mode_equality() {
489        assert_eq!(SwimlaneMode::None, SwimlaneMode::None);
490        assert_ne!(SwimlaneMode::None, SwimlaneMode::ByAssignee);
491        assert_ne!(SwimlaneMode::ByAssignee, SwimlaneMode::ByTag);
492    }
493}
494
495#[cfg(test)]
496mod priority_tests {
497    use super::*;
498
499    #[test]
500    fn priority_default() {
501        let priority = PriorityView::default();
502        assert_eq!(priority, PriorityView::Normal);
503    }
504
505    #[test]
506    fn priority_labels() {
507        assert_eq!(PriorityView::Urgent.label(), "Urgent");
508        assert_eq!(PriorityView::High.label(), "High");
509        assert_eq!(PriorityView::Normal.label(), "Normal");
510        assert_eq!(PriorityView::Low.label(), "Low");
511    }
512
513    #[test]
514    fn priority_colors() {
515        assert_eq!(PriorityView::Urgent.color(), "#DC2626");
516        assert_eq!(PriorityView::High.color(), "#EA580C");
517        assert_eq!(PriorityView::Normal.color(), "#2563EB");
518        assert_eq!(PriorityView::Low.color(), "#6B7280");
519    }
520
521    #[test]
522    fn priority_sort_order() {
523        assert!(PriorityView::Urgent.sort_order() < PriorityView::High.sort_order());
524        assert!(PriorityView::High.sort_order() < PriorityView::Normal.sort_order());
525        assert!(PriorityView::Normal.sort_order() < PriorityView::Low.sort_order());
526    }
527
528    #[test]
529    fn priority_all() {
530        let all = PriorityView::all();
531        assert_eq!(all.len(), 4);
532        assert_eq!(all[0], PriorityView::Urgent);
533        assert_eq!(all[3], PriorityView::Low);
534    }
535
536    #[test]
537    fn priority_display() {
538        assert_eq!(format!("{}", PriorityView::Urgent), "Urgent");
539        assert_eq!(format!("{}", PriorityView::High), "High");
540        assert_eq!(format!("{}", PriorityView::Normal), "Normal");
541        assert_eq!(format!("{}", PriorityView::Low), "Low");
542    }
543}