mirror of
https://github.com/Nutomic/ibis.git
synced 2024-11-28 23:11:09 +00:00
Implement fork article in frontend
This commit is contained in:
parent
911fadb94b
commit
da2c382cb0
17 changed files with 107 additions and 35 deletions
|
@ -49,7 +49,7 @@ pub(in crate::backend::api) async fn create_article(
|
||||||
instance_id: local_instance.id,
|
instance_id: local_instance.id,
|
||||||
local: true,
|
local: true,
|
||||||
};
|
};
|
||||||
let article = DbArticle::create(&form, &data.db_connection)?;
|
let article = DbArticle::create(form, &data.db_connection)?;
|
||||||
|
|
||||||
let edit_data = EditArticleData {
|
let edit_data = EditArticleData {
|
||||||
article_id: article.id,
|
article_id: article.id,
|
||||||
|
@ -178,16 +178,16 @@ pub(in crate::backend::api) async fn fork_article(
|
||||||
"http://{}:{}/article/{}",
|
"http://{}:{}/article/{}",
|
||||||
local_instance.ap_id.inner().domain().unwrap(),
|
local_instance.ap_id.inner().domain().unwrap(),
|
||||||
local_instance.ap_id.inner().port().unwrap(),
|
local_instance.ap_id.inner().port().unwrap(),
|
||||||
original_article.title
|
&fork_form.new_title
|
||||||
))?;
|
))?;
|
||||||
let form = DbArticleForm {
|
let form = DbArticleForm {
|
||||||
title: original_article.title.clone(),
|
title: fork_form.new_title,
|
||||||
text: original_article.text.clone(),
|
text: original_article.text.clone(),
|
||||||
ap_id,
|
ap_id,
|
||||||
instance_id: local_instance.id,
|
instance_id: local_instance.id,
|
||||||
local: true,
|
local: true,
|
||||||
};
|
};
|
||||||
let article = DbArticle::create(&form, &data.db_connection)?;
|
let article = DbArticle::create(form, &data.db_connection)?;
|
||||||
|
|
||||||
// copy edits to new article
|
// copy edits to new article
|
||||||
// this could also be done in sql
|
// this could also be done in sql
|
||||||
|
|
|
@ -32,20 +32,22 @@ impl DbArticle {
|
||||||
Ok(CollectionId::parse(&format!("{}/edits", self.ap_id))?)
|
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();
|
let mut conn = conn.lock().unwrap();
|
||||||
Ok(insert_into(article::table)
|
Ok(insert_into(article::table)
|
||||||
.values(form)
|
.values(form)
|
||||||
.get_result(conn.deref_mut())?)
|
.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();
|
let mut conn = conn.lock().unwrap();
|
||||||
Ok(insert_into(article::table)
|
Ok(insert_into(article::table)
|
||||||
.values(form)
|
.values(&form)
|
||||||
.on_conflict(article::dsl::ap_id)
|
.on_conflict(article::dsl::ap_id)
|
||||||
.do_update()
|
.do_update()
|
||||||
.set(form)
|
.set(&form)
|
||||||
.get_result(conn.deref_mut())?)
|
.get_result(conn.deref_mut())?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -77,7 +77,7 @@ impl Object for DbArticle {
|
||||||
local: false,
|
local: false,
|
||||||
instance_id: instance.id,
|
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?;
|
json.edits.dereference(&article, data).await?;
|
||||||
|
|
||||||
|
|
|
@ -144,7 +144,7 @@ async fn setup(data: &Data<IbisData>) -> Result<(), Error> {
|
||||||
instance_id: instance.id,
|
instance_id: instance.id,
|
||||||
local: true,
|
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
|
// also create an article so its included in most recently edited list
|
||||||
submit_article_update(
|
submit_article_update(
|
||||||
MAIN_PAGE_DEFAULT_TEXT.to_string(),
|
MAIN_PAGE_DEFAULT_TEXT.to_string(),
|
||||||
|
|
|
@ -192,10 +192,8 @@ pub struct EditArticleData {
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize)]
|
#[derive(Deserialize, Serialize)]
|
||||||
pub struct ForkArticleData {
|
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 article_id: i32,
|
||||||
|
pub new_title: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Debug)]
|
#[derive(Deserialize, Serialize, Debug)]
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
use crate::common::LocalUserView;
|
use crate::common::LocalUserView;
|
||||||
use crate::frontend::api::ApiClient;
|
use crate::frontend::api::ApiClient;
|
||||||
use crate::frontend::components::nav::Nav;
|
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::create::CreateArticle;
|
||||||
use crate::frontend::pages::article::edit::EditArticle;
|
use crate::frontend::pages::article::edit::EditArticle;
|
||||||
use crate::frontend::pages::article::history::ArticleHistory;
|
use crate::frontend::pages::article::history::ArticleHistory;
|
||||||
|
@ -92,8 +93,9 @@ pub fn App() -> impl IntoView {
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" view=ReadArticle/>
|
<Route path="/" view=ReadArticle/>
|
||||||
<Route path="/article/:title" 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/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/:title/diff/:hash" view=EditDiff/>
|
||||||
<Route path="/article/create" view=CreateArticle/>
|
<Route path="/article/create" view=CreateArticle/>
|
||||||
<Route path="/article/list" view=ListArticles/>
|
<Route path="/article/list" view=ListArticles/>
|
||||||
|
|
|
@ -12,6 +12,7 @@ pub fn ArticleNav(article: Resource<Option<String>, ArticleView>) -> impl IntoVi
|
||||||
<Suspense>
|
<Suspense>
|
||||||
{move || article.get().map(|article| {
|
{move || article.get().map(|article| {
|
||||||
let article_link = article_link(&article.article);
|
let article_link = article_link(&article.article);
|
||||||
|
let article_link_ = article_link.clone();
|
||||||
view!{
|
view!{
|
||||||
<nav class="inner">
|
<nav class="inner">
|
||||||
<A href=article_link.clone()>"Read"</A>
|
<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>
|
<A href={format!("{article_link}/edit")}>"Edit"</A>
|
||||||
</Show>
|
</Show>
|
||||||
|
<Show when=move || global_state.with(|state| state.my_profile.is_some())>
|
||||||
|
<A href={format!("{article_link_}/actions")}>"Actions"</A>
|
||||||
|
</Show>
|
||||||
</nav>
|
</nav>
|
||||||
}})}
|
}})}
|
||||||
</Suspense>
|
</Suspense>
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
use crate::common::utils::extract_domain;
|
use crate::common::utils::extract_domain;
|
||||||
use crate::common::{DbArticle, DbPerson};
|
use crate::common::{DbArticle, DbPerson};
|
||||||
use leptos::IntoAttribute;
|
use leptos::*;
|
||||||
use leptos::{view, IntoView};
|
|
||||||
|
|
||||||
pub mod api;
|
pub mod api;
|
||||||
pub mod app;
|
pub mod app;
|
||||||
|
|
73
src/frontend/pages/article/actions.rs
Normal file
73
src/frontend/pages/article/actions.rs
Normal 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(¶ms).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>
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,13 +4,10 @@ use crate::frontend::article_title;
|
||||||
use crate::frontend::components::article_nav::ArticleNav;
|
use crate::frontend::components::article_nav::ArticleNav;
|
||||||
use crate::frontend::pages::article_resource;
|
use crate::frontend::pages::article_resource;
|
||||||
use leptos::*;
|
use leptos::*;
|
||||||
use leptos_router::use_params_map;
|
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn EditArticle() -> impl IntoView {
|
pub fn EditArticle() -> impl IntoView {
|
||||||
let params = use_params_map();
|
let article = article_resource();
|
||||||
let title = move || params.get().get("title").cloned();
|
|
||||||
let article = article_resource(title);
|
|
||||||
|
|
||||||
let (text, set_text) = create_signal(String::new());
|
let (text, set_text) = create_signal(String::new());
|
||||||
let (summary, set_summary) = create_signal(String::new());
|
let (summary, set_summary) = create_signal(String::new());
|
||||||
|
|
|
@ -2,13 +2,10 @@ use crate::frontend::components::article_nav::ArticleNav;
|
||||||
use crate::frontend::pages::article_resource;
|
use crate::frontend::pages::article_resource;
|
||||||
use crate::frontend::{article_title, user_link};
|
use crate::frontend::{article_title, user_link};
|
||||||
use leptos::*;
|
use leptos::*;
|
||||||
use leptos_router::*;
|
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn ArticleHistory() -> impl IntoView {
|
pub fn ArticleHistory() -> impl IntoView {
|
||||||
let params = use_params_map();
|
let article = article_resource();
|
||||||
let title = move || params.get().get("title").cloned();
|
|
||||||
let article = article_resource(title);
|
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
<ArticleNav article=article/>
|
<ArticleNav article=article/>
|
||||||
|
|
|
@ -6,7 +6,7 @@ use web_sys::wasm_bindgen::JsCast;
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn ListArticles() -> impl IntoView {
|
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(
|
let articles = create_resource(
|
||||||
move || only_local.get(),
|
move || only_local.get(),
|
||||||
|only_local| async move {
|
|only_local| async move {
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
pub mod actions;
|
||||||
pub mod create;
|
pub mod create;
|
||||||
pub mod edit;
|
pub mod edit;
|
||||||
pub mod history;
|
pub mod history;
|
||||||
|
|
|
@ -2,14 +2,12 @@ use crate::frontend::article_title;
|
||||||
use crate::frontend::components::article_nav::ArticleNav;
|
use crate::frontend::components::article_nav::ArticleNav;
|
||||||
use crate::frontend::pages::article_resource;
|
use crate::frontend::pages::article_resource;
|
||||||
use leptos::*;
|
use leptos::*;
|
||||||
use leptos_router::*;
|
|
||||||
use markdown_it::MarkdownIt;
|
use markdown_it::MarkdownIt;
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn ReadArticle() -> impl IntoView {
|
pub fn ReadArticle() -> impl IntoView {
|
||||||
let params = use_params_map();
|
let article = article_resource();
|
||||||
let title = move || params.get().get("title").cloned();
|
|
||||||
let article = article_resource(title);
|
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
<ArticleNav article=article/>
|
<ArticleNav article=article/>
|
||||||
|
|
|
@ -7,8 +7,7 @@ use leptos_router::*;
|
||||||
#[component]
|
#[component]
|
||||||
pub fn EditDiff() -> impl IntoView {
|
pub fn EditDiff() -> impl IntoView {
|
||||||
let params = use_params_map();
|
let params = use_params_map();
|
||||||
let title = move || params.get().get("title").cloned();
|
let article = article_resource();
|
||||||
let article = article_resource(title);
|
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
<ArticleNav article=article/>
|
<ArticleNav article=article/>
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
use crate::common::{ArticleView, GetArticleData, MAIN_PAGE_NAME};
|
use crate::common::{ArticleView, GetArticleData, MAIN_PAGE_NAME};
|
||||||
use crate::frontend::app::GlobalState;
|
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 article;
|
||||||
pub(crate) mod diff;
|
pub(crate) mod diff;
|
||||||
|
@ -10,9 +11,9 @@ pub(crate) mod register;
|
||||||
pub(crate) mod search;
|
pub(crate) mod search;
|
||||||
pub(crate) mod user_profile;
|
pub(crate) mod user_profile;
|
||||||
|
|
||||||
fn article_resource(
|
fn article_resource() -> Resource<Option<String>, ArticleView> {
|
||||||
title: impl Fn() -> Option<String> + 'static,
|
let params = use_params_map();
|
||||||
) -> Resource<Option<String>, ArticleView> {
|
let title = move || params.get().get("title").cloned();
|
||||||
create_resource(title, move |title| async move {
|
create_resource(title, move |title| async move {
|
||||||
let mut title = title.unwrap_or(MAIN_PAGE_NAME.to_string());
|
let mut title = title.unwrap_or(MAIN_PAGE_NAME.to_string());
|
||||||
let mut domain = None;
|
let mut domain = None;
|
||||||
|
|
|
@ -525,6 +525,7 @@ async fn test_fork_article() -> MyResult<()> {
|
||||||
// fork the article to local instance
|
// fork the article to local instance
|
||||||
let fork_form = ForkArticleData {
|
let fork_form = ForkArticleData {
|
||||||
article_id: resolved_article.id,
|
article_id: resolved_article.id,
|
||||||
|
new_title: resolved_article.title.clone(),
|
||||||
};
|
};
|
||||||
let fork_res = data.beta.fork_article(&fork_form).await?;
|
let fork_res = data.beta.fork_article(&fork_form).await?;
|
||||||
let forked_article = fork_res.article;
|
let forked_article = fork_res.article;
|
||||||
|
|
Loading…
Reference in a new issue