Rewrite article publication with the REST API

- Add a default App and ApiToken for each user, that is used by the front-end
- Add an API route to update an article (CSRF had to be disabled because of a bug in rocket_csrf)
- Use AJAX to publish and edit articles in the new editor, instead of weird hacks with HTML forms
This commit is contained in:
Ana Gelez
2019-08-03 23:04:25 +02:00
parent 4142e73018
commit cc998e7c61
17 changed files with 372 additions and 162 deletions
+97 -151
View File
@@ -16,17 +16,14 @@ macro_rules! mv {
fn get_elt_value(id: &'static str) -> String {
let elt = document().get_element_by_id(id).unwrap();
let inp: Result<InputElement, _> = elt.clone().try_into();
let select: Result<SelectElement, _> = elt.clone().try_into();
let textarea: Result<TextAreaElement, _> = elt.try_into();
inp.map(|i| i.raw_value())
.unwrap_or_else(|_| textarea.unwrap().value())
}
fn set_value<S: AsRef<str>>(id: &'static str, val: S) {
let elt = document().get_element_by_id(id).unwrap();
let inp: Result<InputElement, _> = elt.clone().try_into();
let textarea: Result<TextAreaElement, _> = elt.try_into();
inp.map(|i| i.set_raw_value(val.as_ref()))
.unwrap_or_else(|_| textarea.unwrap().set_value(val.as_ref()))
let res = inp.map(|i| i.raw_value()).unwrap_or_else(|_| {
textarea
.map(|t| t.value())
.unwrap_or_else(|_| select.unwrap().value().unwrap_or_default())
});
res
}
fn no_return(evt: KeyDownEvent) {
@@ -163,12 +160,102 @@ fn init_editor() -> Result<(), EditorError> {
};
});
document()
.get_element_by_id("confirm-publish")?
.add_event_listener(|_: ClickEvent| {
save(false);
});
document()
.get_element_by_id("save-draft")?
.add_event_listener(|_: ClickEvent| {
save(true);
});
show_errors();
setup_close_button();
}
Ok(())
}
fn save(is_draft: bool) {
let req = XmlHttpRequest::new();
if bool::try_from(js! { return window.editing }).unwrap_or(false) {
req.open(
"PUT",
&format!(
"/api/v1/posts/{}",
i32::try_from(js! { return window.post_id }).unwrap()
),
)
.unwrap();
} else {
req.open("POST", "/api/v1/posts").unwrap();
}
req.set_request_header("Accept", "application/json")
.unwrap();
req.set_request_header("Content-Type", "application/json")
.unwrap();
req.set_request_header(
"Authorization",
&format!(
"Bearer {}",
String::try_from(js! { return window.api_token }).unwrap()
),
)
.unwrap();
let req_clone = req.clone();
req.add_event_listener(move |_: ProgressLoadEvent| {
if let Ok(Some(res)) = req_clone.response_text() {
serde_json::from_str(&res)
.map(|res: plume_api::posts::PostData| {
let url = res.url;
js! {
window.location.href = @{url};
};
})
.map_err(|_| {
let json: serde_json::Value = serde_json::from_str(&res).unwrap();
window().alert(&format!(
"Error: {}",
json["error"].as_str().unwrap_or_default()
));
})
.ok();
}
});
let data = plume_api::posts::NewPostData {
title: HtmlElement::try_from(document().get_element_by_id("editor-title").unwrap())
.unwrap()
.inner_text(),
subtitle: document()
.get_element_by_id("editor-subtitle")
.map(|s| HtmlElement::try_from(s).unwrap().inner_text()),
source: HtmlElement::try_from(
document()
.get_element_by_id("editor-default-paragraph")
.unwrap(),
)
.unwrap()
.inner_text(),
author: String::new(), // it is ignored anyway (TODO: remove it ??)
blog_id: i32::try_from(js! { return window.blog_id }).ok(),
published: Some(!is_draft),
creation_date: None,
license: Some(get_elt_value("license")),
tags: Some(
get_elt_value("tags")
.split(',')
.map(|t| t.trim().to_string())
.filter(|t| !t.is_empty())
.collect(),
),
cover_id: get_elt_value("cover").parse().ok(),
};
let json = serde_json::to_string(&data).unwrap();
req.send_with_string(&json).unwrap();
}
fn setup_close_button() {
if let Some(button) = document().get_element_by_id("close-editor") {
button.add_event_listener(|_: ClickEvent| {
@@ -201,108 +288,6 @@ fn show_errors() {
}
}
fn init_popup(
title: &HtmlElement,
subtitle: &HtmlElement,
content: &HtmlElement,
old_ed: &Element,
) -> Result<Element, EditorError> {
let popup = document().create_element("div")?;
popup.class_list().add("popup")?;
popup.set_attribute("id", "publish-popup")?;
let tags = get_elt_value("tags")
.split(',')
.map(str::trim)
.map(str::to_string)
.collect::<Vec<_>>();
let license = get_elt_value("license");
make_input(&i18n!(CATALOG, "Tags"), "popup-tags", &popup).set_raw_value(&tags.join(", "));
make_input(&i18n!(CATALOG, "License"), "popup-license", &popup).set_raw_value(&license);
let cover_label = document().create_element("label")?;
cover_label.append_child(&document().create_text_node(&i18n!(CATALOG, "Cover")));
cover_label.set_attribute("for", "cover")?;
let cover = document().get_element_by_id("cover")?;
cover.parent_element()?.remove_child(&cover).ok();
popup.append_child(&cover_label);
popup.append_child(&cover);
if let Some(draft_checkbox) = document().get_element_by_id("draft") {
let draft_label = document().create_element("label")?;
draft_label.set_attribute("for", "popup-draft")?;
let draft = document().create_element("input").unwrap();
js! {
@{&draft}.id = "popup-draft";
@{&draft}.name = "popup-draft";
@{&draft}.type = "checkbox";
@{&draft}.checked = @{&draft_checkbox}.checked;
};
draft_label.append_child(&draft);
draft_label.append_child(&document().create_text_node(&i18n!(CATALOG, "This is a draft")));
popup.append_child(&draft_label);
}
let button = document().create_element("input")?;
js! {
@{&button}.type = "submit";
@{&button}.value = @{i18n!(CATALOG, "Publish")};
};
button.append_child(&document().create_text_node(&i18n!(CATALOG, "Publish")));
button.add_event_listener(
mv!(title, subtitle, content, old_ed => move |_: ClickEvent| {
title.focus(); // Remove the placeholder before publishing
set_value("title", title.inner_text());
subtitle.focus();
set_value("subtitle", subtitle.inner_text());
content.focus();
set_value("editor-content", content.child_nodes().iter().fold(String::new(), |md, ch| {
let to_append = match ch.node_type() {
NodeType::Element => {
if js!{ return @{&ch}.tagName; } == "DIV" {
(js!{ return @{&ch}.innerHTML; }).try_into().unwrap_or_default()
} else {
(js!{ return @{&ch}.outerHTML; }).try_into().unwrap_or_default()
}
},
NodeType::Text => ch.node_value().unwrap_or_default(),
_ => unreachable!(),
};
format!("{}\n\n{}", md, to_append)
}));
set_value("tags", get_elt_value("popup-tags"));
if let Some(draft) = document().get_element_by_id("popup-draft") {
js!{
document.getElementById("draft").checked = @{draft}.checked;
};
}
let cover = document().get_element_by_id("cover").unwrap();
cover.parent_element().unwrap().remove_child(&cover).ok();
old_ed.append_child(&cover);
set_value("license", get_elt_value("popup-license"));
js! {
@{&old_ed}.submit();
};
}),
);
popup.append_child(&button);
document().body()?.append_child(&popup);
Ok(popup)
}
fn init_popup_bg() -> Result<Element, EditorError> {
let bg = document().create_element("div")?;
bg.class_list().add("popup-bg")?;
bg.set_attribute("id", "popup-bg")?;
document().body()?.append_child(&bg);
bg.add_event_listener(|_: ClickEvent| close_popup());
Ok(bg)
}
fn chars_left(selector: &str, content: &Element) -> Option<i32> {
match document().query_selector(selector) {
Ok(Some(form)) => HtmlElement::try_from(form).ok().and_then(|form| {
@@ -329,42 +314,3 @@ fn chars_left(selector: &str, content: &Element) -> Option<i32> {
_ => None,
}
}
fn close_popup() {
let hide = |x: Element| x.class_list().remove("show");
document().get_element_by_id("publish-popup").map(hide);
document().get_element_by_id("popup-bg").map(hide);
}
fn make_input(label_text: &str, name: &'static str, form: &Element) -> InputElement {
let label = document().create_element("label").unwrap();
label.append_child(&document().create_text_node(label_text));
label.set_attribute("for", name).unwrap();
let inp: InputElement = document()
.create_element("input")
.unwrap()
.try_into()
.unwrap();
inp.set_attribute("name", name).unwrap();
inp.set_attribute("id", name).unwrap();
form.append_child(&label);
form.append_child(&inp);
inp
}
fn make_editable(tag: &'static str) -> Element {
let elt = document()
.create_element(tag)
.expect("Couldn't create editable element");
elt.set_attribute("contenteditable", "true")
.expect("Couldn't make the element editable");
elt
}
fn clear_children(elt: &HtmlElement) {
for child in elt.child_nodes() {
elt.remove_child(&child).unwrap();
}
}
+1
View File
@@ -8,6 +8,7 @@ extern crate gettext_macros;
extern crate lazy_static;
#[macro_use]
extern crate stdweb;
extern crate serde_json;
use stdweb::web::{event::*, *};