Improve submission change displays#125
Conversation
📝 WalkthroughWalkthroughReplaces raw JSON string rendering of submission changes with structured view models. New structs in ChangesStructured Submission Change Views
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
8341c20 to
1e76124
Compare
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
templates/queue_detail.html (1)
26-110: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winConsider extracting the duplicated
change_viewsrendering into a shared Askama partial.This block (lines 26-110) is duplicated almost verbatim in
templates/disc_edit.html(lines 65-148). Since both templates expose achange_viewsfield, you can move the loop body into a partial (e.g.templates/_submission_change_views.html) and{% include %}it in both, eliminating the drift risk across two large blocks.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@templates/queue_detail.html` around lines 26 - 110, The `change_views` rendering is duplicated between `queue_detail.html` and `disc_edit.html`, so move the full loop body into a shared Askama partial such as a submission change views template and include it from both places. Keep the partial aligned with the existing `change_views`, `view`, `section`, and `row` rendering structure so both templates reuse the same markup and any future changes happen in one place.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/routes/queue.rs`:
- Around line 214-359: The new change-rendering helpers in queue.rs are
formatting raw JSON values, so lookup-backed fields like system_code,
media_type, regions, and languages will show codes instead of human-readable
labels. Update the helper flow around friendly_json_value, change_scalar_row,
change_multiline_row, and change_set_row to apply the same reference-data
formatting used by build_review_diff_context() via review_named_value,
display_region, and display_language before building change_views/change_details
rows. Keep the raw JSON parsing, but thread the display conversion through these
helpers so the new UI preserves human-readable output.
- Around line 749-763: The multiline detection in push_labeled_change_detail is
too narrow because it only checks for newline characters, so review-sidecar
fields like contents, cuesheet, and dat may render inline even though they
should be multiline. Update the is_multiline decision to also consider field
semantics in this function, using the field identifier plus the existing
current_value/previous_value checks, so the known multiline fields always get
multiline presentation regardless of whether the normalized text contains a
newline.
---
Nitpick comments:
In `@templates/queue_detail.html`:
- Around line 26-110: The `change_views` rendering is duplicated between
`queue_detail.html` and `disc_edit.html`, so move the full loop body into a
shared Askama partial such as a submission change views template and include it
from both places. Keep the partial aligned with the existing `change_views`,
`view`, `section`, and `row` rendering structure so both templates reuse the
same markup and any future changes happen in one place.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: ced54a36-e20a-44b7-90f4-49dc5b6ab05a
📒 Files selected for processing (8)
src/db/models.rssrc/routes/disc_edit.rssrc/routes/queue.rssrc/services/queue_service.rsstatic/css/app.csstemplates/disc_edit.htmltemplates/queue.htmltemplates/queue_detail.html
| fn display_change_value(value: &serde_json::Value) -> String { | ||
| review_annotation_value(value) | ||
| } | ||
|
|
||
| fn friendly_json_value(value: &serde_json::Value) -> String { | ||
| match value { | ||
| serde_json::Value::Null => String::new(), | ||
| serde_json::Value::String(s) => s.clone(), | ||
| serde_json::Value::Number(n) => n.to_string(), | ||
| serde_json::Value::Bool(b) => b.to_string(), | ||
| serde_json::Value::Array(values) => values | ||
| .iter() | ||
| .map(friendly_json_value) | ||
| .filter(|s| !s.trim().is_empty()) | ||
| .collect::<Vec<_>>() | ||
| .join(", "), | ||
| serde_json::Value::Object(map) => { | ||
| if let (Some(start), Some(end)) = (map.get("start"), map.get("end")) { | ||
| return format!( | ||
| "{}-{}", | ||
| friendly_json_value(start), | ||
| friendly_json_value(end) | ||
| ); | ||
| } | ||
| map.iter() | ||
| .map(|(key, value)| format!("{key}: {}", friendly_json_value(value))) | ||
| .collect::<Vec<_>>() | ||
| .join("; ") | ||
| } | ||
| } | ||
| } | ||
|
|
||
| fn empty_display_value(value: String) -> String { | ||
| if value.trim().is_empty() { | ||
| "(empty)".to_string() | ||
| } else { | ||
| value | ||
| } | ||
| } | ||
|
|
||
| fn status_for_action(action: &str) -> &'static str { | ||
| match action { | ||
| "Added" => "item-added", | ||
| "Removed" => "item-removed", | ||
| "Modified" => "item-changed", | ||
| _ => "", | ||
| } | ||
| } | ||
|
|
||
| fn change_operation_parts( | ||
| change: &serde_json::Value, | ||
| ) -> Option<( | ||
| &'static str, | ||
| Option<&serde_json::Value>, | ||
| Option<&serde_json::Value>, | ||
| )> { | ||
| if let Some(modify) = change.get("modify") { | ||
| return Some(("Modified", modify.get("old"), modify.get("new"))); | ||
| } | ||
| if let Some(add) = change.get("add") { | ||
| return Some(("Added", None, add.get("new").or(Some(add)))); | ||
| } | ||
| if let Some(remove) = change.get("remove") { | ||
| return Some(("Removed", remove.get("old").or(Some(remove)), None)); | ||
| } | ||
| match (change.get("old"), change.get("new")) { | ||
| (Some(old), Some(new)) => Some(("Modified", Some(old), Some(new))), | ||
| (None, Some(new)) => Some(("Added", None, Some(new))), | ||
| (Some(old), None) => Some(("Removed", Some(old), None)), | ||
| (None, None) => None, | ||
| } | ||
| } | ||
|
|
||
| fn change_scalar_row(field: &str, change: &serde_json::Value) -> Option<SubmissionChangeScalarRow> { | ||
| let (action, old, new) = change_operation_parts(change)?; | ||
| Some(SubmissionChangeScalarRow { | ||
| field: change_field_label(field), | ||
| action: action.to_string(), | ||
| previous_value: old | ||
| .map(friendly_json_value) | ||
| .map(empty_display_value) | ||
| .unwrap_or_default(), | ||
| current_value: new | ||
| .map(friendly_json_value) | ||
| .map(empty_display_value) | ||
| .unwrap_or_default(), | ||
| status_class: status_for_action(action).to_string(), | ||
| }) | ||
| } | ||
|
|
||
| fn change_multiline_row( | ||
| field: &str, | ||
| change: &serde_json::Value, | ||
| ) -> Option<SubmissionChangeMultilineRow> { | ||
| let (action, old, new) = change_operation_parts(change)?; | ||
| Some(SubmissionChangeMultilineRow { | ||
| field: change_field_label(field), | ||
| action: action.to_string(), | ||
| previous_value: old.map(friendly_json_value).unwrap_or_default(), | ||
| current_value: new.map(friendly_json_value).unwrap_or_default(), | ||
| status_class: status_for_action(action).to_string(), | ||
| }) | ||
| } | ||
|
|
||
| fn string_vec_values(value: Option<&serde_json::Value>) -> Vec<String> { | ||
| match value { | ||
| Some(serde_json::Value::Array(values)) => values | ||
| .iter() | ||
| .map(friendly_json_value) | ||
| .filter(|s| !s.trim().is_empty()) | ||
| .collect(), | ||
| Some(value) => { | ||
| let display = friendly_json_value(value); | ||
| if display.trim().is_empty() { | ||
| Vec::new() | ||
| } else { | ||
| vec![display] | ||
| } | ||
| } | ||
| None => Vec::new(), | ||
| } | ||
| } | ||
|
|
||
| fn change_set_row(field: &str, change: &serde_json::Value) -> Option<SubmissionChangeSetRow> { | ||
| let added = string_vec_values( | ||
| change | ||
| .get("add") | ||
| .and_then(|value| value.get("new")) | ||
| .or_else(|| change.get("add")), | ||
| ); | ||
| let removed = string_vec_values( | ||
| change | ||
| .get("remove") | ||
| .and_then(|value| value.get("old")) | ||
| .or_else(|| change.get("remove")), | ||
| ); | ||
| if added.is_empty() && removed.is_empty() { | ||
| None | ||
| } else { | ||
| Some(SubmissionChangeSetRow { | ||
| field: change_field_label(field), | ||
| added, | ||
| removed, | ||
| }) | ||
| } | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift
Humanize lookup-backed fields before using these helpers for the new change UI.
These paths stringify stored JSON directly, so system_code, media_type, regions, and languages will render as raw codes in both change_views and change_details. build_review_diff_context() already resolves those fields through review_named_value, display_region, and display_language, so this regresses the “human-readable” presentation the PR is introducing. Thread the same reference-data formatting through these helpers instead of formatting from raw JSON alone.
Also applies to: 777-849
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/routes/queue.rs` around lines 214 - 359, The new change-rendering helpers
in queue.rs are formatting raw JSON values, so lookup-backed fields like
system_code, media_type, regions, and languages will show codes instead of
human-readable labels. Update the helper flow around friendly_json_value,
change_scalar_row, change_multiline_row, and change_set_row to apply the same
reference-data formatting used by build_review_diff_context() via
review_named_value, display_region, and display_language before building
change_views/change_details rows. Keep the raw JSON parsing, but thread the
display conversion through these helpers so the new UI preserves human-readable
output.
| fn push_labeled_change_detail( | ||
| details: &mut Vec<SubmissionChangeDetail>, | ||
| field: &str, | ||
| current_value: String, | ||
| previous_value: String, | ||
| current_label: String, | ||
| previous_label: String, | ||
| current_kind: String, | ||
| previous_kind: String, | ||
| ) { | ||
| if current_value.trim().is_empty() && previous_value.trim().is_empty() { | ||
| return; | ||
| } | ||
| let is_multiline = current_value.contains('\n') || previous_value.contains('\n'); | ||
| details.push(SubmissionChangeDetail { |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Base is_multiline on field semantics, not just newline detection.
contents, cuesheet, dat, and the other review-sidecar fields are multiline in the existing review presentation even when the normalized value is a single line. With the current contains('\n') check, those history entries will fall back to inline rendering unless the text happens to contain a newline.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/routes/queue.rs` around lines 749 - 763, The multiline detection in
push_labeled_change_detail is too narrow because it only checks for newline
characters, so review-sidecar fields like contents, cuesheet, and dat may render
inline even though they should be multiline. Update the is_multiline decision to
also consider field semantics in this function, using the field identifier plus
the existing current_value/previous_value checks, so the known multiline fields
always get multiline presentation regardless of whether the normalized text
contains a newline.
Summary
Validation
Summary by CodeRabbit
New Features
Bug Fixes