fix: add an action field to timeline events

This commit is contained in:
Jon Seager 2025-11-27 14:52:13 +00:00
parent 144257c2c8
commit f750107bb1
No known key found for this signature in database
8 changed files with 22 additions and 20 deletions

View file

@ -0,0 +1 @@
ALTER TABLE timeline_events ADD COLUMN action TEXT NOT NULL DEFAULT 'added';

View file

@ -139,6 +139,7 @@ pub(crate) async fn create_bag(
let event = NewTimelineEvent { let event = NewTimelineEvent {
entity_type: "bag".to_string(), entity_type: "bag".to_string(),
entity_id: bag.id.into_inner(), entity_id: bag.id.into_inner(),
action: "added".to_string(),
occurred_at: chrono::Utc::now(), occurred_at: chrono::Utc::now(),
title: roast.name.to_string(), title: roast.name.to_string(),
details: vec![ details: vec![
@ -235,6 +236,7 @@ pub(crate) async fn update_bag(
let event = NewTimelineEvent { let event = NewTimelineEvent {
entity_type: "bag".to_string(), entity_type: "bag".to_string(),
entity_id: bag.id.into_inner(), entity_id: bag.id.into_inner(),
action: "finished".to_string(),
occurred_at: chrono::Utc::now(), occurred_at: chrono::Utc::now(),
title: format!("{}", roast.name), title: format!("{}", roast.name),
details: vec![TimelineEventDetail { details: vec![TimelineEventDetail {

View file

@ -15,6 +15,7 @@ pub struct TimelineEvent {
pub id: TimelineEventId, pub id: TimelineEventId,
pub entity_type: String, pub entity_type: String,
pub entity_id: i64, pub entity_id: i64,
pub action: String,
pub occurred_at: DateTime<Utc>, pub occurred_at: DateTime<Utc>,
pub title: String, pub title: String,
pub details: Vec<TimelineEventDetail>, pub details: Vec<TimelineEventDetail>,
@ -27,6 +28,7 @@ pub struct TimelineEvent {
pub struct NewTimelineEvent { pub struct NewTimelineEvent {
pub entity_type: String, pub entity_type: String,
pub entity_id: i64, pub entity_id: i64,
pub action: String,
pub occurred_at: DateTime<Utc>, pub occurred_at: DateTime<Utc>,
pub title: String, pub title: String,
pub details: Vec<TimelineEventDetail>, pub details: Vec<TimelineEventDetail>,

View file

@ -130,10 +130,11 @@ impl RoasterRepository for SqlRoasterRepository {
let details_json = Self::details_for_roaster(&roaster)?; let details_json = Self::details_for_roaster(&roaster)?;
query( query(
"INSERT INTO timeline_events (entity_type, entity_id, occurred_at, title, details_json, tasting_notes_json) VALUES (?, ?, ?, ?, ?, ?)", "INSERT INTO timeline_events (entity_type, entity_id, action, occurred_at, title, details_json, tasting_notes_json) VALUES (?, ?, ?, ?, ?, ?, ?)",
) )
.bind("roaster") .bind("roaster")
.bind(i64::from(roaster.id)) .bind(i64::from(roaster.id))
.bind("added")
.bind(roaster.created_at) .bind(roaster.created_at)
.bind(&roaster.name) .bind(&roaster.name)
.bind(details_json) .bind(details_json)

View file

@ -166,10 +166,11 @@ impl RoastRepository for SqlRoastRepository {
}; };
query( query(
"INSERT INTO timeline_events (entity_type, entity_id, occurred_at, title, details_json, tasting_notes_json) VALUES (?, ?, ?, ?, ?, ?)", "INSERT INTO timeline_events (entity_type, entity_id, action, occurred_at, title, details_json, tasting_notes_json) VALUES (?, ?, ?, ?, ?, ?, ?)",
) )
.bind("roast") .bind("roast")
.bind(i64::from(roast.id)) .bind(i64::from(roast.id))
.bind("added")
.bind(roast.created_at) .bind(roast.created_at)
.bind(&roast.name) .bind(&roast.name)
.bind(details_json) .bind(details_json)

View file

@ -25,9 +25,9 @@ impl SqlTimelineEventRepository {
impl TimelineEventRepository for SqlTimelineEventRepository { impl TimelineEventRepository for SqlTimelineEventRepository {
async fn insert(&self, event: NewTimelineEvent) -> Result<TimelineEvent, RepositoryError> { async fn insert(&self, event: NewTimelineEvent) -> Result<TimelineEvent, RepositoryError> {
let query = r#" let query = r#"
INSERT INTO timeline_events (entity_type, entity_id, occurred_at, title, details_json, tasting_notes_json) INSERT INTO timeline_events (entity_type, entity_id, action, occurred_at, title, details_json, tasting_notes_json)
VALUES (?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?)
RETURNING id, entity_type, entity_id, occurred_at, title, details_json, tasting_notes_json RETURNING id, entity_type, entity_id, action, occurred_at, title, details_json, tasting_notes_json
"#; "#;
let details_json = serde_json::to_string(&event.details).map_err(|err| { let details_json = serde_json::to_string(&event.details).map_err(|err| {
@ -43,6 +43,7 @@ impl TimelineEventRepository for SqlTimelineEventRepository {
let record = sqlx::query_as::<_, TimelineEventRecord>(query) let record = sqlx::query_as::<_, TimelineEventRecord>(query)
.bind(event.entity_type) .bind(event.entity_type)
.bind(event.entity_id) .bind(event.entity_id)
.bind(event.action)
.bind(event.occurred_at) .bind(event.occurred_at)
.bind(event.title) .bind(event.title)
.bind(details_json) .bind(details_json)
@ -65,7 +66,7 @@ impl TimelineEventRepository for SqlTimelineEventRepository {
let order_clause = format!("t.occurred_at {direction_sql}, t.id DESC"); let order_clause = format!("t.occurred_at {direction_sql}, t.id DESC");
let base_query = "SELECT let base_query = "SELECT
t.id, t.entity_type, t.entity_id, t.occurred_at, t.title, t.details_json, t.tasting_notes_json, t.id, t.entity_type, t.entity_id, t.action, t.occurred_at, t.title, t.details_json, t.tasting_notes_json,
CASE CASE
WHEN t.entity_type = 'roaster' THEN r.slug WHEN t.entity_type = 'roaster' THEN r.slug
WHEN t.entity_type = 'roast' THEN rst.slug WHEN t.entity_type = 'roast' THEN rst.slug
@ -103,6 +104,7 @@ struct TimelineEventRecord {
id: i64, id: i64,
entity_type: String, entity_type: String,
entity_id: i64, entity_id: i64,
action: String,
occurred_at: DateTime<Utc>, occurred_at: DateTime<Utc>,
title: String, title: String,
details_json: Option<String>, details_json: Option<String>,
@ -137,6 +139,7 @@ impl TimelineEventRecord {
id: TimelineEventId::from(self.id), id: TimelineEventId::from(self.id),
entity_type: self.entity_type, entity_type: self.entity_type,
entity_id: self.entity_id, entity_id: self.entity_id,
action: self.action,
occurred_at: self.occurred_at, occurred_at: self.occurred_at,
title: self.title, title: self.title,
details, details,

View file

@ -440,6 +440,7 @@ impl TimelineEventView {
id, id,
entity_type, entity_type,
entity_id, entity_id,
action,
occurred_at, occurred_at,
title, title,
details, details,
@ -448,16 +449,11 @@ impl TimelineEventView {
roaster_slug, roaster_slug,
} = event; } = event;
let kind_label = match entity_type.as_str() { let kind_label = match (entity_type.as_str(), action.as_str()) {
"roaster" => "Roaster Added", ("roaster", "added") => "Roaster Added",
"roast" => "Roast Added", ("roast", "added") => "Roast Added",
"bag" => { ("bag", "added") => "Bag Added",
if title.starts_with("Finished") { ("bag", "finished") => "Bag Finished",
"Bag Finished"
} else {
"Bag Added"
}
}
_ => "Event", _ => "Event",
}; };

View file

@ -410,8 +410,4 @@ async fn closing_a_bag_surfaces_on_the_timeline() {
body.contains("Bag Finished"), body.contains("Bag Finished"),
"Expected 'Bag Finished' badge in timeline HTML, got: {body}" "Expected 'Bag Finished' badge in timeline HTML, got: {body}"
); );
assert!(
body.contains(&format!("Finished: {}", roast_name)),
"Expected bag finished title to appear in timeline HTML, got: {body}"
);
} }