crates/gunnar-ui-web/src/members.rs
//! **Members** on a repository's Settings tab (owner 2026-10-07, the members
//! model): the account's admin gives a member Read or Write on one of the
//! account's repositories, and takes it away.
//!
//! The server answers who the members are and what gunnar says they hold
//! (`GET /api/repos/{ns}/{name}/members`, asked as the person — gunnar answers
//! the account's admin and refuses anybody else, and then this panel draws
//! nothing); the buttons are `PUT` / `DELETE …/members/{id}`, signed as the
//! person, and gunnar decides. The page decides nothing.
//!
//! The words are here, on the host side of the wasm boundary (same argument
//! as [`crate::room`]); the panel is `pages::repo::MembersPanel`.
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Deserialize, PartialEq)]
pub struct Member {
pub id: String,
pub name: String,
pub repo_level: Option<String>,
pub account_level: Option<String>,
}
#[derive(Debug, Clone, Deserialize, PartialEq)]
pub struct Members {
pub account: String,
pub repo: String,
#[serde(default)]
pub members: Vec<Member>,
}
/// What the page sends: `read` or `write`.
#[derive(Serialize)]
pub struct Give<'a> {
pub level: &'a str,
}
/// What a member holds here, in words: the grant on this repository, else the
/// one on the whole account, else nothing.
pub fn holds(m: &Member, account: &str) -> String {
let cap = |w: &str| {
let mut c = w.chars();
c.next().map(|f| f.to_uppercase().collect::<String>() + c.as_str()).unwrap_or_default()
};
match (m.repo_level.as_deref(), m.account_level.as_deref()) {
(Some(repo), _) => format!("{} on this repository", cap(repo)),
(None, Some(all)) => format!("{} on all of {account}", cap(all)),
(None, None) => "Nothing".to_owned(),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn member(repo: Option<&str>, all: Option<&str>) -> Member {
Member { id: "ben".into(), name: "Ben".into(), repo_level: repo.map(Into::into), account_level: all.map(Into::into) }
}
/// What a member holds is said as gunnar's grant: this repository's
/// first, then the account's.
#[test]
fn a_member_holds_what_gunnar_says_here_first() {
assert_eq!(holds(&member(Some("write"), Some("read")), "acme"), "Write on this repository");
assert_eq!(holds(&member(None, Some("read")), "acme"), "Read on all of acme");
assert_eq!(holds(&member(None, None), "acme"), "Nothing");
}
}