A stored file on a record shows as its name and opens from the row
Test / test (pull_request) Successful in 1m16s

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GT37Z1Xtfd9pUuQtMTg6Yt
This commit is contained in:
Bendik Aagaard Lynghaug
2026-09-30 17:38:32 +02:00
co-authored by Claude Opus 5.5
parent a822f2175f
commit c766dedc0e
3 changed files with 46 additions and 0 deletions
+38
View File
@@ -1400,6 +1400,7 @@ fn AlternativeCard(
feature_name=feature_name.clone()
transitions=spec.transitions.clone()
link=spec.link.clone()
bucket=spec.bucket().map(str::to_string)
empty=spec.empty.clone()
pending_transitions=pending_transitions
transition_batch=transition_batch
@@ -1853,6 +1854,7 @@ fn ResourceFeature(
feature_name: String,
transitions: Vec<Transition>,
link: Option<RowLink>,
bucket: Option<String>,
empty: Option<String>,
pending_transitions: RwSignal<std::collections::HashMap<(String, String), String>>,
transition_batch: ServerAction<TransitionAnswers>,
@@ -1888,6 +1890,7 @@ fn ResourceFeature(
let feature_name = feature_name.clone();
let transitions = transitions.clone();
let link = link.clone();
let bucket = bucket.clone();
let empty = empty.clone().unwrap_or_else(|| t(&lang().get(), "nothing_here_yet").to_string());
data.get()
.map(|res| match res {
@@ -1897,6 +1900,7 @@ fn ResourceFeature(
feature_name=feature_name
transitions=transitions
link=link
bucket=bucket
empty=empty
value=value
pending_transitions=pending_transitions
@@ -1925,6 +1929,7 @@ fn ResourceValue(
feature_name: String,
transitions: Vec<Transition>,
link: Option<RowLink>,
bucket: Option<String>,
empty: String,
value: serde_json::Value,
pending_transitions: RwSignal<std::collections::HashMap<(String, String), String>>,
@@ -1957,12 +1962,14 @@ fn ResourceValue(
let feature_name = feature_name.clone();
let transitions = transitions.clone();
let link = link.clone();
let bucket = bucket.clone();
move |answer: Answer| {
view! {
<AnswerRow
feature_name=feature_name.clone()
transitions=transitions.clone()
link=link.clone()
bucket=bucket.clone()
answer=answer
pending_transitions=pending_transitions
transition_batch=transition_batch
@@ -2217,11 +2224,20 @@ fn SelectField(
}
}
/// The file's own name, if a field's value is an upload's key: a
/// uuid, a dash, and the name it was sent with (`upload.rs`).
pub fn uploaded_filename(value: &str) -> Option<&str> {
let (id, rest) = value.split_at_checked(36)?;
let shaped = id.char_indices().all(|(i, c)| if matches!(i, 8 | 13 | 18 | 23) { c == '-' } else { c.is_ascii_hexdigit() });
rest.strip_prefix('-').filter(|name| shaped && !name.is_empty())
}
#[component]
fn AnswerRow(
feature_name: String,
transitions: Vec<Transition>,
link: Option<RowLink>,
bucket: Option<String>,
answer: Answer,
pending_transitions: RwSignal<std::collections::HashMap<(String, String), String>>,
transition_batch: ServerAction<TransitionAnswers>,
@@ -2255,6 +2271,9 @@ fn AnswerRow(
let item_id = answer.id.clone();
let key = (feature_name.clone(), item_id.clone());
let link = link.map(|l| (l.href(&answer.id, &answer.responses), l.label));
// Where a stored file on this record is fetched from: the record
// names it, never the key (see `upload::attachment`).
let attachment_base = bucket.map(|b| format!("/attachment/{b}/{}", answer.id));
view! {
<article class="answer-row" class:decided=move || decided>
@@ -2288,6 +2307,17 @@ fn AnswerRow(
.into_any();
}
let text = value.as_str().map(str::to_string).unwrap_or_else(|| value.to_string());
// A stored file shows as its name, and opens.
if let (Some(base), Some(filename)) = (attachment_base.clone(), uploaded_filename(&text)) {
let href = format!("{base}/{key}");
return view! {
<div class="answer-field">
<span class="answer-field-key">{key}</span>
<a class="answer-field-value answer-file" href=href rel="external">{filename.to_string()}</a>
</div>
}
.into_any();
}
view! {
<div class="answer-field">
<span class="answer-field-key">{key}</span>
@@ -3028,6 +3058,14 @@ mod responsible_tests {
Responsible { name: "R".into(), contact: contact.into(), group: group.map(String::from) }
}
#[test]
fn an_uploads_key_shows_as_the_name_it_was_sent_with() {
assert_eq!(super::uploaded_filename("3f2b8c1e-9a4d-4e7b-8c21-0a1b2c3d4e5f-rapport 2026.pdf"), Some("rapport 2026.pdf"));
assert_eq!(super::uploaded_filename("rapport.pdf"), None);
assert_eq!(super::uploaded_filename("3f2b8c1e-9a4d-4e7b-8c21-0a1b2c3d4e5f-"), None);
assert_eq!(super::uploaded_filename("Storgata 1, 1850 Mysen - se vedlegg i e-post fra oss"), None);
}
#[test]
fn only_the_person_a_page_names_may_suggest_a_change_to_it() {
assert!(is_responsible(Some(&user(Some("kari@vel.no"), &[])), &named("mailto:Kari@Vel.no", None)));