| 1 |
|
| 2 |
|
| 3 |
|
| 4 |
|
| 5 |
|
| 6 |
|
| 7 |
|
| 8 |
|
| 9 |
|
| 10 |
|
| 11 |
|
| 12 |
|
| 13 |
|
| 14 |
|
| 15 |
use chrono::{DateTime, Utc}; |
| 16 |
use serde::{Deserialize, Serialize}; |
| 17 |
use std::sync::Arc; |
| 18 |
use tauri::State; |
| 19 |
use tracing::instrument; |
| 20 |
use uuid::Uuid; |
| 21 |
|
| 22 |
use goingson_core::{search_parser, ProjectId, SearchQuery, SearchResultItem, SearchResultType}; |
| 23 |
|
| 24 |
use crate::state::{AppState, DESKTOP_USER_ID}; |
| 25 |
use super::ApiError; |
| 26 |
|
| 27 |
|
| 28 |
|
| 29 |
#[derive(Debug, Deserialize)] |
| 30 |
#[serde(rename_all = "camelCase")] |
| 31 |
pub struct SearchInput { |
| 32 |
pub query: String, |
| 33 |
#[serde(rename = "type")] |
| 34 |
pub result_type: Option<String>, |
| 35 |
pub project_id: Option<ProjectId>, |
| 36 |
pub date_from: Option<DateTime<Utc>>, |
| 37 |
pub date_to: Option<DateTime<Utc>>, |
| 38 |
pub limit: Option<i64>, |
| 39 |
pub offset: Option<i64>, |
| 40 |
} |
| 41 |
|
| 42 |
#[derive(Debug, Serialize)] |
| 43 |
#[serde(rename_all = "camelCase")] |
| 44 |
pub struct SearchResultResponse { |
| 45 |
pub id: Uuid, |
| 46 |
pub result_type: String, |
| 47 |
pub title: String, |
| 48 |
pub snippet: Option<String>, |
| 49 |
pub project_id: Option<ProjectId>, |
| 50 |
pub project_name: Option<String>, |
| 51 |
pub rank: f64, |
| 52 |
} |
| 53 |
|
| 54 |
impl From<SearchResultItem> for SearchResultResponse { |
| 55 |
fn from(item: SearchResultItem) -> Self { |
| 56 |
SearchResultResponse { |
| 57 |
id: item.id, |
| 58 |
result_type: match item.result_type { |
| 59 |
SearchResultType::Task => "task".to_string(), |
| 60 |
SearchResultType::Email => "email".to_string(), |
| 61 |
SearchResultType::Project => "project".to_string(), |
| 62 |
SearchResultType::Event => "event".to_string(), |
| 63 |
SearchResultType::Contact => "contact".to_string(), |
| 64 |
}, |
| 65 |
title: item.title, |
| 66 |
snippet: item.snippet, |
| 67 |
project_id: item.project_id, |
| 68 |
project_name: item.project_name, |
| 69 |
rank: item.rank, |
| 70 |
} |
| 71 |
} |
| 72 |
} |
| 73 |
|
| 74 |
#[derive(Debug, Serialize)] |
| 75 |
#[serde(rename_all = "camelCase")] |
| 76 |
pub struct SearchResultsResponse { |
| 77 |
pub results: Vec<SearchResultResponse>, |
| 78 |
pub query: String, |
| 79 |
pub total: usize, |
| 80 |
|
| 81 |
pub active_filters: Vec<String>, |
| 82 |
} |
| 83 |
|
| 84 |
|
| 85 |
|
| 86 |
|
| 87 |
|
| 88 |
|
| 89 |
|
| 90 |
|
| 91 |
|
| 92 |
|
| 93 |
|
| 94 |
|
| 95 |
|
| 96 |
|
| 97 |
|
| 98 |
|
| 99 |
|
| 100 |
|
| 101 |
|
| 102 |
|
| 103 |
#[tauri::command] |
| 104 |
#[instrument(skip_all)] |
| 105 |
pub async fn search(state: State<'_, Arc<AppState>>, input: SearchInput) -> Result<SearchResultsResponse, ApiError> { |
| 106 |
|
| 107 |
let parsed = search_parser::parse_search_query(&input.query); |
| 108 |
|
| 109 |
|
| 110 |
let mut active_filters: Vec<String> = Vec::new(); |
| 111 |
for f in &parsed.is_filters { |
| 112 |
let label = match f { |
| 113 |
search_parser::IsFilter::Overdue => "is:overdue", |
| 114 |
search_parser::IsFilter::Today => "is:today", |
| 115 |
search_parser::IsFilter::Tomorrow => "is:tomorrow", |
| 116 |
search_parser::IsFilter::ThisWeek => "is:thisweek", |
| 117 |
search_parser::IsFilter::Snoozed => "is:snoozed", |
| 118 |
search_parser::IsFilter::Pending => "is:pending", |
| 119 |
search_parser::IsFilter::Started => "is:started", |
| 120 |
search_parser::IsFilter::Completed => "is:completed", |
| 121 |
search_parser::IsFilter::Waiting => "is:waiting", |
| 122 |
}; |
| 123 |
active_filters.push(label.to_string()); |
| 124 |
} |
| 125 |
if let Some(ref p) = parsed.priority { |
| 126 |
let label = match p { |
| 127 |
goingson_core::Priority::High => "priority:high", |
| 128 |
goingson_core::Priority::Medium => "priority:medium", |
| 129 |
goingson_core::Priority::Low => "priority:low", |
| 130 |
}; |
| 131 |
active_filters.push(label.to_string()); |
| 132 |
} |
| 133 |
if let Some(ref name) = parsed.project_name { |
| 134 |
active_filters.push(format!("in:{}", name)); |
| 135 |
} |
| 136 |
for t in &parsed.result_types { |
| 137 |
let label = match t { |
| 138 |
SearchResultType::Task => "type:task", |
| 139 |
SearchResultType::Email => "type:email", |
| 140 |
SearchResultType::Event => "type:event", |
| 141 |
SearchResultType::Project => "type:project", |
| 142 |
SearchResultType::Contact => "type:contact", |
| 143 |
}; |
| 144 |
active_filters.push(label.to_string()); |
| 145 |
} |
| 146 |
for tag in &parsed.tags_include { |
| 147 |
active_filters.push(format!("tag:{}", tag)); |
| 148 |
} |
| 149 |
for tag in &parsed.tags_exclude { |
| 150 |
active_filters.push(format!("-tag:{}", tag)); |
| 151 |
} |
| 152 |
|
| 153 |
|
| 154 |
let explicit_types = input.result_type.as_ref().map(|t| { |
| 155 |
t.split(',') |
| 156 |
.filter_map(|s| match s.trim().to_lowercase().as_str() { |
| 157 |
"task" => Some(SearchResultType::Task), |
| 158 |
"email" => Some(SearchResultType::Email), |
| 159 |
"project" => Some(SearchResultType::Project), |
| 160 |
"event" => Some(SearchResultType::Event), |
| 161 |
"contact" => Some(SearchResultType::Contact), |
| 162 |
_ => None, |
| 163 |
}) |
| 164 |
.collect::<Vec<_>>() |
| 165 |
}).filter(|v| !v.is_empty()); |
| 166 |
|
| 167 |
|
| 168 |
let types = if !parsed.result_types.is_empty() { |
| 169 |
Some(parsed.result_types.clone()) |
| 170 |
} else { |
| 171 |
explicit_types |
| 172 |
}; |
| 173 |
|
| 174 |
|
| 175 |
let query = SearchQuery { |
| 176 |
query: parsed.text.clone(), |
| 177 |
types, |
| 178 |
project_id: input.project_id, |
| 179 |
project_name: parsed.project_name.clone(), |
| 180 |
date_from: parsed.date_from.or(input.date_from), |
| 181 |
date_to: parsed.date_to.or(input.date_to), |
| 182 |
limit: input.limit.or(Some(50)), |
| 183 |
offset: input.offset, |
| 184 |
is_filters: parsed.is_filters.clone(), |
| 185 |
priority: parsed.priority.clone(), |
| 186 |
tags_include: parsed.tags_include.clone(), |
| 187 |
tags_exclude: parsed.tags_exclude.clone(), |
| 188 |
}; |
| 189 |
|
| 190 |
let (results, total) = state.search.search(DESKTOP_USER_ID, query).await?; |
| 191 |
|
| 192 |
Ok(SearchResultsResponse { |
| 193 |
results: results.into_iter().map(SearchResultResponse::from).collect(), |
| 194 |
query: input.query, |
| 195 |
total, |
| 196 |
active_filters, |
| 197 |
}) |
| 198 |
} |
| 199 |
|