The header knows who is here; my cases; controls that scale #10
@@ -8,6 +8,7 @@ content repos pin the tag they run (see the end of this file).
|
||||
|
||||
- The header knows who is here: on a site with sign-in, a link top right to sign in, or the person's name and the way out. A page with a `slideshow` gets the wordmark as a bar of its own, the strip full width under it, and the question after.
|
||||
- "My cases": `where: { field: email, equals: "{user.email}" }` lists the records that carry the signed-in person's own address, filled from the session and never from the caller; nobody signed in matches nothing. An attachment is fetched only through a list that shows its record, so a list of my cases opens my files and nobody else's.
|
||||
- A stored file on a listed record shows as the name it was sent with and opens from the row, for whoever the list shows the record to. It used to show as its storage key, a string nobody could open.
|
||||
- Form controls scale with the text around them (em, not rem): fields, pills, buttons and labels follow a site's body size. The date picker's calendar icon is the size of the text beside it, and the file picker's button takes the field's colours instead of the browser's.
|
||||
- `KANIDM_GROUP_PREFIX`: a site on a Kanidm it shares with others keeps plain group names in its content; the prefix is put on when portal adds or removes a member, and the client's claim map takes it off.
|
||||
- Fixed: `maps` never reached the browser (`serde(skip)`), so a page with a map drew it on the server and dropped it on hydration.
|
||||
|
||||
+38
@@ -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)));
|
||||
|
||||
@@ -1269,6 +1269,13 @@ textarea:focus {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
/* A stored file on a record: its name, and it opens. */
|
||||
.answer-file {
|
||||
color: var(--accent);
|
||||
text-decoration: none;
|
||||
border-bottom: 0.06rem solid var(--accent);
|
||||
}
|
||||
|
||||
/* The page about a listed record, when the list offers one. */
|
||||
.answer-link {
|
||||
display: inline-block;
|
||||
|
||||
Reference in New Issue
Block a user