Validate action targets; format timestamps client-side; clippy cleanup
Deploy / deploy (push) Successful in 1m0s
Deploy / deploy (push) Successful in 1m0s
- A dangling alternative.action (navigating to "Nothing here") is now a lint/load-time rejection like any other dead reference. - Rows re-rendered after a client-side refetch showed raw milliseconds - format_ms now formats via js_sys::Date under hydrate, matching the server's rendering. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
6932816c42
commit
4d71b6ccd0
+16
-2
@@ -492,7 +492,7 @@ fn AlternativeCard(
|
|||||||
for feature in &alternative.features {
|
for feature in &alternative.features {
|
||||||
for req in &feature.requirements {
|
for req in &feature.requirements {
|
||||||
if req.kind == "file" {
|
if req.kind == "file" {
|
||||||
file_refs.entry(req.name.clone()).or_insert_with(NodeRef::new);
|
file_refs.entry(req.name.clone()).or_default();
|
||||||
} else if req.kind == "select" {
|
} else if req.kind == "select" {
|
||||||
select_field_map
|
select_field_map
|
||||||
.entry(req.name.clone())
|
.entry(req.name.clone())
|
||||||
@@ -1335,7 +1335,21 @@ fn format_ms(ms: i64) -> String {
|
|||||||
.map(|t| t.format("%Y-%m-%d %H:%M UTC").to_string())
|
.map(|t| t.format("%Y-%m-%d %H:%M UTC").to_string())
|
||||||
.unwrap_or_else(|| ms.to_string())
|
.unwrap_or_else(|| ms.to_string())
|
||||||
}
|
}
|
||||||
#[cfg(not(feature = "ssr"))]
|
// Rows re-rendered client-side (a post-confirm refetch) format
|
||||||
|
// here, not on the server - raw milliseconds would show otherwise.
|
||||||
|
#[cfg(all(not(feature = "ssr"), feature = "hydrate"))]
|
||||||
|
{
|
||||||
|
let date = js_sys::Date::new(&wasm_bindgen::JsValue::from_f64(ms as f64));
|
||||||
|
format!(
|
||||||
|
"{:04}-{:02}-{:02} {:02}:{:02} UTC",
|
||||||
|
date.get_utc_full_year(),
|
||||||
|
date.get_utc_month() + 1,
|
||||||
|
date.get_utc_date(),
|
||||||
|
date.get_utc_hours(),
|
||||||
|
date.get_utc_minutes(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
#[cfg(all(not(feature = "ssr"), not(feature = "hydrate")))]
|
||||||
{
|
{
|
||||||
ms.to_string()
|
ms.to_string()
|
||||||
}
|
}
|
||||||
|
|||||||
+31
-5
@@ -233,11 +233,8 @@ impl Requirement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Extracts `scheme://host` from a repo's normal browser URL (e.g.
|
/// Extracts `scheme://host` from a repo's normal browser URL - the
|
||||||
/// `https://project.uhhm.no/uhhm/questions` -> `https://project.uhhm.no`)
|
/// Gitea API base every helper in this module builds requests against.
|
||||||
/// - the Gitea API base every helper in this module builds requests
|
|
||||||
/// against, including ones (like `resolve_gitea_repo`) that have
|
|
||||||
/// nothing to do with content loading, just the same Gitea instance.
|
|
||||||
#[cfg(feature = "ssr")]
|
#[cfg(feature = "ssr")]
|
||||||
pub fn gitea_api_base(repo_url: &str) -> anyhow::Result<String> {
|
pub fn gitea_api_base(repo_url: &str) -> anyhow::Result<String> {
|
||||||
let parsed = url::Url::parse(repo_url)
|
let parsed = url::Url::parse(repo_url)
|
||||||
@@ -407,6 +404,16 @@ pub fn validate_questions(
|
|||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
for question in questions.values() {
|
for question in questions.values() {
|
||||||
for alternative in &question.alternatives {
|
for alternative in &question.alternatives {
|
||||||
|
// A dangling action is a literal dead end: the submit
|
||||||
|
// button navigates to "Nothing here".
|
||||||
|
if let Some(action) = &alternative.action {
|
||||||
|
if !questions.contains_key(action) {
|
||||||
|
anyhow::bail!(
|
||||||
|
"question {:?} alternative {:?}: action {:?} does not match any declared question id",
|
||||||
|
question.id, alternative.name, action
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
if let Some(st) = &alternative.self_transition {
|
if let Some(st) = &alternative.self_transition {
|
||||||
if let Some(schema) = aggregates.get(&st.bucket) {
|
if let Some(schema) = aggregates.get(&st.bucket) {
|
||||||
if !schema.has_state(&st.to) {
|
if !schema.has_state(&st.to) {
|
||||||
@@ -727,4 +734,23 @@ alternatives:
|
|||||||
let err = validate_questions(&questions, &schema_fixture()).unwrap_err();
|
let err = validate_questions(&questions, &schema_fixture()).unwrap_err();
|
||||||
assert!(err.to_string().contains("not a declared edge"));
|
assert!(err.to_string().contains("not a declared edge"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dangling_action_is_rejected() {
|
||||||
|
let question: Question =
|
||||||
|
serde_yaml::from_str("id: /a\nname: A\nalternatives:\n - name: Go\n action: /nowhere\n")
|
||||||
|
.unwrap();
|
||||||
|
let questions = std::collections::HashMap::from([(question.id.clone(), question)]);
|
||||||
|
let err = validate_questions(&questions, &Default::default()).unwrap_err();
|
||||||
|
assert!(err.to_string().contains("does not match any declared question id"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn self_referencing_action_passes() {
|
||||||
|
let question: Question =
|
||||||
|
serde_yaml::from_str("id: /a\nname: A\nalternatives:\n - name: Go\n action: /a\n")
|
||||||
|
.unwrap();
|
||||||
|
let questions = std::collections::HashMap::from([(question.id.clone(), question)]);
|
||||||
|
assert!(validate_questions(&questions, &Default::default()).is_ok());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user