use axum::extract::{Path, Query, State}; use axum::response::Response; use maud::html; use crate::handler::html::{HtmlRepoPath, archived_class, resolve, response_html}; use crate::service::GitService; use crate::share::access::BasicAuth; use crate::share::validate::validate_revision; #[derive(Debug, Clone, serde::Deserialize, utoipa::IntoParams)] pub struct RefsDropdownQuery { #[serde(default)] pub revision: Option, } #[utoipa::path( get, path = "/repos/{space_name}/{repo_name}/html/refs-dropdown", tag = "html", description = "渲染分支与标签切换下拉框片段。", operation_id = "render_refs_dropdown", params(HtmlRepoPath, RefsDropdownQuery), responses( (status = 200, description = "refs dropdown html fragment", content_type = "text/html"), (status = 404, description = "repo not found") ) )] pub async fn render_refs_dropdown( State(service): State, Path(path): Path, Query(q): Query, auth: BasicAuth, ) -> Response { let ctx = match resolve(service.clone(), path, auth).await { Ok(c) => c, Err(resp) => return resp, }; let current_rev = q .revision .as_deref() .filter(|r| validate_revision(r).is_ok()) .unwrap_or(ctx.dto.default_branch.as_str()); let branches = service .list_branches(ctx.space_name.clone(), ctx.repo_name.clone()) .await .unwrap_or_default(); let tags = service .list_tags(ctx.space_name.clone(), ctx.repo_name.clone()) .await .unwrap_or_default(); let base = "git-refs-dropdown relative inline-flex w-full max-w-xs flex-col"; let cls = archived_class(&ctx.dto, base); let default = ctx.dto.default_branch.clone(); let space = ctx.space_name.clone(); let repo = ctx.repo_name.clone(); let is_custom_rev = !branches.iter().any(|b| b.name == current_rev) && !tags.iter().any(|t| t.name == current_rev) && current_rev != default; let markup = html! { div class=(cls) data-component="git-refs-dropdown" data-space=(space) data-repo=(repo) data-default-branch=(default) data-current-revision=(current_rev) data-archived=(if ctx.dto.is_archived { "true" } else { "false" }) { label class="mb-1 block text-xs font-medium text-zinc-600" { "Switch ref" } select class="w-full rounded border border-zinc-200 bg-white px-2 py-1 text-sm text-zinc-800" data-action="switch-ref" { @if is_custom_rev { option value=(current_rev) selected[true] { "Tree: " (current_rev) } } @if !branches.is_empty() { optgroup label="Branches" { @for b in &branches { @let is_sel = b.name == current_rev; @let is_def = b.name == default; option value=(b.name) selected[is_sel] { (b.name) (if is_def { " (default)" } else { "" }) } } } } @else { option value=(default) selected[true] { (default) " (default)" } } @if !tags.is_empty() { optgroup label="Tags" { @for t in &tags { @let is_sel = t.name == current_rev; option value=(t.name) selected[is_sel] { (t.name) } } } } } p class="mt-1 text-xs text-zinc-500" { (ctx.dto.branches_count) " branches · " (ctx.dto.tags_count) " tags" } } }; response_html(markup, 30) }