Skip to main content

max / goingson

3.4 KB · 100 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 crate::state::{AppState, DESKTOP_USER_ID};
17 use super::{ApiError, OptionNotFound};
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(state: State<'_, Arc<AppState>>, task_id: TaskId) -> Result<Vec<StatusToken>, ApiError> {
46 state.tasks
47 .get_by_id(task_id, DESKTOP_USER_ID)
48 .await?
49 .or_not_found("task", task_id)?;
50
51 Ok(state.tasks.get_status_tokens_for_task(task_id).await?)
52 }
53
54 /// Records (or updates) a status token on a task.
55 ///
56 /// Idempotent in `(task_id, kind, reference)`: re-recording the same token updates
57 /// its `state`/`is_primary` in place, which is how the UI toggles pushed/primary.
58 ///
59 /// # Errors
60 ///
61 /// Returns `VALIDATION_ERROR` if `kind` or `reference` is empty.
62 /// Returns `NOT_FOUND` if the task doesn't exist.
63 /// Returns `DATABASE_ERROR` if the write fails.
64 #[tauri::command]
65 #[instrument(skip_all)]
66 pub async fn record_task_status_token(state: State<'_, Arc<AppState>>, task_id: TaskId, input: StatusTokenInput) -> Result<StatusToken, ApiError> {
67 let kind = input.kind.trim();
68 let reference = input.reference.trim();
69 if kind.is_empty() {
70 return Err(ApiError::validation("kind", "Token kind is required"));
71 }
72 if reference.is_empty() {
73 return Err(ApiError::validation("reference", "Token reference is required"));
74 }
75 let state_val = input
76 .state
77 .as_deref()
78 .map(TokenState::from_str_or_default)
79 .unwrap_or_default();
80 let is_primary = input.is_primary.unwrap_or(false);
81
82 let token = state.tasks
83 .record_status_token(task_id, DESKTOP_USER_ID, kind, reference, state_val, is_primary)
84 .await?
85 .or_not_found("task", task_id)?;
86
87 Ok(token)
88 }
89
90 /// Deletes a status token by id.
91 ///
92 /// # Errors
93 ///
94 /// Returns `DATABASE_ERROR` if the delete fails.
95 #[tauri::command]
96 #[instrument(skip_all)]
97 pub async fn delete_task_status_token(state: State<'_, Arc<AppState>>, token_id: StatusTokenId) -> Result<bool, ApiError> {
98 Ok(state.tasks.delete_status_token(token_id, DESKTOP_USER_ID).await?)
99 }
100