Skip to main content

max / goingson

3.6 KB · 125 lines History Blame Raw
1 //! Status-token commands for tasks.
2 //!
3 //! A status token is a typed at-a-glance marker on a task (the first kind is
4 //! `commit`) whose `state` drives the task's colour indicator. These commands let
5 //! the UI list, record/update, and remove tokens; the token id is deterministic in
6 //! `(task_id, kind, reference)`, so `record_task_status_token` doubles as the
7 //! flag-toggle path (re-record the same token with a new `state`/`is_primary`).
8
9 use serde::Deserialize;
10 use std::sync::Arc;
11 use tauri::State;
12 use tracing::instrument;
13
14 use goingson_core::{ParseableEnum, StatusToken, StatusTokenId, TaskId, TokenState};
15
16 use super::{ApiError, OptionNotFound};
17 use crate::state::{AppState, DESKTOP_USER_ID};
18
19 // Types
20
21 /// Frontend input for recording (or re-recording) a status token.
22 #[derive(Debug, Deserialize)]
23 #[serde(rename_all = "camelCase")]
24 pub struct StatusTokenInput {
25 /// Token kind, e.g. "commit".
26 pub kind: String,
27 /// The kind's value; for a commit, a `<repo>@<shortsha>` reference.
28 pub reference: String,
29 /// Colour-mapped state ("Pending" or "Complete"); defaults to Pending.
30 pub state: Option<String>,
31 /// Whether this token resolved the task (at most one primary per task).
32 pub is_primary: Option<bool>,
33 }
34
35 // Commands
36
37 /// Lists a task's status tokens, in append order.
38 ///
39 /// # Errors
40 ///
41 /// Returns `NOT_FOUND` if the task doesn't exist.
42 /// Returns `DATABASE_ERROR` if the query fails.
43 #[tauri::command]
44 #[instrument(skip_all)]
45 pub async fn list_task_status_tokens(
46 state: State<'_, Arc<AppState>>,
47 task_id: TaskId,
48 ) -> Result<Vec<StatusToken>, ApiError> {
49 state
50 .tasks
51 .get_by_id(task_id, DESKTOP_USER_ID)
52 .await?
53 .or_not_found("task", task_id)?;
54
55 Ok(state.tasks.get_status_tokens_for_task(task_id).await?)
56 }
57
58 /// Records (or updates) a status token on a task.
59 ///
60 /// Idempotent in `(task_id, kind, reference)`: re-recording the same token updates
61 /// its `state`/`is_primary` in place, which is how the UI toggles pushed/primary.
62 ///
63 /// # Errors
64 ///
65 /// Returns `VALIDATION_ERROR` if `kind` or `reference` is empty.
66 /// Returns `NOT_FOUND` if the task doesn't exist.
67 /// Returns `DATABASE_ERROR` if the write fails.
68 #[tauri::command]
69 #[instrument(skip_all)]
70 pub async fn record_task_status_token(
71 state: State<'_, Arc<AppState>>,
72 task_id: TaskId,
73 input: StatusTokenInput,
74 ) -> Result<StatusToken, ApiError> {
75 let kind = input.kind.trim();
76 let reference = input.reference.trim();
77 if kind.is_empty() {
78 return Err(ApiError::validation("kind", "Token kind is required"));
79 }
80 if reference.is_empty() {
81 return Err(ApiError::validation(
82 "reference",
83 "Token reference is required",
84 ));
85 }
86 let state_val = input
87 .state
88 .as_deref()
89 .map(TokenState::from_str_or_default)
90 .unwrap_or_default();
91 let is_primary = input.is_primary.unwrap_or(false);
92
93 let token = state
94 .tasks
95 .record_status_token(
96 task_id,
97 DESKTOP_USER_ID,
98 kind,
99 reference,
100 state_val,
101 is_primary,
102 )
103 .await?
104 .or_not_found("task", task_id)?;
105
106 Ok(token)
107 }
108
109 /// Deletes a status token by id.
110 ///
111 /// # Errors
112 ///
113 /// Returns `DATABASE_ERROR` if the delete fails.
114 #[tauri::command]
115 #[instrument(skip_all)]
116 pub async fn delete_task_status_token(
117 state: State<'_, Arc<AppState>>,
118 token_id: StatusTokenId,
119 ) -> Result<bool, ApiError> {
120 Ok(state
121 .tasks
122 .delete_status_token(token_id, DESKTOP_USER_ID)
123 .await?)
124 }
125