//! Status-token commands for tasks. //! //! A status token is a typed at-a-glance marker on a task (the first kind is //! `commit`) whose `state` drives the task's colour indicator. These commands let //! the UI list, record/update, and remove tokens; the token id is deterministic in //! `(task_id, kind, reference)`, so `record_task_status_token` doubles as the //! flag-toggle path (re-record the same token with a new `state`/`is_primary`). use serde::Deserialize; use std::sync::Arc; use tauri::State; use tracing::instrument; use goingson_core::{ParseableEnum, StatusToken, StatusTokenId, TaskId, TokenState}; use super::{ApiError, OptionNotFound}; use crate::state::{AppState, DESKTOP_USER_ID}; // Types /// Frontend input for recording (or re-recording) a status token. #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct StatusTokenInput { /// Token kind, e.g. "commit". pub kind: String, /// The kind's value; for a commit, a `@` reference. pub reference: String, /// Colour-mapped state ("Pending" or "Complete"); defaults to Pending. pub state: Option, /// Whether this token resolved the task (at most one primary per task). pub is_primary: Option, } // Commands /// Lists a task's status tokens, in append order. /// /// # Errors /// /// Returns `NOT_FOUND` if the task doesn't exist. /// Returns `DATABASE_ERROR` if the query fails. #[tauri::command] #[instrument(skip_all)] pub async fn list_task_status_tokens( state: State<'_, Arc>, task_id: TaskId, ) -> Result, ApiError> { state .tasks .get_by_id(task_id, DESKTOP_USER_ID) .await? .or_not_found("task", task_id)?; Ok(state.tasks.get_status_tokens_for_task(task_id).await?) } /// Records (or updates) a status token on a task. /// /// Idempotent in `(task_id, kind, reference)`: re-recording the same token updates /// its `state`/`is_primary` in place, which is how the UI toggles pushed/primary. /// /// # Errors /// /// Returns `VALIDATION_ERROR` if `kind` or `reference` is empty. /// Returns `NOT_FOUND` if the task doesn't exist. /// Returns `DATABASE_ERROR` if the write fails. #[tauri::command] #[instrument(skip_all)] pub async fn record_task_status_token( state: State<'_, Arc>, task_id: TaskId, input: StatusTokenInput, ) -> Result { let kind = input.kind.trim(); let reference = input.reference.trim(); if kind.is_empty() { return Err(ApiError::validation("kind", "Token kind is required")); } if reference.is_empty() { return Err(ApiError::validation( "reference", "Token reference is required", )); } let state_val = input .state .as_deref() .map(TokenState::from_str_or_default) .unwrap_or_default(); let is_primary = input.is_primary.unwrap_or(false); let token = state .tasks .record_status_token( task_id, DESKTOP_USER_ID, kind, reference, state_val, is_primary, ) .await? .or_not_found("task", task_id)?; Ok(token) } /// Deletes a status token by id. /// /// # Errors /// /// Returns `DATABASE_ERROR` if the delete fails. #[tauri::command] #[instrument(skip_all)] pub async fn delete_task_status_token( state: State<'_, Arc>, token_id: StatusTokenId, ) -> Result { Ok(state .tasks .delete_status_token(token_id, DESKTOP_USER_ID) .await?) }