mirror of
https://github.com/Nutomic/ibis.git
synced 2024-11-22 18:31:10 +00:00
basic article editing and history
This commit is contained in:
parent
e0cc23c0bc
commit
8305680fda
25 changed files with 303 additions and 93 deletions
|
@ -49,13 +49,16 @@ create table edit (
|
||||||
hash uuid not null,
|
hash uuid not null,
|
||||||
ap_id varchar(255) not null unique,
|
ap_id varchar(255) not null unique,
|
||||||
diff text not null,
|
diff text not null,
|
||||||
|
summary text not null,
|
||||||
article_id int REFERENCES article ON UPDATE CASCADE ON DELETE CASCADE NOT NULL,
|
article_id int REFERENCES article ON UPDATE CASCADE ON DELETE CASCADE NOT NULL,
|
||||||
previous_version_id uuid not null
|
previous_version_id uuid not null,
|
||||||
|
created timestamptz not null
|
||||||
);
|
);
|
||||||
|
|
||||||
create table conflict (
|
create table conflict (
|
||||||
id uuid primary key,
|
id uuid primary key,
|
||||||
diff text not null,
|
diff text not null,
|
||||||
|
summary text not null,
|
||||||
creator_id int REFERENCES local_user ON UPDATE CASCADE ON DELETE CASCADE NOT NULL,
|
creator_id int REFERENCES local_user ON UPDATE CASCADE ON DELETE CASCADE NOT NULL,
|
||||||
article_id int REFERENCES article ON UPDATE CASCADE ON DELETE CASCADE NOT NULL,
|
article_id int REFERENCES article ON UPDATE CASCADE ON DELETE CASCADE NOT NULL,
|
||||||
previous_version_id uuid not null
|
previous_version_id uuid not null
|
||||||
|
|
|
@ -20,6 +20,7 @@ use axum::Extension;
|
||||||
use axum::Form;
|
use axum::Form;
|
||||||
use axum::Json;
|
use axum::Json;
|
||||||
use axum_macros::debug_handler;
|
use axum_macros::debug_handler;
|
||||||
|
use chrono::Utc;
|
||||||
use diffy::create_patch;
|
use diffy::create_patch;
|
||||||
|
|
||||||
/// Create a new article with empty text, and federate it to followers.
|
/// Create a new article with empty text, and federate it to followers.
|
||||||
|
@ -70,11 +71,19 @@ pub(in crate::backend::api) async fn edit_article(
|
||||||
DbConflict::delete(resolve_conflict_id, &data.db_connection)?;
|
DbConflict::delete(resolve_conflict_id, &data.db_connection)?;
|
||||||
}
|
}
|
||||||
let original_article = DbArticle::read_view(edit_form.article_id, &data.db_connection)?;
|
let original_article = DbArticle::read_view(edit_form.article_id, &data.db_connection)?;
|
||||||
|
dbg!(&edit_form.new_text, &original_article.article.text);
|
||||||
|
if edit_form.new_text == original_article.article.text {
|
||||||
|
return Err(anyhow!("Edit contains no changes").into());
|
||||||
|
}
|
||||||
|
if edit_form.summary.is_empty() {
|
||||||
|
return Err(anyhow!("No summary given").into());
|
||||||
|
}
|
||||||
|
|
||||||
if edit_form.previous_version_id == original_article.latest_version {
|
if edit_form.previous_version_id == original_article.latest_version {
|
||||||
// No intermediate changes, simply submit new version
|
// No intermediate changes, simply submit new version
|
||||||
submit_article_update(
|
submit_article_update(
|
||||||
edit_form.new_text.clone(),
|
edit_form.new_text.clone(),
|
||||||
|
edit_form.summary.clone(),
|
||||||
edit_form.previous_version_id,
|
edit_form.previous_version_id,
|
||||||
&original_article.article,
|
&original_article.article,
|
||||||
user.person.id,
|
user.person.id,
|
||||||
|
@ -93,6 +102,7 @@ pub(in crate::backend::api) async fn edit_article(
|
||||||
let form = DbConflictForm {
|
let form = DbConflictForm {
|
||||||
id: EditVersion::new(&patch.to_string())?,
|
id: EditVersion::new(&patch.to_string())?,
|
||||||
diff: patch.to_string(),
|
diff: patch.to_string(),
|
||||||
|
summary: edit_form.summary.clone(),
|
||||||
creator_id: user.local_user.id,
|
creator_id: user.local_user.id,
|
||||||
article_id: original_article.article.id,
|
article_id: original_article.article.id,
|
||||||
previous_version_id: previous_version.hash,
|
previous_version_id: previous_version.hash,
|
||||||
|
@ -159,10 +169,12 @@ pub(in crate::backend::api) async fn fork_article(
|
||||||
let form = DbEditForm {
|
let form = DbEditForm {
|
||||||
ap_id,
|
ap_id,
|
||||||
diff: e.diff,
|
diff: e.diff,
|
||||||
|
summary: e.summary,
|
||||||
creator_id: e.creator_id,
|
creator_id: e.creator_id,
|
||||||
article_id: article.id,
|
article_id: article.id,
|
||||||
hash: e.hash,
|
hash: e.hash,
|
||||||
previous_version_id: e.previous_version_id,
|
previous_version_id: e.previous_version_id,
|
||||||
|
created: Utc::now(),
|
||||||
};
|
};
|
||||||
DbEdit::create(&form, &data.db_connection)?;
|
DbEdit::create(&form, &data.db_connection)?;
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,6 +25,7 @@ use std::sync::Mutex;
|
||||||
pub struct DbConflict {
|
pub struct DbConflict {
|
||||||
pub id: EditVersion,
|
pub id: EditVersion,
|
||||||
pub diff: String,
|
pub diff: String,
|
||||||
|
pub summary: String,
|
||||||
pub creator_id: i32,
|
pub creator_id: i32,
|
||||||
pub article_id: i32,
|
pub article_id: i32,
|
||||||
pub previous_version_id: EditVersion,
|
pub previous_version_id: EditVersion,
|
||||||
|
@ -35,6 +36,7 @@ pub struct DbConflict {
|
||||||
pub struct DbConflictForm {
|
pub struct DbConflictForm {
|
||||||
pub id: EditVersion,
|
pub id: EditVersion,
|
||||||
pub diff: String,
|
pub diff: String,
|
||||||
|
pub summary: String,
|
||||||
pub creator_id: i32,
|
pub creator_id: i32,
|
||||||
pub article_id: i32,
|
pub article_id: i32,
|
||||||
pub previous_version_id: EditVersion,
|
pub previous_version_id: EditVersion,
|
||||||
|
@ -82,6 +84,7 @@ impl DbConflict {
|
||||||
// federate the change
|
// federate the change
|
||||||
submit_article_update(
|
submit_article_update(
|
||||||
new_text,
|
new_text,
|
||||||
|
self.summary.clone(),
|
||||||
self.previous_version_id.clone(),
|
self.previous_version_id.clone(),
|
||||||
&original_article,
|
&original_article,
|
||||||
self.creator_id,
|
self.creator_id,
|
||||||
|
|
|
@ -3,6 +3,7 @@ use crate::backend::error::MyResult;
|
||||||
use crate::common::EditVersion;
|
use crate::common::EditVersion;
|
||||||
use crate::common::{DbArticle, DbEdit};
|
use crate::common::{DbArticle, DbEdit};
|
||||||
use activitypub_federation::fetch::object_id::ObjectId;
|
use activitypub_federation::fetch::object_id::ObjectId;
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
use diesel::ExpressionMethods;
|
use diesel::ExpressionMethods;
|
||||||
use diesel::{insert_into, AsChangeset, Insertable, PgConnection, QueryDsl, RunQueryDsl};
|
use diesel::{insert_into, AsChangeset, Insertable, PgConnection, QueryDsl, RunQueryDsl};
|
||||||
use diffy::create_patch;
|
use diffy::create_patch;
|
||||||
|
@ -16,8 +17,10 @@ pub struct DbEditForm {
|
||||||
pub hash: EditVersion,
|
pub hash: EditVersion,
|
||||||
pub ap_id: ObjectId<DbEdit>,
|
pub ap_id: ObjectId<DbEdit>,
|
||||||
pub diff: String,
|
pub diff: String,
|
||||||
|
pub summary: String,
|
||||||
pub article_id: i32,
|
pub article_id: i32,
|
||||||
pub previous_version_id: EditVersion,
|
pub previous_version_id: EditVersion,
|
||||||
|
pub created: DateTime<Utc>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DbEditForm {
|
impl DbEditForm {
|
||||||
|
@ -25,6 +28,7 @@ impl DbEditForm {
|
||||||
original_article: &DbArticle,
|
original_article: &DbArticle,
|
||||||
creator_id: i32,
|
creator_id: i32,
|
||||||
updated_text: &str,
|
updated_text: &str,
|
||||||
|
summary: String,
|
||||||
previous_version_id: EditVersion,
|
previous_version_id: EditVersion,
|
||||||
) -> MyResult<Self> {
|
) -> MyResult<Self> {
|
||||||
let diff = create_patch(&original_article.text, updated_text);
|
let diff = create_patch(&original_article.text, updated_text);
|
||||||
|
@ -37,6 +41,8 @@ impl DbEditForm {
|
||||||
creator_id,
|
creator_id,
|
||||||
article_id: original_article.id,
|
article_id: original_article.id,
|
||||||
previous_version_id,
|
previous_version_id,
|
||||||
|
summary,
|
||||||
|
created: Utc::now(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,6 +16,7 @@ diesel::table! {
|
||||||
conflict (id) {
|
conflict (id) {
|
||||||
id -> Uuid,
|
id -> Uuid,
|
||||||
diff -> Text,
|
diff -> Text,
|
||||||
|
summary -> Text,
|
||||||
creator_id -> Int4,
|
creator_id -> Int4,
|
||||||
article_id -> Int4,
|
article_id -> Int4,
|
||||||
previous_version_id -> Uuid,
|
previous_version_id -> Uuid,
|
||||||
|
@ -30,8 +31,10 @@ diesel::table! {
|
||||||
#[max_length = 255]
|
#[max_length = 255]
|
||||||
ap_id -> Varchar,
|
ap_id -> Varchar,
|
||||||
diff -> Text,
|
diff -> Text,
|
||||||
|
summary -> Text,
|
||||||
article_id -> Int4,
|
article_id -> Int4,
|
||||||
previous_version_id -> Uuid,
|
previous_version_id -> Uuid,
|
||||||
|
created -> Timestamptz,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -88,13 +91,6 @@ diesel::table! {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
diesel::table! {
|
|
||||||
secret (id) {
|
|
||||||
id -> Int4,
|
|
||||||
jwt_secret -> Varchar,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
diesel::joinable!(article -> instance (instance_id));
|
diesel::joinable!(article -> instance (instance_id));
|
||||||
diesel::joinable!(conflict -> article (article_id));
|
diesel::joinable!(conflict -> article (article_id));
|
||||||
diesel::joinable!(conflict -> local_user (creator_id));
|
diesel::joinable!(conflict -> local_user (creator_id));
|
||||||
|
@ -113,5 +109,4 @@ diesel::allow_tables_to_appear_in_same_query!(
|
||||||
jwt_secret,
|
jwt_secret,
|
||||||
local_user,
|
local_user,
|
||||||
person,
|
person,
|
||||||
secret,
|
|
||||||
);
|
);
|
||||||
|
|
|
@ -7,6 +7,7 @@ use crate::common::DbInstance;
|
||||||
use crate::common::EditVersion;
|
use crate::common::EditVersion;
|
||||||
use crate::common::{DbArticle, DbEdit};
|
use crate::common::{DbArticle, DbEdit};
|
||||||
use activitypub_federation::config::Data;
|
use activitypub_federation::config::Data;
|
||||||
|
use chrono::Utc;
|
||||||
|
|
||||||
pub mod accept;
|
pub mod accept;
|
||||||
pub mod create_article;
|
pub mod create_article;
|
||||||
|
@ -17,12 +18,19 @@ pub mod update_remote_article;
|
||||||
|
|
||||||
pub async fn submit_article_update(
|
pub async fn submit_article_update(
|
||||||
new_text: String,
|
new_text: String,
|
||||||
|
summary: String,
|
||||||
previous_version: EditVersion,
|
previous_version: EditVersion,
|
||||||
original_article: &DbArticle,
|
original_article: &DbArticle,
|
||||||
creator_id: i32,
|
creator_id: i32,
|
||||||
data: &Data<MyDataHandle>,
|
data: &Data<MyDataHandle>,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
let form = DbEditForm::new(original_article, creator_id, &new_text, previous_version)?;
|
let form = DbEditForm::new(
|
||||||
|
original_article,
|
||||||
|
creator_id,
|
||||||
|
&new_text,
|
||||||
|
summary,
|
||||||
|
previous_version,
|
||||||
|
)?;
|
||||||
if original_article.local {
|
if original_article.local {
|
||||||
let edit = DbEdit::create(&form, &data.db_connection)?;
|
let edit = DbEdit::create(&form, &data.db_connection)?;
|
||||||
let updated_article =
|
let updated_article =
|
||||||
|
@ -37,8 +45,10 @@ pub async fn submit_article_update(
|
||||||
hash: form.hash,
|
hash: form.hash,
|
||||||
ap_id: form.ap_id,
|
ap_id: form.ap_id,
|
||||||
diff: form.diff,
|
diff: form.diff,
|
||||||
|
summary: form.summary,
|
||||||
article_id: form.article_id,
|
article_id: form.article_id,
|
||||||
previous_version_id: form.previous_version_id,
|
previous_version_id: form.previous_version_id,
|
||||||
|
created: Utc::now(),
|
||||||
};
|
};
|
||||||
let instance = DbInstance::read(original_article.instance_id, &data.db_connection)?;
|
let instance = DbInstance::read(original_article.instance_id, &data.db_connection)?;
|
||||||
UpdateRemoteArticle::send(edit, instance, data).await?;
|
UpdateRemoteArticle::send(edit, instance, data).await?;
|
||||||
|
|
|
@ -77,6 +77,7 @@ impl ActivityHandler for RejectEdit {
|
||||||
let form = DbConflictForm {
|
let form = DbConflictForm {
|
||||||
id: EditVersion::new(&self.object.content)?,
|
id: EditVersion::new(&self.object.content)?,
|
||||||
diff: self.object.content,
|
diff: self.object.content,
|
||||||
|
summary: self.object.summary,
|
||||||
creator_id: creator.id,
|
creator_id: creator.id,
|
||||||
article_id: article.id,
|
article_id: article.id,
|
||||||
previous_version_id: self.object.previous_version,
|
previous_version_id: self.object.previous_version,
|
||||||
|
|
|
@ -7,6 +7,7 @@ use crate::common::{DbArticle, DbEdit};
|
||||||
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 activitypub_federation::traits::Object;
|
use activitypub_federation::traits::Object;
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
|
@ -24,10 +25,12 @@ pub struct ApubEdit {
|
||||||
kind: PatchType,
|
kind: PatchType,
|
||||||
pub id: ObjectId<DbEdit>,
|
pub id: ObjectId<DbEdit>,
|
||||||
pub content: String,
|
pub content: String,
|
||||||
|
pub summary: String,
|
||||||
pub version: EditVersion,
|
pub version: EditVersion,
|
||||||
pub previous_version: EditVersion,
|
pub previous_version: EditVersion,
|
||||||
pub object: ObjectId<DbArticle>,
|
pub object: ObjectId<DbArticle>,
|
||||||
pub attributed_to: ObjectId<DbPerson>,
|
pub attributed_to: ObjectId<DbPerson>,
|
||||||
|
pub published: DateTime<Utc>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait::async_trait]
|
#[async_trait::async_trait]
|
||||||
|
@ -50,10 +53,12 @@ impl Object for DbEdit {
|
||||||
kind: PatchType::Patch,
|
kind: PatchType::Patch,
|
||||||
id: self.ap_id,
|
id: self.ap_id,
|
||||||
content: self.diff,
|
content: self.diff,
|
||||||
|
summary: self.summary,
|
||||||
version: self.hash,
|
version: self.hash,
|
||||||
previous_version: self.previous_version_id,
|
previous_version: self.previous_version_id,
|
||||||
object: article.ap_id,
|
object: article.ap_id,
|
||||||
attributed_to: creator.ap_id,
|
attributed_to: creator.ap_id,
|
||||||
|
published: self.created,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -72,9 +77,11 @@ impl Object for DbEdit {
|
||||||
creator_id: creator.id,
|
creator_id: creator.id,
|
||||||
ap_id: json.id,
|
ap_id: json.id,
|
||||||
diff: json.content,
|
diff: json.content,
|
||||||
|
summary: json.summary,
|
||||||
article_id: article.id,
|
article_id: article.id,
|
||||||
hash: json.version,
|
hash: json.version,
|
||||||
previous_version_id: json.previous_version,
|
previous_version_id: json.previous_version,
|
||||||
|
created: json.published,
|
||||||
};
|
};
|
||||||
let edit = DbEdit::create(&form, &data.db_connection)?;
|
let edit = DbEdit::create(&form, &data.db_connection)?;
|
||||||
Ok(edit)
|
Ok(edit)
|
||||||
|
|
|
@ -9,7 +9,7 @@ use url::{ParseError, Url};
|
||||||
|
|
||||||
pub fn generate_activity_id(domain: &Url) -> Result<Url, ParseError> {
|
pub fn generate_activity_id(domain: &Url) -> Result<Url, ParseError> {
|
||||||
let port = domain.port().unwrap();
|
let port = domain.port().unwrap();
|
||||||
let domain = domain.domain().unwrap();
|
let domain = domain.host_str().unwrap();
|
||||||
let id: String = thread_rng()
|
let id: String = thread_rng()
|
||||||
.sample_iter(&Alphanumeric)
|
.sample_iter(&Alphanumeric)
|
||||||
.take(7)
|
.take(7)
|
||||||
|
|
|
@ -60,9 +60,11 @@ pub struct DbEdit {
|
||||||
#[cfg(not(feature = "ssr"))]
|
#[cfg(not(feature = "ssr"))]
|
||||||
pub ap_id: String,
|
pub ap_id: String,
|
||||||
pub diff: String,
|
pub diff: String,
|
||||||
|
pub summary: String,
|
||||||
pub article_id: i32,
|
pub article_id: i32,
|
||||||
/// First edit of an article always has `EditVersion::default()` here
|
/// First edit of an article always has `EditVersion::default()` here
|
||||||
pub previous_version_id: EditVersion,
|
pub previous_version_id: EditVersion,
|
||||||
|
pub created: DateTime<Utc>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The version hash of a specific edit. Generated by taking an SHA256 hash of the diff
|
/// The version hash of a specific edit. Generated by taking an SHA256 hash of the diff
|
||||||
|
@ -134,6 +136,8 @@ pub struct EditArticleData {
|
||||||
/// Full, new text of the article. A diff against `previous_version` is generated on the backend
|
/// Full, new text of the article. A diff against `previous_version` is generated on the backend
|
||||||
/// side to handle conflicts.
|
/// side to handle conflicts.
|
||||||
pub new_text: String,
|
pub new_text: String,
|
||||||
|
/// What was changed
|
||||||
|
pub summary: String,
|
||||||
/// The version that this edit is based on, ie [DbArticle.latest_version] or
|
/// The version that this edit is based on, ie [DbArticle.latest_version] or
|
||||||
/// [ApiConflict.previous_version]
|
/// [ApiConflict.previous_version]
|
||||||
pub previous_version_id: EditVersion,
|
pub previous_version_id: EditVersion,
|
||||||
|
|
|
@ -71,6 +71,7 @@ impl ApiClient {
|
||||||
let edit_form = EditArticleData {
|
let edit_form = EditArticleData {
|
||||||
article_id: article.article.id,
|
article_id: article.article.id,
|
||||||
new_text,
|
new_text,
|
||||||
|
summary: "initial text".to_string(),
|
||||||
previous_version_id: article.latest_version,
|
previous_version_id: article.latest_version,
|
||||||
resolve_conflict_id: None,
|
resolve_conflict_id: None,
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
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::edit_article::EditArticle;
|
use crate::frontend::pages::article::edit::EditArticle;
|
||||||
|
use crate::frontend::pages::article::history::ArticleHistory;
|
||||||
|
use crate::frontend::pages::article::read::ReadArticle;
|
||||||
|
use crate::frontend::pages::diff::EditDiff;
|
||||||
use crate::frontend::pages::login::Login;
|
use crate::frontend::pages::login::Login;
|
||||||
use crate::frontend::pages::read_article::ReadArticle;
|
|
||||||
use crate::frontend::pages::register::Register;
|
use crate::frontend::pages::register::Register;
|
||||||
use crate::frontend::pages::Page;
|
use crate::frontend::pages::Page;
|
||||||
use leptos::{
|
use leptos::{
|
||||||
|
@ -77,6 +79,8 @@ pub fn App() -> impl IntoView {
|
||||||
<Route path={Page::Home.path()} view=ReadArticle/>
|
<Route path={Page::Home.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/edit" view=EditArticle/>
|
||||||
|
<Route path="/article/:title/history" view=ArticleHistory/>
|
||||||
|
<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/>
|
||||||
</Routes>
|
</Routes>
|
||||||
|
|
24
src/frontend/components/article_nav.rs
Normal file
24
src/frontend/components/article_nav.rs
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
use crate::common::ArticleView;
|
||||||
|
use crate::frontend::app::GlobalState;
|
||||||
|
use leptos::*;
|
||||||
|
use leptos_router::*;
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn ArticleNav(article: Resource<String, ArticleView>) -> impl IntoView {
|
||||||
|
let global_state = use_context::<RwSignal<GlobalState>>().unwrap();
|
||||||
|
view! {
|
||||||
|
<Suspense fallback=|| view! { "Loading..." }>
|
||||||
|
{move || article.get().map(|article| {
|
||||||
|
let title = article.article.title;
|
||||||
|
view!{
|
||||||
|
<nav class="inner">
|
||||||
|
<A href={format!("/article/{title}")}>"Read"</A>
|
||||||
|
<A href={format!("/article/{title}/history")}>"History"</A>
|
||||||
|
<Show when=move || global_state.with(|state| state.my_profile.is_some())>
|
||||||
|
<A href={format!("/article/{title}/edit")}>"Edit"</A>
|
||||||
|
</Show>
|
||||||
|
</nav>
|
||||||
|
}})}
|
||||||
|
</Suspense>
|
||||||
|
}
|
||||||
|
}
|
|
@ -65,10 +65,9 @@ pub fn CredentialsForm(
|
||||||
<div>
|
<div>
|
||||||
<button
|
<button
|
||||||
prop:disabled=move || button_is_disabled.get()
|
prop:disabled=move || button_is_disabled.get()
|
||||||
on:click=move |_| dispatch_action()
|
on:click=move |_| dispatch_action()>
|
||||||
>
|
|
||||||
{action_label}
|
{action_label}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,2 +1,3 @@
|
||||||
|
pub mod article_nav;
|
||||||
pub(crate) mod credentials;
|
pub(crate) mod credentials;
|
||||||
pub mod nav;
|
pub mod nav;
|
||||||
|
|
|
@ -3,3 +3,7 @@ pub mod app;
|
||||||
mod components;
|
mod components;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
pub mod pages;
|
pub mod pages;
|
||||||
|
|
||||||
|
#[cfg(feature = "hydrate")]
|
||||||
|
#[wasm_bindgen::prelude::wasm_bindgen]
|
||||||
|
pub fn hydrate() {}
|
||||||
|
|
|
@ -1,16 +0,0 @@
|
||||||
use leptos::*;
|
|
||||||
use leptos_router::*;
|
|
||||||
|
|
||||||
#[component]
|
|
||||||
pub fn Article() -> impl IntoView {
|
|
||||||
view! {
|
|
||||||
<nav class="inner">
|
|
||||||
<li>
|
|
||||||
<A href="read">"Read"</A>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<A href="edit">"Edit"</A>
|
|
||||||
</li>
|
|
||||||
</nav>
|
|
||||||
}
|
|
||||||
}
|
|
93
src/frontend/pages/article/edit.rs
Normal file
93
src/frontend/pages/article/edit.rs
Normal file
|
@ -0,0 +1,93 @@
|
||||||
|
use crate::common::EditArticleData;
|
||||||
|
use crate::frontend::app::GlobalState;
|
||||||
|
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 = params.get_untracked().get("title").cloned();
|
||||||
|
let article = article_resource(title.unwrap());
|
||||||
|
|
||||||
|
let (text, set_text) = create_signal(String::new());
|
||||||
|
// TODO: set initial text, otherwise submit with no changes results in empty text
|
||||||
|
//article.with(|article| set_text.update(article.as_ref().unwrap().article.text.clone()));
|
||||||
|
let (summary, set_summary) = create_signal(String::new());
|
||||||
|
let (edit_response, set_edit_response) = create_signal(None::<()>);
|
||||||
|
let (edit_error, set_edit_error) = create_signal(None::<String>);
|
||||||
|
let (wait_for_response, set_wait_for_response) = create_signal(false);
|
||||||
|
let button_is_disabled =
|
||||||
|
Signal::derive(move || wait_for_response.get() || summary.get().is_empty());
|
||||||
|
let submit_action = create_action(move |(new_text, summary): &(String, String)| {
|
||||||
|
let new_text = new_text.clone();
|
||||||
|
let summary = summary.clone();
|
||||||
|
async move {
|
||||||
|
let form = EditArticleData {
|
||||||
|
article_id: article.get().unwrap().article.id,
|
||||||
|
new_text,
|
||||||
|
summary,
|
||||||
|
previous_version_id: article.get().unwrap().latest_version,
|
||||||
|
resolve_conflict_id: None,
|
||||||
|
};
|
||||||
|
set_wait_for_response.update(|w| *w = true);
|
||||||
|
let res = GlobalState::api_client().edit_article(&form).await;
|
||||||
|
set_wait_for_response.update(|w| *w = false);
|
||||||
|
match res {
|
||||||
|
Ok(_res) => {
|
||||||
|
set_edit_response.update(|v| *v = Some(()));
|
||||||
|
set_edit_error.update(|e| *e = None);
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
let msg = err.0.to_string();
|
||||||
|
log::warn!("Unable to edit: {msg}");
|
||||||
|
set_edit_error.update(|e| *e = Some(msg));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
view! {
|
||||||
|
<ArticleNav article=article.clone()/>
|
||||||
|
<Show
|
||||||
|
when=move || edit_response.get().is_some()
|
||||||
|
fallback=move || {
|
||||||
|
view! {
|
||||||
|
<Suspense fallback=|| view! { "Loading..." }> {
|
||||||
|
move || article.get().map(|article|
|
||||||
|
view! {
|
||||||
|
<div class="item-view">
|
||||||
|
<h1>{article.article.title.replace('_', " ")}</h1>
|
||||||
|
<textarea on:keyup=move |ev| {
|
||||||
|
let val = event_target_value(&ev);
|
||||||
|
set_text.update(|p| *p = val);
|
||||||
|
}>
|
||||||
|
{article.article.text}
|
||||||
|
</textarea>
|
||||||
|
</div>
|
||||||
|
{move || {
|
||||||
|
edit_error
|
||||||
|
.get()
|
||||||
|
.map(|err| {
|
||||||
|
view! { <p style="color:red;">{err}</p> }
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
<input type="text" on:keyup=move |ev| {
|
||||||
|
let val = event_target_value(&ev);
|
||||||
|
set_summary.update(|p| *p = val);
|
||||||
|
}/>
|
||||||
|
<button
|
||||||
|
prop:disabled=move || button_is_disabled.get()
|
||||||
|
on:click=move |_| submit_action.dispatch((text.get(), summary.get()))>
|
||||||
|
Submit
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</Suspense>
|
||||||
|
}}>
|
||||||
|
Edit successful!
|
||||||
|
</Show>
|
||||||
|
}
|
||||||
|
}
|
34
src/frontend/pages/article/history.rs
Normal file
34
src/frontend/pages/article/history.rs
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
use crate::frontend::components::article_nav::ArticleNav;
|
||||||
|
use crate::frontend::pages::article_resource;
|
||||||
|
use leptos::*;
|
||||||
|
use leptos_router::*;
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn ArticleHistory() -> impl IntoView {
|
||||||
|
let params = use_params_map();
|
||||||
|
let title = params.get().get("title").cloned();
|
||||||
|
let article = article_resource(title.unwrap());
|
||||||
|
|
||||||
|
view! {
|
||||||
|
<ArticleNav article=article.clone()/>
|
||||||
|
<Suspense fallback=|| view! { "Loading..." }> {
|
||||||
|
move || article.get().map(|article| {
|
||||||
|
let title = article.article.title;
|
||||||
|
view! {
|
||||||
|
<div class="item-view">
|
||||||
|
<h1>{title.replace('_', " ")}</h1>
|
||||||
|
{
|
||||||
|
article.edits.into_iter().rev().map(|edit| {
|
||||||
|
let path = format!("/article/{title}/diff/{}", edit.hash.0);
|
||||||
|
// TODO: need to return username from backend and show it
|
||||||
|
let label = format!("{} ({})", edit.summary, edit.created.to_rfc2822());
|
||||||
|
view! {<li><a href={path}>{label}</a></li> }
|
||||||
|
}).collect::<Vec<_>>()
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</Suspense>
|
||||||
|
}
|
||||||
|
}
|
3
src/frontend/pages/article/mod.rs
Normal file
3
src/frontend/pages/article/mod.rs
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
pub mod edit;
|
||||||
|
pub mod history;
|
||||||
|
pub mod read;
|
29
src/frontend/pages/article/read.rs
Normal file
29
src/frontend/pages/article/read.rs
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
use crate::frontend::components::article_nav::ArticleNav;
|
||||||
|
use crate::frontend::pages::article_resource;
|
||||||
|
use leptos::*;
|
||||||
|
use leptos_router::*;
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn ReadArticle() -> impl IntoView {
|
||||||
|
let params = use_params_map();
|
||||||
|
let title = params
|
||||||
|
.get_untracked()
|
||||||
|
.get("title")
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or("Main_Page".to_string());
|
||||||
|
let article = article_resource(title);
|
||||||
|
|
||||||
|
view! {
|
||||||
|
<ArticleNav article=article.clone()/>
|
||||||
|
<Suspense fallback=|| view! { "Loading..." }> {
|
||||||
|
move || article.get().map(|article|
|
||||||
|
view! {
|
||||||
|
<div class="item-view">
|
||||||
|
<h1>{article.article.title.replace('_', " ")}</h1>
|
||||||
|
<div>{article.article.text}</div>
|
||||||
|
</div>
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</Suspense>
|
||||||
|
}
|
||||||
|
}
|
32
src/frontend/pages/diff.rs
Normal file
32
src/frontend/pages/diff.rs
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
use crate::frontend::components::article_nav::ArticleNav;
|
||||||
|
use crate::frontend::pages::article_resource;
|
||||||
|
use leptos::*;
|
||||||
|
use leptos_router::*;
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn EditDiff() -> impl IntoView {
|
||||||
|
let params = use_params_map();
|
||||||
|
let title = params.get_untracked().get("title").cloned().unwrap();
|
||||||
|
let article = article_resource(title);
|
||||||
|
|
||||||
|
view! {
|
||||||
|
<ArticleNav article=article.clone()/>
|
||||||
|
<Suspense fallback=|| view! { "Loading..." }> {
|
||||||
|
move || article.get().map(|article| {
|
||||||
|
let hash = params
|
||||||
|
.get_untracked()
|
||||||
|
.get("hash")
|
||||||
|
.cloned().unwrap();
|
||||||
|
let edit = article.edits.iter().find(|e| e.hash.0.to_string() == hash).unwrap();
|
||||||
|
// TODO: need to show username
|
||||||
|
view! {
|
||||||
|
<div class="item-view">
|
||||||
|
<h1>{article.article.title.replace('_', " ")}</h1>
|
||||||
|
<pre>{edit.diff.clone()}</pre>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</Suspense>
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,11 +0,0 @@
|
||||||
use leptos::*;
|
|
||||||
|
|
||||||
#[component]
|
|
||||||
pub fn EditArticle() -> impl IntoView {
|
|
||||||
view! {
|
|
||||||
<div class="item-view">
|
|
||||||
<h1>Title...</h1>
|
|
||||||
<textarea></textarea>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,7 +1,10 @@
|
||||||
pub mod article;
|
use crate::common::{ArticleView, GetArticleData};
|
||||||
pub mod edit_article;
|
use crate::frontend::app::GlobalState;
|
||||||
|
use leptos::{create_resource, Resource};
|
||||||
|
|
||||||
|
pub(crate) mod article;
|
||||||
|
pub(crate) mod diff;
|
||||||
pub mod login;
|
pub mod login;
|
||||||
pub mod read_article;
|
|
||||||
pub mod register;
|
pub mod register;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, Default)]
|
#[derive(Debug, Clone, Copy, Default)]
|
||||||
|
@ -21,3 +24,19 @@ impl Page {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn article_resource(title: String) -> Resource<String, ArticleView> {
|
||||||
|
create_resource(
|
||||||
|
move || title.clone(),
|
||||||
|
move |title| async move {
|
||||||
|
GlobalState::api_client()
|
||||||
|
.get_article(GetArticleData {
|
||||||
|
title: Some(title),
|
||||||
|
instance_id: None,
|
||||||
|
id: None,
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
|
@ -1,47 +0,0 @@
|
||||||
use crate::common::GetArticleData;
|
|
||||||
use crate::frontend::app::GlobalState;
|
|
||||||
use leptos::*;
|
|
||||||
use leptos_router::*;
|
|
||||||
|
|
||||||
#[component]
|
|
||||||
pub fn ReadArticle() -> impl IntoView {
|
|
||||||
let params = use_params_map();
|
|
||||||
let article = create_resource(
|
|
||||||
move || {
|
|
||||||
params
|
|
||||||
.get()
|
|
||||||
.get("title")
|
|
||||||
.cloned()
|
|
||||||
.unwrap_or("Main_Page".to_string())
|
|
||||||
},
|
|
||||||
move |title| async move {
|
|
||||||
GlobalState::api_client()
|
|
||||||
.get_article(GetArticleData {
|
|
||||||
title: Some(title),
|
|
||||||
instance_id: None,
|
|
||||||
id: None,
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
let global_state = use_context::<RwSignal<GlobalState>>().unwrap();
|
|
||||||
let (count, set_count) = create_signal(0);
|
|
||||||
view! {
|
|
||||||
<Suspense fallback=|| view! { "Loading..." }>
|
|
||||||
{move || article.get().map(|article|
|
|
||||||
view! {
|
|
||||||
<div class="item-view">
|
|
||||||
<h1>{article.article.title.replace('_', " ")}</h1>
|
|
||||||
<Show when=move || global_state.with(|state| state.my_profile.is_some())>
|
|
||||||
<button on:click=move |_| {
|
|
||||||
set_count.update(|n| *n += 1);
|
|
||||||
}>Edit {move || count.get()}</button>
|
|
||||||
</Show>
|
|
||||||
<div>{article.article.text}</div>
|
|
||||||
</div>
|
|
||||||
})}
|
|
||||||
</Suspense>
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in a new issue