fix: wire edit forms for Datastar form submission and add CLI tests
Add {contentType: 'form'} to all edit template @put() calls so form
inputs are submitted by name rather than as Datastar signals. Replace
the quick notes text input on the brew edit page with toggle pill
buttons matching the add form. Add CLI tests for brew and cup update
commands.
This commit is contained in:
parent
5f99ca0953
commit
cb28a6f8bb
11 changed files with 167 additions and 21 deletions
|
|
@ -15,7 +15,7 @@
|
||||||
data-signals:_submit-error="''"
|
data-signals:_submit-error="''"
|
||||||
data-signals:_roast-date="'{{ roast_date }}'"
|
data-signals:_roast-date="'{{ roast_date }}'"
|
||||||
data-signals:_amount="{{ amount }}"
|
data-signals:_amount="{{ amount }}"
|
||||||
data-on:submit="$_submitting = true; $_submitError = ''; @put('/api/v1/bags/{{ id }}')"
|
data-on:submit="$_submitting = true; $_submitError = ''; @put('/api/v1/bags/{{ id }}', {contentType: 'form'})"
|
||||||
data-on:datastar-fetch="if (!$_submitting) return;
|
data-on:datastar-fetch="if (!$_submitting) return;
|
||||||
if (evt.detail.type === 'finished') { $_submitting = false; sessionStorage.setItem('toast', 'Bag updated'); window.location.href = evt.detail.response.headers.get('location') || '/bags/{{ id }}' }
|
if (evt.detail.type === 'finished') { $_submitting = false; sessionStorage.setItem('toast', 'Bag updated'); window.location.href = evt.detail.response.headers.get('location') || '/bags/{{ id }}' }
|
||||||
else if (evt.detail.type === 'error') { $_submitting = false; $_submitError = 'Failed to save changes.' }"
|
else if (evt.detail.type === 'error') { $_submitting = false; $_submitError = 'Failed to save changes.' }"
|
||||||
|
|
|
||||||
|
|
@ -19,8 +19,13 @@
|
||||||
data-signals:_water-volume="{{ water_volume }}"
|
data-signals:_water-volume="{{ water_volume }}"
|
||||||
data-signals:_water-temp="{{ water_temp }}"
|
data-signals:_water-temp="{{ water_temp }}"
|
||||||
data-signals:_brew-time="{{ brew_time }}"
|
data-signals:_brew-time="{{ brew_time }}"
|
||||||
data-signals:_quick-notes="'{{ quick_notes }}'"
|
data-signals:_qn-good="{% if quick_notes.contains("good") %}true{% else %}false{% endif %}"
|
||||||
data-on:submit="$_submitting = true; $_submitError = ''; @put('/api/v1/brews/{{ id }}')"
|
data-signals:_qn-too-fast="{% if quick_notes.contains("too-fast") %}true{% else %}false{% endif %}"
|
||||||
|
data-signals:_qn-too-slow="{% if quick_notes.contains("too-slow") %}true{% else %}false{% endif %}"
|
||||||
|
data-signals:_qn-too-hot="{% if quick_notes.contains("too-hot") %}true{% else %}false{% endif %}"
|
||||||
|
data-signals:_qn-under-extracted="{% if quick_notes.contains("under-extracted") %}true{% else %}false{% endif %}"
|
||||||
|
data-signals:_qn-over-extracted="{% if quick_notes.contains("over-extracted") %}true{% else %}false{% endif %}"
|
||||||
|
data-on:submit="$_submitting = true; $_submitError = ''; @put('/api/v1/brews/{{ id }}', {contentType: 'form'})"
|
||||||
data-on:datastar-fetch="if (!$_submitting) return;
|
data-on:datastar-fetch="if (!$_submitting) return;
|
||||||
if (evt.detail.type === 'finished') { $_submitting = false; sessionStorage.setItem('toast', 'Brew updated'); window.location.href = evt.detail.response.headers.get('location') || '/brews/{{ id }}' }
|
if (evt.detail.type === 'finished') { $_submitting = false; sessionStorage.setItem('toast', 'Brew updated'); window.location.href = evt.detail.response.headers.get('location') || '/brews/{{ id }}' }
|
||||||
else if (evt.detail.type === 'error') { $_submitting = false; $_submitError = 'Failed to save changes.' }"
|
else if (evt.detail.type === 'error') { $_submitting = false; $_submitError = 'Failed to save changes.' }"
|
||||||
|
|
@ -301,19 +306,55 @@
|
||||||
<!-- Quick Notes -->
|
<!-- Quick Notes -->
|
||||||
<div>
|
<div>
|
||||||
<h4 class="text-sm font-semibold text-text mb-3">Quick Notes</h4>
|
<h4 class="text-sm font-semibold text-text mb-3">Quick Notes</h4>
|
||||||
<label class="flex flex-col gap-1 text-sm">
|
<div class="flex flex-wrap gap-2">
|
||||||
<span
|
<button
|
||||||
class="text-xs font-semibold text-text-muted uppercase tracking-wide"
|
type="button"
|
||||||
>Notes (comma separated)</span
|
data-on:click="$_qnGood = !$_qnGood"
|
||||||
|
data-attr:class="$_qnGood ? 'pill pill-success cursor-pointer select-none transition' : 'pill pill-muted cursor-pointer select-none transition'"
|
||||||
>
|
>
|
||||||
|
Good
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
data-on:click="$_qnTooFast = !$_qnTooFast"
|
||||||
|
data-attr:class="$_qnTooFast ? 'pill pill-warning cursor-pointer select-none transition' : 'pill pill-muted cursor-pointer select-none transition'"
|
||||||
|
>
|
||||||
|
Too Fast
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
data-on:click="$_qnTooSlow = !$_qnTooSlow"
|
||||||
|
data-attr:class="$_qnTooSlow ? 'pill pill-warning cursor-pointer select-none transition' : 'pill pill-muted cursor-pointer select-none transition'"
|
||||||
|
>
|
||||||
|
Too Slow
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
data-on:click="$_qnTooHot = !$_qnTooHot"
|
||||||
|
data-attr:class="$_qnTooHot ? 'pill pill-warning cursor-pointer select-none transition' : 'pill pill-muted cursor-pointer select-none transition'"
|
||||||
|
>
|
||||||
|
Too Hot
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
data-on:click="$_qnUnderExtracted = !$_qnUnderExtracted"
|
||||||
|
data-attr:class="$_qnUnderExtracted ? 'pill pill-warning cursor-pointer select-none transition' : 'pill pill-muted cursor-pointer select-none transition'"
|
||||||
|
>
|
||||||
|
Under Extracted
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
data-on:click="$_qnOverExtracted = !$_qnOverExtracted"
|
||||||
|
data-attr:class="$_qnOverExtracted ? 'pill pill-warning cursor-pointer select-none transition' : 'pill pill-muted cursor-pointer select-none transition'"
|
||||||
|
>
|
||||||
|
Over Extracted
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="hidden"
|
||||||
name="quick_notes"
|
name="quick_notes"
|
||||||
class="input-field"
|
data-attr:value="[$_qnGood && 'good', $_qnTooFast && 'too-fast', $_qnTooSlow && 'too-slow', $_qnTooHot && 'too-hot', $_qnUnderExtracted && 'under-extracted', $_qnOverExtracted && 'over-extracted'].filter(Boolean).join(',')"
|
||||||
placeholder="good, too-fast, under-extracted"
|
|
||||||
data-bind:_quick-notes
|
|
||||||
/>
|
/>
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{ img::deferred_upload("edit-brew-image", "Brew Image") }}
|
{{ img::deferred_upload("edit-brew-image", "Brew Image") }}
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@
|
||||||
data-signals:_latitude="{{ latitude }}"
|
data-signals:_latitude="{{ latitude }}"
|
||||||
data-signals:_longitude="{{ longitude }}"
|
data-signals:_longitude="{{ longitude }}"
|
||||||
data-signals:_website="'{{ website }}'"
|
data-signals:_website="'{{ website }}'"
|
||||||
data-on:submit="$_submitting = true; $_submitError = ''; @put('/api/v1/cafes/{{ id }}')"
|
data-on:submit="$_submitting = true; $_submitError = ''; @put('/api/v1/cafes/{{ id }}', {contentType: 'form'})"
|
||||||
data-on:datastar-fetch="if (!$_submitting) return;
|
data-on:datastar-fetch="if (!$_submitting) return;
|
||||||
if (evt.detail.type === 'finished') { $_submitting = false; sessionStorage.setItem('toast', 'Cafe updated'); window.location.href = evt.detail.response.headers.get('location') || '/cafes/{{ id }}' }
|
if (evt.detail.type === 'finished') { $_submitting = false; sessionStorage.setItem('toast', 'Cafe updated'); window.location.href = evt.detail.response.headers.get('location') || '/cafes/{{ id }}' }
|
||||||
else if (evt.detail.type === 'error') { $_submitting = false; $_submitError = 'Failed to save changes.' }"
|
else if (evt.detail.type === 'error') { $_submitting = false; $_submitError = 'Failed to save changes.' }"
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
class="flex flex-col gap-4"
|
class="flex flex-col gap-4"
|
||||||
data-signals:_submitting="false"
|
data-signals:_submitting="false"
|
||||||
data-signals:_submit-error="''"
|
data-signals:_submit-error="''"
|
||||||
data-on:submit="$_submitting = true; $_submitError = ''; @put('/api/v1/cups/{{ id }}')"
|
data-on:submit="$_submitting = true; $_submitError = ''; @put('/api/v1/cups/{{ id }}', {contentType: 'form'})"
|
||||||
data-on:datastar-fetch="if (!$_submitting) return;
|
data-on:datastar-fetch="if (!$_submitting) return;
|
||||||
if (evt.detail.type === 'finished') { $_submitting = false; sessionStorage.setItem('toast', 'Cup updated'); window.location.href = evt.detail.response.headers.get('location') || '/cups/{{ id }}' }
|
if (evt.detail.type === 'finished') { $_submitting = false; sessionStorage.setItem('toast', 'Cup updated'); window.location.href = evt.detail.response.headers.get('location') || '/cups/{{ id }}' }
|
||||||
else if (evt.detail.type === 'error') { $_submitting = false; $_submitError = 'Failed to save changes.' }"
|
else if (evt.detail.type === 'error') { $_submitting = false; $_submitError = 'Failed to save changes.' }"
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
data-signals:_submit-error="''"
|
data-signals:_submit-error="''"
|
||||||
data-signals:_make="'{{ make }}'"
|
data-signals:_make="'{{ make }}'"
|
||||||
data-signals:_model="'{{ model }}'"
|
data-signals:_model="'{{ model }}'"
|
||||||
data-on:submit="$_submitting = true; $_submitError = ''; @put('/api/v1/gear/{{ id }}')"
|
data-on:submit="$_submitting = true; $_submitError = ''; @put('/api/v1/gear/{{ id }}', {contentType: 'form'})"
|
||||||
data-on:datastar-fetch="if (!$_submitting) return;
|
data-on:datastar-fetch="if (!$_submitting) return;
|
||||||
if (evt.detail.type === 'finished') { $_submitting = false; sessionStorage.setItem('toast', 'Gear updated'); window.location.href = evt.detail.response.headers.get('location') || '/gear/{{ id }}' }
|
if (evt.detail.type === 'finished') { $_submitting = false; sessionStorage.setItem('toast', 'Gear updated'); window.location.href = evt.detail.response.headers.get('location') || '/gear/{{ id }}' }
|
||||||
else if (evt.detail.type === 'error') { $_submitting = false; $_submitError = 'Failed to save changes.' }"
|
else if (evt.detail.type === 'error') { $_submitting = false; $_submitError = 'Failed to save changes.' }"
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@
|
||||||
data-signals:_producer="'{{ producer }}'"
|
data-signals:_producer="'{{ producer }}'"
|
||||||
data-signals:_process="'{{ process }}'"
|
data-signals:_process="'{{ process }}'"
|
||||||
data-signals:_tasting-notes="'{{ tasting_notes }}'"
|
data-signals:_tasting-notes="'{{ tasting_notes }}'"
|
||||||
data-on:submit="$_submitting = true; $_submitError = ''; @put('/api/v1/roasts/{{ id }}')"
|
data-on:submit="$_submitting = true; $_submitError = ''; @put('/api/v1/roasts/{{ id }}', {contentType: 'form'})"
|
||||||
data-on:datastar-fetch="if (!$_submitting) return;
|
data-on:datastar-fetch="if (!$_submitting) return;
|
||||||
if (evt.detail.type === 'finished') { $_submitting = false; sessionStorage.setItem('toast', 'Roast updated'); window.location.href = evt.detail.response.headers.get('location') || '/roasts/{{ id }}' }
|
if (evt.detail.type === 'finished') { $_submitting = false; sessionStorage.setItem('toast', 'Roast updated'); window.location.href = evt.detail.response.headers.get('location') || '/roasts/{{ id }}' }
|
||||||
else if (evt.detail.type === 'error') { $_submitting = false; $_submitError = 'Failed to save changes.' }"
|
else if (evt.detail.type === 'error') { $_submitting = false; $_submitError = 'Failed to save changes.' }"
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@
|
||||||
data-signals:_country="'{{ country }}'"
|
data-signals:_country="'{{ country }}'"
|
||||||
data-signals:_city="'{{ city }}'"
|
data-signals:_city="'{{ city }}'"
|
||||||
data-signals:_homepage="'{{ homepage }}'"
|
data-signals:_homepage="'{{ homepage }}'"
|
||||||
data-on:submit="$_submitting = true; $_submitError = ''; @put('/api/v1/roasters/{{ id }}')"
|
data-on:submit="$_submitting = true; $_submitError = ''; @put('/api/v1/roasters/{{ id }}', {contentType: 'form'})"
|
||||||
data-on:datastar-fetch="if (!$_submitting) return;
|
data-on:datastar-fetch="if (!$_submitting) return;
|
||||||
if (evt.detail.type === 'finished') { $_submitting = false; sessionStorage.setItem('toast', 'Roaster updated'); window.location.href = evt.detail.response.headers.get('location') || '/roasters/{{ id }}' }
|
if (evt.detail.type === 'finished') { $_submitting = false; sessionStorage.setItem('toast', 'Roaster updated'); window.location.href = evt.detail.response.headers.get('location') || '/roasters/{{ id }}' }
|
||||||
else if (evt.detail.type === 'error') { $_submitting = false; $_submitError = 'Failed to save changes.' }"
|
else if (evt.detail.type === 'error') { $_submitting = false; $_submitError = 'Failed to save changes.' }"
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
use crate::helpers::{
|
use crate::helpers::{
|
||||||
create_bag, create_gear, create_roast, create_roaster, create_token, run_brewlog,
|
create_bag, create_brew, create_gear, create_roast, create_roaster, create_token, run_brewlog,
|
||||||
};
|
};
|
||||||
|
use crate::test_macros::define_cli_auth_test;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn brew_add_creates_brew_via_api() {
|
fn brew_add_creates_brew_via_api() {
|
||||||
|
|
@ -235,3 +236,45 @@ fn brew_delete_removes_brew() {
|
||||||
"brew get should fail after deletion"
|
"brew get should fail after deletion"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
define_cli_auth_test!(
|
||||||
|
test_update_brew_requires_authentication,
|
||||||
|
&["brew", "update", "--id", "123", "--coffee-weight", "16.0"]
|
||||||
|
);
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_update_brew_with_authentication() {
|
||||||
|
let token = create_token("test-update-brew");
|
||||||
|
let roaster_id = create_roaster("Update Brew Roaster", &token);
|
||||||
|
let roast_id = create_roast(&roaster_id, "Update Brew Roast", &token);
|
||||||
|
let bag_id = create_bag(&roast_id, &token);
|
||||||
|
let grinder_id = create_gear("grinder", "Weber", "Key", &token);
|
||||||
|
let brewer_id = create_gear("brewer", "Origami", "Air", &token);
|
||||||
|
let brew_id = create_brew(&bag_id, &grinder_id, &brewer_id, &token);
|
||||||
|
|
||||||
|
let output = run_brewlog(
|
||||||
|
&[
|
||||||
|
"brew",
|
||||||
|
"update",
|
||||||
|
"--id",
|
||||||
|
&brew_id,
|
||||||
|
"--coffee-weight",
|
||||||
|
"16.0",
|
||||||
|
"--water-temp",
|
||||||
|
"94.0",
|
||||||
|
],
|
||||||
|
&[("BREWLOG_TOKEN", &token)],
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
output.status.success(),
|
||||||
|
"brew update should succeed: {}",
|
||||||
|
String::from_utf8_lossy(&output.stderr)
|
||||||
|
);
|
||||||
|
|
||||||
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
|
let brew: serde_json::Value = serde_json::from_str(&stdout).expect("Should output valid JSON");
|
||||||
|
|
||||||
|
assert_eq!(brew["coffee_weight"], 16.0);
|
||||||
|
assert_eq!(brew["water_temp"], 94.0);
|
||||||
|
}
|
||||||
|
|
|
||||||
36
tests/cli/cups_cli.rs
Normal file
36
tests/cli/cups_cli.rs
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
use crate::helpers::{
|
||||||
|
create_cafe, create_cup, create_roast, create_roaster, create_token, run_brewlog,
|
||||||
|
};
|
||||||
|
use crate::test_macros::define_cli_auth_test;
|
||||||
|
|
||||||
|
define_cli_auth_test!(
|
||||||
|
test_update_cup_requires_authentication,
|
||||||
|
&["cup", "update", "--id", "123", "--roast-id", "1"]
|
||||||
|
);
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_update_cup_with_authentication() {
|
||||||
|
let token = create_token("test-update-cup");
|
||||||
|
let roaster_id = create_roaster("Update Cup Roaster", &token);
|
||||||
|
let roast_id = create_roast(&roaster_id, "Update Cup Roast", &token);
|
||||||
|
let cafe_id = create_cafe("Update Cup Cafe", "London", "UK", "51.5", "-0.1", &token);
|
||||||
|
let cup_id = create_cup(&roast_id, &cafe_id, &token);
|
||||||
|
|
||||||
|
// Create a second roast to update the cup to
|
||||||
|
let roast_id_2 = create_roast(&roaster_id, "Update Cup Roast 2", &token);
|
||||||
|
|
||||||
|
let output = run_brewlog(
|
||||||
|
&["cup", "update", "--id", &cup_id, "--roast-id", &roast_id_2],
|
||||||
|
&[("BREWLOG_TOKEN", &token)],
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
output.status.success(),
|
||||||
|
"cup update should succeed: {}",
|
||||||
|
String::from_utf8_lossy(&output.stderr)
|
||||||
|
);
|
||||||
|
|
||||||
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
|
let cup: serde_json::Value = serde_json::from_str(&stdout).expect("Should output valid JSON");
|
||||||
|
assert!(cup["id"].is_i64(), "cup should have an id");
|
||||||
|
}
|
||||||
|
|
@ -316,3 +316,28 @@ pub fn create_gear(category: &str, make: &str, model: &str, token: &str) -> Stri
|
||||||
"gear",
|
"gear",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn create_brew(bag_id: &str, grinder_id: &str, brewer_id: &str, token: &str) -> String {
|
||||||
|
create_entity_cli(
|
||||||
|
&[
|
||||||
|
"brew",
|
||||||
|
"add",
|
||||||
|
"--bag-id",
|
||||||
|
bag_id,
|
||||||
|
"--grinder-id",
|
||||||
|
grinder_id,
|
||||||
|
"--brewer-id",
|
||||||
|
brewer_id,
|
||||||
|
],
|
||||||
|
token,
|
||||||
|
"brew",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn create_cup(roast_id: &str, cafe_id: &str, token: &str) -> String {
|
||||||
|
create_entity_cli(
|
||||||
|
&["cup", "add", "--roast-id", roast_id, "--cafe-id", cafe_id],
|
||||||
|
token,
|
||||||
|
"cup",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ pub mod backup_cli;
|
||||||
pub mod bags_cli;
|
pub mod bags_cli;
|
||||||
pub mod brews_cli;
|
pub mod brews_cli;
|
||||||
pub mod cafes_cli;
|
pub mod cafes_cli;
|
||||||
|
pub mod cups_cli;
|
||||||
pub mod gear_cli;
|
pub mod gear_cli;
|
||||||
pub mod helpers;
|
pub mod helpers;
|
||||||
pub mod roasters_cli;
|
pub mod roasters_cli;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue