simple-web-app

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README

commit e57ad4bcdeca11e8c57bf6e874ea0d8adb511c09
parent cfcd136f9f62f5e20df384f95af056ed25f7e8c4
Author: Silas Brack <silasbrack@gmail.com>
Date:   Sun,  7 Jun 2026 18:23:58 +0200

feat: restore tag selection using hidden field with JS

Use a hidden input populated by JS on checkbox change to send tags
as a comma-separated string, avoiding serde_urlencoded's inability
to handle repeated form keys.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

Diffstat:
Msrc/handlers/story.rs | 29+++++++++++++++++++++++++++--
Mtemplates/edit.html | 15+++++++++++++--
Mtemplates/submit.html | 15+++++++++++++--
3 files changed, 53 insertions(+), 6 deletions(-)

diff --git a/src/handlers/story.rs b/src/handlers/story.rs @@ -139,11 +139,17 @@ pub async fn show_submit( Ok(Html(app.render()?)) } +fn parse_tags(s: &str) -> Vec<i64> { + s.split(',').filter_map(|s| s.trim().parse().ok()).collect() +} + #[derive(Debug, Deserialize)] pub struct SubmitForm { pub title: String, pub url: Option<String>, pub text: Option<String>, + #[serde(default)] + pub tags: String, } pub async fn submit_story( @@ -200,6 +206,13 @@ pub async fn submit_story( .await .map_err(|e| render_submit_error(&form, &all_tags, &format!("Failed to create story: {}", e)))?; + // Add tags + for tag_id in parse_tags(&form.tags) { + if let Err(e) = trailbase::add_tag_to_story(&client, story.id, tag_id).await { + tracing::warn!("Failed to add tag {} to story {}: {}", tag_id, story.id, e); + } + } + Ok(Redirect::to(&format!("/story/{}", story.id))) } @@ -210,7 +223,7 @@ fn render_submit_error(form: &SubmitForm, all_tags: &[trailbase::Category], erro url: form.url.clone().unwrap_or_default(), text: form.text.clone().unwrap_or_default(), all_tags: all_tags.to_vec(), - selected_tags: vec![], + selected_tags: parse_tags(&form.tags), }; let app = ApplicationTemplate { content: submit.render().unwrap_or_default(), @@ -268,6 +281,8 @@ pub struct EditForm { pub title: String, pub url: Option<String>, pub text: Option<String>, + #[serde(default)] + pub tags: String, } pub async fn edit_story( @@ -316,6 +331,16 @@ pub async fn edit_story( .await .map_err(|e| render_edit_error(id, &form, &[], &format!("Failed to update: {}", e)))?; + // Update tags: remove all, then re-add selected + if let Err(e) = trailbase::remove_all_tags_from_story(&client, id).await { + tracing::warn!("Failed to remove tags from story {}: {}", id, e); + } + for tag_id in parse_tags(&form.tags) { + if let Err(e) = trailbase::add_tag_to_story(&client, id, tag_id).await { + tracing::warn!("Failed to add tag {} to story {}: {}", tag_id, id, e); + } + } + Ok(Redirect::to(&format!("/story/{}", id))) } @@ -327,7 +352,7 @@ fn render_edit_error(story_id: i64, form: &EditForm, all_tags: &[trailbase::Cate url: form.url.clone().unwrap_or_default(), text: form.text.clone().unwrap_or_default(), all_tags: all_tags.to_vec(), - selected_tags: vec![], + selected_tags: parse_tags(&form.tags), }; let app = ApplicationTemplate { content: edit.render().unwrap_or_default(), diff --git a/templates/edit.html b/templates/edit.html @@ -35,8 +35,9 @@ {% match tag.name %} {% when Some with (name) %} <label class="tag-checkbox"> - <input type="checkbox" name="tags" value="{{ tag.id }}" - {% if selected_tags.contains(tag.id) %} checked{% endif %} /> + <input type="checkbox" value="{{ tag.id }}" + {% if selected_tags.contains(tag.id) %} checked{% endif %} + onchange="updateTags(this.closest('form'))" /> {{ name }} </label> {% when None %} @@ -46,9 +47,19 @@ </div> {% endif %} + <input type="hidden" name="tags" id="tags-hidden" value="" /> <div class="form-actions"> <button type="submit">Save Changes</button> <a href="/story/{{ story_id }}">Cancel</a> </div> </form> + <script> + function updateTags(form) { + var checked = form.querySelectorAll('.tag-checkboxes input[type=checkbox]:checked'); + var ids = Array.from(checked).map(function(cb) { return cb.value; }); + form.querySelector('#tags-hidden').value = ids.join(','); + } + // Initialize on load + document.querySelectorAll('.submit-form').forEach(updateTags); + </script> </div> diff --git a/templates/submit.html b/templates/submit.html @@ -35,8 +35,9 @@ {% match tag.name %} {% when Some with (name) %} <label class="tag-checkbox"> - <input type="checkbox" name="tags" value="{{ tag.id }}" - {% if selected_tags.contains(tag.id) %} checked{% endif %} /> + <input type="checkbox" value="{{ tag.id }}" + {% if selected_tags.contains(tag.id) %} checked{% endif %} + onchange="updateTags(this.closest('form'))" /> {{ name }} </label> {% when None %} @@ -46,10 +47,20 @@ </div> {% endif %} + <input type="hidden" name="tags" id="tags-hidden" value="" /> <div class="form-actions"> <button type="submit">Submit</button> </div> </form> + <script> + function updateTags(form) { + var checked = form.querySelectorAll('.tag-checkboxes input[type=checkbox]:checked'); + var ids = Array.from(checked).map(function(cb) { return cb.value; }); + form.querySelector('#tags-hidden').value = ids.join(','); + } + // Initialize on load + document.querySelectorAll('.submit-form').forEach(updateTags); + </script> <p class="submit-info"> Leave URL blank to submit a question or discussion.