basic search input and results page

This commit is contained in:
Felix Ableitner 2024-01-29 17:04:22 +01:00
parent 52121fae66
commit baf1fb7505
12 changed files with 89 additions and 20 deletions

View File

@ -6,12 +6,12 @@ use crate::backend::error::MyResult;
use crate::backend::federation::activities::create_article::CreateArticle; use crate::backend::federation::activities::create_article::CreateArticle;
use crate::backend::federation::activities::submit_article_update; use crate::backend::federation::activities::submit_article_update;
use crate::backend::utils::generate_article_version; use crate::backend::utils::generate_article_version;
use crate::common::DbInstance;
use crate::common::GetArticleData; use crate::common::GetArticleData;
use crate::common::LocalUserView; use crate::common::LocalUserView;
use crate::common::{ApiConflict, ResolveObject}; use crate::common::{ApiConflict, ResolveObject};
use crate::common::{ArticleView, DbArticle, DbEdit}; use crate::common::{ArticleView, DbArticle, DbEdit};
use crate::common::{CreateArticleData, EditArticleData, EditVersion, ForkArticleData}; use crate::common::{CreateArticleData, EditArticleData, EditVersion, ForkArticleData};
use crate::common::{DbInstance, SearchArticleData};
use activitypub_federation::config::Data; use activitypub_federation::config::Data;
use activitypub_federation::fetch::object_id::ObjectId; use activitypub_federation::fetch::object_id::ObjectId;
use anyhow::anyhow; use anyhow::anyhow;
@ -203,3 +203,13 @@ pub(super) async fn resolve_article(
latest_version, latest_version,
})) }))
} }
/// Search articles for matching title or body text.
#[debug_handler]
pub(super) async fn search_article(
Query(query): Query<SearchArticleData>,
data: Data<MyDataHandle>,
) -> MyResult<Json<Vec<DbArticle>>> {
let article = DbArticle::search(&query.query, &data.db_connection)?;
Ok(Json(article))
}

View File

@ -1,4 +1,4 @@
use crate::backend::api::article::{create_article, resolve_article}; use crate::backend::api::article::{create_article, resolve_article, search_article};
use crate::backend::api::article::{edit_article, fork_article, get_article}; use crate::backend::api::article::{edit_article, fork_article, get_article};
use crate::backend::api::instance::get_local_instance; use crate::backend::api::instance::get_local_instance;
use crate::backend::api::instance::{follow_instance, resolve_instance}; use crate::backend::api::instance::{follow_instance, resolve_instance};
@ -9,10 +9,9 @@ use crate::backend::api::user::{my_profile, AUTH_COOKIE};
use crate::backend::database::conflict::DbConflict; use crate::backend::database::conflict::DbConflict;
use crate::backend::database::MyDataHandle; use crate::backend::database::MyDataHandle;
use crate::backend::error::MyResult; use crate::backend::error::MyResult;
use crate::common::ApiConflict;
use crate::common::LocalUserView; use crate::common::LocalUserView;
use crate::common::{ApiConflict, DbArticle, SearchArticleData};
use activitypub_federation::config::Data; use activitypub_federation::config::Data;
use axum::extract::Query;
use axum::routing::{get, post}; use axum::routing::{get, post};
use axum::{ use axum::{
http::Request, http::Request,
@ -85,13 +84,3 @@ async fn edit_conflicts(
.collect(); .collect();
Ok(Json(conflicts)) Ok(Json(conflicts))
} }
/// Search articles for matching title or body text.
#[debug_handler]
async fn search_article(
Query(query): Query<SearchArticleData>,
data: Data<MyDataHandle>,
) -> MyResult<Json<Vec<DbArticle>>> {
let article = DbArticle::search(&query.query, &data.db_connection)?;
Ok(Json(article))
}

View File

@ -131,6 +131,7 @@ impl DbArticle {
.replace('%', "\\%") .replace('%', "\\%")
.replace('_', "\\_") .replace('_', "\\_")
.replace(' ', "%"); .replace(' ', "%");
let replaced = format!("%{replaced}%");
Ok(article::table Ok(article::table
.filter( .filter(
article::dsl::title article::dsl::title

View File

@ -44,6 +44,12 @@ pub struct DbArticle {
pub local: bool, pub local: bool,
} }
impl DbArticle {
pub fn title(&self) -> String {
self.title.replace('_', " ")
}
}
/// Represents a single change to the article. /// Represents a single change to the article.
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "ssr", derive(Queryable, Selectable))] #[cfg_attr(feature = "ssr", derive(Queryable, Selectable))]

View File

@ -7,6 +7,7 @@ use crate::frontend::pages::article::read::ReadArticle;
use crate::frontend::pages::diff::EditDiff; use crate::frontend::pages::diff::EditDiff;
use crate::frontend::pages::login::Login; use crate::frontend::pages::login::Login;
use crate::frontend::pages::register::Register; use crate::frontend::pages::register::Register;
use crate::frontend::pages::search::Search;
use crate::frontend::pages::Page; use crate::frontend::pages::Page;
use leptos::{ use leptos::{
component, create_local_resource, create_rw_signal, expect_context, provide_context, component, create_local_resource, create_rw_signal, expect_context, provide_context,
@ -74,6 +75,7 @@ pub fn App() -> impl IntoView {
<Route path="/article/:title/diff/:hash" view=EditDiff/> <Route path="/article/:title/diff/:hash" view=EditDiff/>
<Route path={Page::Login.path()} view=Login/> <Route path={Page::Login.path()} view=Login/>
<Route path={Page::Register.path()} view=Register/> <Route path={Page::Register.path()} view=Register/>
<Route path="/search" view=Search/>
</Routes> </Routes>
</main> </main>
</Router> </Router>

View File

@ -7,7 +7,7 @@ use leptos_router::*;
pub fn ArticleNav(article: Resource<String, ArticleView>) -> impl IntoView { pub fn ArticleNav(article: Resource<String, ArticleView>) -> impl IntoView {
let global_state = use_context::<RwSignal<GlobalState>>().unwrap(); let global_state = use_context::<RwSignal<GlobalState>>().unwrap();
view! { view! {
<Suspense fallback=|| view! { "Loading..." }> <Suspense>
{move || article.get().map(|article| { {move || article.get().map(|article| {
let title = article.article.title; let title = article.article.title;
view!{ view!{

View File

@ -12,11 +12,28 @@ pub fn Nav() -> impl IntoView {
.get_untracked() .get_untracked()
.update_my_profile(); .update_my_profile();
}); });
let (search_query, set_search_query) = create_signal(String::new());
view! { view! {
<nav class="inner"> <nav class="inner">
<li> <li>
<A href="/">"Main Page"</A> <A href="/">"Main Page"</A>
</li> </li>
<li>
<form on:submit=move |ev| {
ev.prevent_default();
let navigate = leptos_router::use_navigate();
let query = search_query.get();
navigate(&format!("/search?query={query}"), Default::default());
}>
<input type="text" placeholder="Search"
prop:value=search_query
on:keyup=move |ev: ev::KeyboardEvent| {
let val = event_target_value(&ev);
set_search_query.update(|v| *v = val);
} />
<button>Go</button>
</form>
</li>
<Show <Show
when=move || global_state.with(|state| state.my_profile.is_none()) when=move || global_state.with(|state| state.my_profile.is_none())
fallback=move || { fallback=move || {

View File

@ -61,7 +61,7 @@ pub fn EditArticle() -> impl IntoView {
set_text.set(article.article.text.clone()); set_text.set(article.article.text.clone());
view! { view! {
<div class="item-view"> <div class="item-view">
<h1>{article.article.title.replace('_', " ")}</h1> <h1>{article.article.title()}</h1>
<textarea on:keyup=move |ev| { <textarea on:keyup=move |ev| {
let val = event_target_value(&ev); let val = event_target_value(&ev);
set_text.update(|p| *p = val); set_text.update(|p| *p = val);

View File

@ -13,13 +13,12 @@ pub fn ArticleHistory() -> impl IntoView {
<ArticleNav article=article/> <ArticleNav article=article/>
<Suspense fallback=|| view! { "Loading..." }> { <Suspense fallback=|| view! { "Loading..." }> {
move || article.get().map(|article| { move || article.get().map(|article| {
let title = article.article.title;
view! { view! {
<div class="item-view"> <div class="item-view">
<h1>{title.replace('_', " ")}</h1> <h1>{article.article.title()}</h1>
{ {
article.edits.into_iter().rev().map(|edit| { article.edits.into_iter().rev().map(|edit| {
let path = format!("/article/{title}/diff/{}", edit.hash.0); let path = format!("/article/{}/diff/{}", article.article.title, edit.hash.0);
// TODO: need to return username from backend and show it // TODO: need to return username from backend and show it
let label = format!("{} ({})", edit.summary, edit.created.to_rfc2822()); let label = format!("{} ({})", edit.summary, edit.created.to_rfc2822());
view! {<li><a href={path}>{label}</a></li> } view! {<li><a href={path}>{label}</a></li> }

View File

@ -21,7 +21,7 @@ pub fn ReadArticle() -> impl IntoView {
move || article.get().map(|article| move || article.get().map(|article|
view! { view! {
<div class="item-view"> <div class="item-view">
<h1>{article.article.title.replace('_', " ")}</h1> <h1>{article.article.title()}</h1>
<div inner_html={parser.parse(&article.article.text).render()}/> <div inner_html={parser.parse(&article.article.text).render()}/>
</div> </div>
}) })

View File

@ -6,6 +6,7 @@ pub(crate) mod article;
pub(crate) mod diff; pub(crate) mod diff;
pub mod login; pub mod login;
pub mod register; pub mod register;
pub(crate) mod search;
#[derive(Debug, Clone, Copy, Default)] #[derive(Debug, Clone, Copy, Default)]
pub enum Page { pub enum Page {

View File

@ -0,0 +1,44 @@
use crate::common::SearchArticleData;
use crate::frontend::app::GlobalState;
use leptos::*;
use leptos_router::use_query_map;
#[component]
pub fn Search() -> impl IntoView {
let params = use_query_map();
let query = params.get_untracked().get("query").cloned().unwrap();
let query_ = query.clone();
let search_results = create_resource(
move || query_.clone(),
move |query| async move {
GlobalState::api_client()
.search(&SearchArticleData { query })
.await
.unwrap()
},
);
view! {
<h1>"Search results for "{query}</h1>
<Suspense fallback=|| view! { "Loading..." }> {
move || search_results.get().map(|search_results| {
let is_empty = search_results.is_empty();
view! {
<Show when=move || !is_empty
fallback=|| view! { <p>No results found</p> }>
<ul>
{
search_results
.iter()
.map(|a| view! { <li>
<a href={format!("/article/{}", a.title)}>{a.title()}</a>
</li>})
.collect::<Vec<_>>()
}
</ul>
</Show>
}})
}
</Suspense>
}
}