Implement support for editing page title

This commit is contained in:
Magnus Hoff 2017-09-21 10:09:57 +02:00
parent e82350e499
commit 3feed530ff
6 changed files with 82 additions and 29 deletions

View file

@ -15,9 +15,9 @@ function queryArgsFromForm(form) {
let hasBeenOpen = false; let hasBeenOpen = false;
function openEditor() { function openEditor() {
const article = document.querySelector("article"); const container = document.querySelector(".container");
const rendered = article.querySelector(".rendered"); const rendered = container.querySelector(".rendered");
const editor = article.querySelector(".editor"); const editor = container.querySelector(".editor");
const textarea = editor.querySelector('textarea[name="body"]'); const textarea = editor.querySelector('textarea[name="body"]');
const shadow = editor.querySelector('textarea.shadow-control'); const shadow = editor.querySelector('textarea.shadow-control');
const form = editor.querySelector("form"); const form = editor.querySelector("form");
@ -29,7 +29,7 @@ function openEditor() {
textarea.style.height = rendered.clientHeight + "px"; textarea.style.height = rendered.clientHeight + "px";
article.classList.add('edit'); container.classList.add('edit');
autosizeTextarea(textarea, shadow); autosizeTextarea(textarea, shadow);
@ -63,11 +63,23 @@ function openEditor() {
if (!response.ok) throw new Error("Unexpected status code (" + response.status + ")"); if (!response.ok) throw new Error("Unexpected status code (" + response.status + ")");
const result = await response.json(); const result = await response.json();
form.elements.base_revision.value = result.revision;
// Update url-bar, page title and footer
window.history.replaceState(null, result.title, result.slug);
document.querySelector("title").textContent = result.title;
revision.textContent = result.revision; revision.textContent = result.revision;
lastUpdated.textContent = result.created; lastUpdated.textContent = result.created;
// Update body:
rendered.innerHTML = result.rendered; rendered.innerHTML = result.rendered;
article.classList.remove('edit');
// Update form:
form.elements.base_revision.value = result.revision;
for (const element of form.elements) {
element.defaultValue = element.value;
}
container.classList.remove('edit');
textarea.disabled = false; textarea.disabled = false;
}() }()
@ -82,7 +94,7 @@ function openEditor() {
ev.preventDefault(); ev.preventDefault();
ev.stopPropagation(); ev.stopPropagation();
article.classList.remove('edit'); container.classList.remove('edit');
form.reset(); form.reset();
}); });
} }

View file

@ -91,7 +91,7 @@ body {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
article { .container {
flex: 1; flex: 1;
} }
@ -146,6 +146,15 @@ textarea {
overflow: hidden; overflow: hidden;
} }
h1>input {
font: inherit;
border: none;
background: none;
padding: 0;
width: 100%;
}
.shadow-control { .shadow-control {
visibility: hidden; visibility: hidden;
position: fixed; position: fixed;

View file

@ -86,6 +86,7 @@ impl Resource for ArticleResource {
revision: i32, revision: i32,
created: &'a chrono::DateTime<Local>, created: &'a chrono::DateTime<Local>,
slug: &'a str,
title: &'a str, title: &'a str,
raw: &'a str, raw: &'a str,
rendered: String, rendered: String,
@ -106,6 +107,7 @@ impl Resource for ArticleResource {
article_id: data.article_id, article_id: data.article_id,
revision: data.revision, revision: data.revision,
created: &Local.from_utc_datetime(&data.created), created: &Local.from_utc_datetime(&data.created),
slug: &data.slug,
title: &data.title, title: &data.title,
raw: &data.body, raw: &data.body,
rendered: render_markdown(&data.body), rendered: render_markdown(&data.body),
@ -125,12 +127,22 @@ impl Resource for ArticleResource {
#[derive(Deserialize)] #[derive(Deserialize)]
struct UpdateArticle { struct UpdateArticle {
base_revision: i32, base_revision: i32,
title: String,
body: String, body: String,
} }
#[derive(BartDisplay)]
#[template="templates/article_revision_contents.html"]
struct Template<'a> {
title: &'a str,
rendered: String,
}
#[derive(Serialize)] #[derive(Serialize)]
struct PutResponse<'a> { struct PutResponse<'a> {
slug: &'a str,
revision: i32, revision: i32,
title: &'a str,
rendered: &'a str, rendered: &'a str,
created: &'a str, created: &'a str,
} }
@ -143,15 +155,20 @@ impl Resource for ArticleResource {
.map_err(Into::into) .map_err(Into::into)
}) })
.and_then(move |update: UpdateArticle| { .and_then(move |update: UpdateArticle| {
self.state.update_article(self.article_id, update.base_revision, update.body) self.state.update_article(self.article_id, update.base_revision, update.title, update.body)
}) })
.and_then(|updated| { .and_then(|updated| {
futures::finished(Response::new() futures::finished(Response::new()
.with_status(hyper::StatusCode::Ok) .with_status(hyper::StatusCode::Ok)
.with_header(ContentType(APPLICATION_JSON.clone())) .with_header(ContentType(APPLICATION_JSON.clone()))
.with_body(serde_json::to_string(&PutResponse { .with_body(serde_json::to_string(&PutResponse {
slug: &updated.slug,
revision: updated.revision, revision: updated.revision,
rendered: &render_markdown(&updated.body), title: &updated.title,
rendered: &Template {
title: &updated.title,
rendered: render_markdown(&updated.body),
}.to_string(),
created: &Local.from_utc_datetime(&updated.created).to_string(), created: &Local.from_utc_datetime(&updated.created).to_string(),
}).expect("Should never fail")) }).expect("Should never fail"))
) )

View file

@ -26,7 +26,7 @@ pub enum SlugLookup {
Redirect(String), Redirect(String),
} }
fn decide_slug(conn: &SqliteConnection, prev_title: &str, title: &str, prev_slug: &str) -> Result<String, Error> { fn decide_slug(conn: &SqliteConnection, article_id: i32, prev_title: &str, title: &str, prev_slug: &str) -> Result<String, Error> {
if title == prev_title { if title == prev_title {
return Ok(prev_slug.to_owned()); return Ok(prev_slug.to_owned());
} }
@ -44,6 +44,7 @@ fn decide_slug(conn: &SqliteConnection, prev_title: &str, title: &str, prev_slug
loop { loop {
let slug_in_use = article_revisions::table let slug_in_use = article_revisions::table
.filter(article_revisions::article_id.ne(article_id))
.filter(article_revisions::slug.eq(&slug)) .filter(article_revisions::slug.eq(&slug))
.filter(article_revisions::latest.eq(true)) .filter(article_revisions::latest.eq(true))
.count() .count()
@ -124,7 +125,9 @@ impl State {
}) })
} }
pub fn update_article(&self, article_id: i32, base_revision: i32, body: String) -> CpuFuture<models::ArticleRevision, Error> { pub fn update_article(&self, article_id: i32, base_revision: i32, title: String, body: String)
-> CpuFuture<models::ArticleRevision, Error>
{
let connection_pool = self.connection_pool.clone(); let connection_pool = self.connection_pool.clone();
self.cpu_pool.spawn_fn(move || { self.cpu_pool.spawn_fn(move || {
@ -150,8 +153,7 @@ impl State {
} }
let new_revision = base_revision + 1; let new_revision = base_revision + 1;
let title = prev_title.clone(); // TODO Have title be a parameter to this function let slug = decide_slug(&*conn, article_id, &prev_title, &title, &prev_slug)?;
let slug = decide_slug(&*conn, &prev_title, &title, &prev_slug)?;
#[derive(Insertable)] #[derive(Insertable)]
#[table_name="article_revisions"] #[table_name="article_revisions"]

View file

@ -1,26 +1,32 @@
<script src="_assets/script-{{script_js_checksum}}.js" defer></script> <script src="_assets/script-{{script_js_checksum}}.js" defer></script>
<div class="container">
<div class="rendered">
{{>article_revision_contents.html}}
</div>
<div class="editor">
<form action="" method="POST">
<header> <header>
<h1>{{title}}</h1> <h1><input autocomplete=off type=text name=title value="{{title}}"></h1>
</header> </header>
<article> <article>
<div class="rendered"> <input autocomplete=off type=hidden name=base_revision value="{{revision}}">
{{{rendered}}} <textarea autocomplete=off name=body>{{raw}}</textarea>
</div> <textarea autocomplete=off class="shadow-control"></textarea>
<div class="editor">
<form action="" method="POST">
<input autocomplete=off type=hidden name=base_revision value="{{revision}}">
<textarea autocomplete=off name=body>{{raw}}</textarea>
<textarea autocomplete=off class="shadow-control"></textarea>
<div class="editor-controls">
<a class="cancel" href="{{article_id}}">Cancel</a>
<button type=submit>Save</button>
</div>
</form>
</div>
</article> </article>
<div class="editor-controls">
<a class="cancel" href="{{slug}}">Cancel</a>
<button type=submit>Save</button>
</div>
</form>
</div>
</div>
<footer> <footer>
<p><a id="openEditor" href="?editor">Edit</a></p> <p><a id="openEditor" href="?editor">Edit</a></p>
<dl> <dl>

View file

@ -0,0 +1,7 @@
<header>
<h1>{{title}}</h1>
</header>
<article>
{{{rendered}}}
</article>