Implement fork article in frontend

This commit is contained in:
Felix Ableitner 2024-02-14 12:22:24 +01:00
parent 911fadb94b
commit da2c382cb0
17 changed files with 107 additions and 35 deletions

View File

@ -49,7 +49,7 @@ pub(in crate::backend::api) async fn create_article(
instance_id: local_instance.id,
local: true,
};
let article = DbArticle::create(&form, &data.db_connection)?;
let article = DbArticle::create(form, &data.db_connection)?;
let edit_data = EditArticleData {
article_id: article.id,
@ -178,16 +178,16 @@ pub(in crate::backend::api) async fn fork_article(
"http://{}:{}/article/{}",
local_instance.ap_id.inner().domain().unwrap(),
local_instance.ap_id.inner().port().unwrap(),
original_article.title
&fork_form.new_title
))?;
let form = DbArticleForm {
title: original_article.title.clone(),
title: fork_form.new_title,
text: original_article.text.clone(),
ap_id,
instance_id: local_instance.id,
local: true,
};
let article = DbArticle::create(&form, &data.db_connection)?;
let article = DbArticle::create(form, &data.db_connection)?;
// copy edits to new article
// this could also be done in sql

View File

@ -32,20 +32,22 @@ impl DbArticle {
Ok(CollectionId::parse(&format!("{}/edits", self.ap_id))?)
}
pub fn create(form: &DbArticleForm, conn: &Mutex<PgConnection>) -> MyResult<Self> {
pub fn create(mut form: DbArticleForm, conn: &Mutex<PgConnection>) -> MyResult<Self> {
form.title = form.title.replace(' ', "_");
let mut conn = conn.lock().unwrap();
Ok(insert_into(article::table)
.values(form)
.get_result(conn.deref_mut())?)
}
pub fn create_or_update(form: &DbArticleForm, conn: &Mutex<PgConnection>) -> MyResult<Self> {
pub fn create_or_update(mut form: DbArticleForm, conn: &Mutex<PgConnection>) -> MyResult<Self> {
form.title = form.title.replace(' ', "_");
let mut conn = conn.lock().unwrap();
Ok(insert_into(article::table)
.values(form)
.values(&form)
.on_conflict(article::dsl::ap_id)
.do_update()
.set(form)
.set(&form)
.get_result(conn.deref_mut())?)
}

View File

@ -77,7 +77,7 @@ impl Object for DbArticle {
local: false,
instance_id: instance.id,
};
let article = DbArticle::create_or_update(&form, &data.db_connection)?;
let article = DbArticle::create_or_update(form, &data.db_connection)?;
json.edits.dereference(&article, data).await?;

View File

@ -144,7 +144,7 @@ async fn setup(data: &Data<IbisData>) -> Result<(), Error> {
instance_id: instance.id,
local: true,
};
let article = DbArticle::create(&form, &data.db_connection)?;
let article = DbArticle::create(form, &data.db_connection)?;
// also create an article so its included in most recently edited list
submit_article_update(
MAIN_PAGE_DEFAULT_TEXT.to_string(),

View File

@ -192,10 +192,8 @@ pub struct EditArticleData {
#[derive(Deserialize, Serialize)]
pub struct ForkArticleData {
// TODO: could add optional param new_title so there is no problem with title collision
// in case local article with same title exists. however that makes it harder to discover
// variants of same article.
pub article_id: i32,
pub new_title: String,
}
#[derive(Deserialize, Serialize, Debug)]

View File

@ -1,6 +1,7 @@
use crate::common::LocalUserView;
use crate::frontend::api::ApiClient;
use crate::frontend::components::nav::Nav;
use crate::frontend::pages::article::actions::ArticleActions;
use crate::frontend::pages::article::create::CreateArticle;
use crate::frontend::pages::article::edit::EditArticle;
use crate::frontend::pages::article::history::ArticleHistory;
@ -92,8 +93,9 @@ pub fn App() -> impl IntoView {
<Routes>
<Route path="/" view=ReadArticle/>
<Route path="/article/:title" view=ReadArticle/>
<Route path="/article/:title/edit" view=EditArticle/>
<Route path="/article/:title/history" view=ArticleHistory/>
<Route path="/article/:title/edit" view=EditArticle/>
<Route path="/article/:title/actions" view=ArticleActions/>
<Route path="/article/:title/diff/:hash" view=EditDiff/>
<Route path="/article/create" view=CreateArticle/>
<Route path="/article/list" view=ListArticles/>

View File

@ -12,6 +12,7 @@ pub fn ArticleNav(article: Resource<Option<String>, ArticleView>) -> impl IntoVi
<Suspense>
{move || article.get().map(|article| {
let article_link = article_link(&article.article);
let article_link_ = article_link.clone();
view!{
<nav class="inner">
<A href=article_link.clone()>"Read"</A>
@ -22,6 +23,9 @@ pub fn ArticleNav(article: Resource<Option<String>, ArticleView>) -> impl IntoVi
})>
<A href={format!("{article_link}/edit")}>"Edit"</A>
</Show>
<Show when=move || global_state.with(|state| state.my_profile.is_some())>
<A href={format!("{article_link_}/actions")}>"Actions"</A>
</Show>
</nav>
}})}
</Suspense>

View File

@ -1,7 +1,6 @@
use crate::common::utils::extract_domain;
use crate::common::{DbArticle, DbPerson};
use leptos::IntoAttribute;
use leptos::{view, IntoView};
use leptos::*;
pub mod api;
pub mod app;

View File

@ -0,0 +1,73 @@
use crate::common::ForkArticleData;
use crate::frontend::app::GlobalState;
use crate::frontend::article_link;
use crate::frontend::article_title;
use crate::frontend::components::article_nav::ArticleNav;
use crate::frontend::pages::article_resource;
use crate::frontend::DbArticle;
use leptos::*;
use leptos_router::Redirect;
#[component]
pub fn ArticleActions() -> impl IntoView {
let article = article_resource();
let (new_title, set_new_title) = create_signal(String::new());
let (fork_response, set_fork_response) = create_signal(Option::<DbArticle>::None);
let (error, set_error) = create_signal(None::<String>);
let fork_action = create_action(move |(article_id, new_title): &(i32, String)| {
let params = ForkArticleData {
article_id: *article_id,
new_title: new_title.to_string(),
};
async move {
set_error.update(|e| *e = None);
let result = GlobalState::api_client().fork_article(&params).await;
match result {
Ok(res) => set_fork_response.set(Some(res.article)),
Err(err) => {
set_error.update(|e| *e = Some(err.0.to_string()));
}
}
}
});
// TODO: show fork article option (with option to set different title). after forking do redirect
view! {
<ArticleNav article=article/>
<Suspense fallback=|| view! { "Loading..." }> {
move || article.get().map(|article|
view! {
<div class="item-view">
<h1>{article_title(&article.article)}</h1>
{move || {
error
.get()
.map(|err| {
view! { <p style="color:red;">{err}</p> }
})
}}
<Show when=move || !article.article.local>
<input
placeholder="New Title"
on:keyup=move |ev: ev::KeyboardEvent| {
let val = event_target_value(&ev);
set_new_title.update(|v| *v = val);
} />
<button
disabled=move || new_title.get().is_empty()
on:click=move |_| fork_action.dispatch((article.article.id, new_title.get()))>Fork Article</button>
<p>
"You can fork a remote article to the local instance. This is useful if the original
instance is dead, or if there are disagreements how the article should be written."
</p>
</Show>
</div>
})
}
</Suspense>
<Show when=move || fork_response.get().is_some()>
<Redirect path={article_link(&fork_response.get().unwrap())}/>
</Show>
<p>"TODO: add option for admin to delete article etc"</p>
}
}

View File

@ -4,13 +4,10 @@ use crate::frontend::article_title;
use crate::frontend::components::article_nav::ArticleNav;
use crate::frontend::pages::article_resource;
use leptos::*;
use leptos_router::use_params_map;
#[component]
pub fn EditArticle() -> impl IntoView {
let params = use_params_map();
let title = move || params.get().get("title").cloned();
let article = article_resource(title);
let article = article_resource();
let (text, set_text) = create_signal(String::new());
let (summary, set_summary) = create_signal(String::new());

View File

@ -2,13 +2,10 @@ use crate::frontend::components::article_nav::ArticleNav;
use crate::frontend::pages::article_resource;
use crate::frontend::{article_title, user_link};
use leptos::*;
use leptos_router::*;
#[component]
pub fn ArticleHistory() -> impl IntoView {
let params = use_params_map();
let title = move || params.get().get("title").cloned();
let article = article_resource(title);
let article = article_resource();
view! {
<ArticleNav article=article/>

View File

@ -6,7 +6,7 @@ use web_sys::wasm_bindgen::JsCast;
#[component]
pub fn ListArticles() -> impl IntoView {
let (only_local, set_only_local) = create_signal(true);
let (only_local, set_only_local) = create_signal(false);
let articles = create_resource(
move || only_local.get(),
|only_local| async move {

View File

@ -1,3 +1,4 @@
pub mod actions;
pub mod create;
pub mod edit;
pub mod history;

View File

@ -2,14 +2,12 @@ use crate::frontend::article_title;
use crate::frontend::components::article_nav::ArticleNav;
use crate::frontend::pages::article_resource;
use leptos::*;
use leptos_router::*;
use markdown_it::MarkdownIt;
#[component]
pub fn ReadArticle() -> impl IntoView {
let params = use_params_map();
let title = move || params.get().get("title").cloned();
let article = article_resource(title);
let article = article_resource();
view! {
<ArticleNav article=article/>

View File

@ -7,8 +7,7 @@ use leptos_router::*;
#[component]
pub fn EditDiff() -> impl IntoView {
let params = use_params_map();
let title = move || params.get().get("title").cloned();
let article = article_resource(title);
let article = article_resource();
view! {
<ArticleNav article=article/>

View File

@ -1,6 +1,7 @@
use crate::common::{ArticleView, GetArticleData, MAIN_PAGE_NAME};
use crate::frontend::app::GlobalState;
use leptos::{create_resource, Resource};
use leptos::{create_resource, Resource, SignalGet};
use leptos_router::use_params_map;
pub(crate) mod article;
pub(crate) mod diff;
@ -10,9 +11,9 @@ pub(crate) mod register;
pub(crate) mod search;
pub(crate) mod user_profile;
fn article_resource(
title: impl Fn() -> Option<String> + 'static,
) -> Resource<Option<String>, ArticleView> {
fn article_resource() -> Resource<Option<String>, ArticleView> {
let params = use_params_map();
let title = move || params.get().get("title").cloned();
create_resource(title, move |title| async move {
let mut title = title.unwrap_or(MAIN_PAGE_NAME.to_string());
let mut domain = None;

View File

@ -525,6 +525,7 @@ async fn test_fork_article() -> MyResult<()> {
// fork the article to local instance
let fork_form = ForkArticleData {
article_id: resolved_article.id,
new_title: resolved_article.title.clone(),
};
let fork_res = data.beta.fork_article(&fork_form).await?;
let forked_article = fork_res.article;