Validate action targets; format timestamps client-side; clippy cleanup
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:
Bendik Aagaard Lynghaug
2026-08-12 23:05:23 +02:00
co-authored by Claude Sonnet 5
parent 6932816c42
commit 4d71b6ccd0
2 changed files with 47 additions and 7 deletions
+31 -5
View File
@@ -233,11 +233,8 @@ impl Requirement {
}
}
/// Extracts `scheme://host` from a repo's normal browser URL (e.g.
/// `https://project.uhhm.no/uhhm/questions` -> `https://project.uhhm.no`)
/// - 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.
/// Extracts `scheme://host` from a repo's normal browser URL - the
/// Gitea API base every helper in this module builds requests against.
#[cfg(feature = "ssr")]
pub fn gitea_api_base(repo_url: &str) -> anyhow::Result<String> {
let parsed = url::Url::parse(repo_url)
@@ -407,6 +404,16 @@ pub fn validate_questions(
) -> anyhow::Result<()> {
for question in questions.values() {
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(schema) = aggregates.get(&st.bucket) {
if !schema.has_state(&st.to) {
@@ -727,4 +734,23 @@ alternatives:
let err = validate_questions(&questions, &schema_fixture()).unwrap_err();
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());
}
}