update repo panel

This commit is contained in:
2026-08-27 14:52:50 +07:00
parent 932679311c
commit 1a8f96b3ba
8 changed files with 184 additions and 83 deletions
+3 -1
View File
@@ -1 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none"><path d="M20 9L13.4142 15.5858C12.6332 16.3668 11.3669 16.3668 10.5858 15.5858L4 9" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
<path d="M5.75 9.5L12 15.75L18.25 9.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 244 B

After

Width:  |  Height:  |  Size: 209 B

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
<circle cx="12" cy="12" r="5.25" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><path d="M6.5 12H1.75" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><path d="M22.25 12H17.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 357 B

+3
View File
@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
<path d="M20.255 17.05V10.35C20.255 9.78995 20.255 9.50992 20.146 9.29601C20.0501 9.10785 19.8972 8.95487 19.709 8.85899C19.4951 8.75 19.2151 8.75 18.655 8.75H5.35C4.78995 8.75 4.50992 8.75 4.29601 8.85899C4.10785 8.95487 3.95487 9.10785 3.85899 9.29601C3.75 9.50992 3.75 9.78995 3.75 10.35V17.05C3.75 18.1701 3.75 18.7302 3.96799 19.158C4.15973 19.5343 4.46569 19.8403 4.84202 20.032C5.26984 20.25 5.82989 20.25 6.95 20.25H17.055C18.1751 20.25 18.7352 20.25 19.163 20.032C19.5393 19.8403 19.8453 19.5343 20.037 19.158C20.255 18.7302 20.255 18.1701 20.255 17.05Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M5.75 8V3.75C5.75 3.19772 6.19772 2.75 6.75 2.75H9.67157C10.202 2.75 10.7107 2.96071 11.0858 3.33579L12.2071 4.45711C12.3946 4.64464 12.649 4.75 12.9142 4.75H17.25C17.8023 4.75 18.25 5.19772 18.25 5.75V8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M7.75 12.75L11.75 12.75" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

+3 -1
View File
@@ -1 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none"><path d="M12 5.25V12M12 12V18.75M12 12H5.25M12 12H18.75" stroke="black" stroke-width="1.5" stroke-linecap="round"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
<path d="M12 5.25V12M12 12V18.75M12 12H5.25M12 12H18.75" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 193 B

After

Width:  |  Height:  |  Size: 203 B

+4
View File
@@ -76,6 +76,8 @@ pub enum CustomIconName {
Filter,
GlobalOn,
GlobalOff,
GitFile,
GitCommit,
GitIssueDone,
GitIssueOpen,
GitIssueClosed,
@@ -98,6 +100,8 @@ impl IconNamed for CustomIconName {
CustomIconName::Filter => "icons/filter.svg",
CustomIconName::GlobalOn => "icons/global-on.svg",
CustomIconName::GlobalOff => "icons/global-off.svg",
CustomIconName::GitCommit => "icons/git-commit.svg",
CustomIconName::GitFile => "icons/git-file.svg",
CustomIconName::GitIssueDone => "icons/git-issue-done.svg",
CustomIconName::GitIssueOpen => "icons/git-issue-open.svg",
CustomIconName::GitIssueClosed => "icons/git-issue-close.svg",
@@ -1,7 +1,3 @@
//! Issues panel: a bottom panel listing every issue of the repository with
//! its title, event id, author, age and status, filterable by status via
//! the header's All/Open/Closed filter.
use std::rc::Rc;
use assets::CustomIconName;
@@ -43,8 +39,8 @@ enum IssueFilter {
All,
/// Issues whose resolved status is [`RepoStatus::Open`].
Open,
/// Issues whose resolved status is [`RepoStatus::Closed`] or
/// [`RepoStatus::Applied`] (both are "done" states).
/// Issues whose resolved status is
/// [`RepoStatus::Closed`] or [`RepoStatus::Applied`] (both are "done" states).
Closed,
}
@@ -74,8 +70,7 @@ pub struct IssuesView {
filter: IssueFilter,
/// Per-row heights of the virtual list.
item_sizes: Rc<Vec<Size<Pixels>>>,
/// Number of rows [`Self::item_sizes`] was built for (the filtered
/// issue count); rebuilt on change.
/// Number of rows [`Self::item_sizes`] was built for (the filtered issue count).
issue_len: usize,
/// Indices into the store's `issues` matching [`Self::filter`], rebuilt
/// every render; the virtual list renders this slice.
@@ -350,7 +345,7 @@ impl IssuesView {
/// Open the "new issue" dialog: a title and a content input that submit
/// through [`RepoStore::open_issue`] when confirmed.
fn open_new_issue_dialog(store: Entity<RepoStore>, window: &mut Window, cx: &mut App) {
pub(super) fn open_new_issue_dialog(store: Entity<RepoStore>, window: &mut Window, cx: &mut App) {
let subject = cx.new(|cx| InputState::new(window, cx).placeholder("Issue title"));
let content = cx.new(|cx| TextareaState::new(window, cx).placeholder("Describe the issue…"));
+159 -71
View File
@@ -8,12 +8,13 @@ use dock::{BasePanel, DockArea, DockPlacement, Panel, PanelEvent, panel_handle};
use gix::Repository;
use gpui::prelude::*;
use gpui::{
AnyElement, App, Context, Entity, EventEmitter, FocusHandle, Focusable, PathPromptOptions,
Pixels, Render, SharedString, Size, Subscription, Task, WeakEntity, Window, div, px, size,
Action, AnyElement, App, Context, Entity, EventEmitter, FocusHandle, Focusable,
PathPromptOptions, Pixels, Render, SharedString, Size, Subscription, Task, WeakEntity, Window,
div, px, relative, size,
};
use gpui_base::Disableable;
use gpui_component::avatar::{Avatar, AvatarGroup};
use gpui_component::button::{Button, ButtonVariants};
use gpui_base::{Button as BaseButton, Disableable};
use gpui_component::avatar::Avatar;
use gpui_component::button::{Button, ButtonVariants, DropdownButton};
use gpui_component::combobox::{
Caret, Combobox, ComboboxEvent, ComboboxState, ComboboxTriggerContext,
};
@@ -21,8 +22,7 @@ use gpui_component::searchable_list::SearchableVec;
use gpui_component::tag::Tag;
use gpui_component::tree::TreeState;
use gpui_component::{
ActiveTheme, Icon, IconName, Selectable, Sizable, StyledExt, VirtualListScrollHandle, h_flex,
v_flex,
ActiveTheme, Icon, IconName, Sizable, StyledExt, VirtualListScrollHandle, h_flex, v_flex,
};
use signed_core::Announcement;
use signed_git::{CommitList, FileCommit};
@@ -46,8 +46,8 @@ use browser::{
use commits::COMMIT_ROW_HEIGHT;
use diff::CommitDiffView;
use helpers::{TreeItemSeed, build_tree_items, is_markdown_path, tree_items};
use issues::IssuesView;
use pull_requests::PullRequestsView;
use issues::{IssuesView, open_new_issue_dialog};
use pull_requests::{PullRequestsView, open_new_pull_request_dialog};
/// What kind of ref the header selectors switch to.
#[derive(Clone, Copy, PartialEq, Eq)]
@@ -58,6 +58,16 @@ enum RefKind {
Tag,
}
/// Header actions dispatched by the dropdown menus of the header buttons.
#[derive(Clone, Action, PartialEq, Eq)]
#[action(namespace = repo_detail, no_json)]
enum RepoAction {
/// Open the "new issue" dialog.
NewIssue,
/// Open the "new pull request" dialog.
NewPR,
}
/// Everything loaded from the local clone for the explorer: the tree seeds,
/// README, refs and HEAD commit. Computed on a background thread (see
/// [`load_repo_data`]) and applied on the main thread.
@@ -1029,8 +1039,8 @@ impl RepoDetailView {
fn render_header(&self, cx: &mut Context<Self>) -> AnyElement {
let store = self.store.read(cx);
let announcement = store.announcement.as_ref().unwrap_or(&self.initial);
let issue_count = store.issue_count();
let pull_request_count = store.pull_request_count();
let issue_count = SharedString::from(store.issue_count().to_string());
let pr_count = SharedString::from(store.pull_request_count().to_string());
let name = self.display_name(cx);
let description = announcement.description();
@@ -1039,6 +1049,16 @@ impl RepoDetailView {
let worktree_empty = self.switching_ref || self.worktree.is_none();
v_flex()
.on_action(
cx.listener(|this, action: &RepoAction, window, cx| match action {
RepoAction::NewIssue => {
open_new_issue_dialog(this.store.clone(), window, cx);
}
RepoAction::NewPR => {
open_new_pull_request_dialog(this.store.clone(), window, cx);
}
}),
)
.px_4()
.pb_4()
.w_full()
@@ -1048,13 +1068,14 @@ impl RepoDetailView {
.child(
h_flex()
.w_full()
.gap_2()
.gap_4()
.items_start()
.justify_between()
.child(
v_flex()
.flex_1()
.min_w_0()
.gap_1()
.child(div().font_semibold().child(name))
.child(
div()
@@ -1062,6 +1083,7 @@ impl RepoDetailView {
.text_sm()
.text_color(cx.theme().muted_foreground)
.line_clamp(2)
.line_height(relative(1.2))
.text_ellipsis()
.child(description),
)
@@ -1086,36 +1108,59 @@ impl RepoDetailView {
.gap_2()
.justify_end()
.child(
Button::new("issues")
.child(
h_flex()
.gap_2()
.text_sm()
.child(SharedString::from("Issues"))
.child(Tag::new().xsmall().child(SharedString::from(
issue_count.to_string(),
))),
)
DropdownButton::new("issues")
.outline()
.on_click(cx.listener(|this, _event, window, cx| {
this.open_issue_detail(window, cx);
})),
.button(
Button::new("issues-open")
.child(
h_flex().gap_2().text_sm().child("Issues").child(
Tag::secondary()
.xsmall()
.border_0()
.child(issue_count),
),
)
.secondary()
.on_click(cx.listener(|this, _event, window, cx| {
this.open_issue_detail(window, cx);
})),
)
.dropdown_menu(|menu, _, _| {
menu.menu_element_with_icon(
IconName::Plus,
Box::new(RepoAction::NewIssue),
|_, _| div().text_xs().child("New issue"),
)
}),
)
.child(
Button::new("prs")
.child(
h_flex()
.gap_2()
.text_sm()
.child(SharedString::from("Pull Requests"))
.child(Tag::new().xsmall().child(SharedString::from(
pull_request_count.to_string(),
))),
)
DropdownButton::new("prs")
.outline()
.on_click(cx.listener(|this, _event, window, cx| {
this.open_pull_request_detail(window, cx);
})),
.button(
Button::new("prs-open")
.child(
h_flex()
.gap_2()
.text_sm()
.child("Pull Requests")
.child(
Tag::secondary()
.xsmall()
.border_0()
.child(pr_count),
),
)
.on_click(cx.listener(|this, _event, window, cx| {
this.open_pull_request_detail(window, cx);
})),
)
.dropdown_menu(|menu, _, _| {
menu.menu_element_with_icon(
IconName::Plus,
Box::new(RepoAction::NewPR),
|_, _| div().text_xs().child("New PR"),
)
}),
)
.child(
Button::new("link")
@@ -1141,27 +1186,69 @@ impl RepoDetailView {
.items_center()
.gap_2()
.child(
Button::new("files-tab")
.label("Files")
BaseButton::new("files-tab")
.flex()
.items_center()
.h_8()
.px_2()
.gap_2()
.child(
h_flex()
.gap_1()
.text_sm()
.child(Icon::new(CustomIconName::GitFile).small())
.child("Files"),
)
.text_color(cx.theme().button_foreground)
.rounded(cx.theme().radius)
.hover(|this| this.bg(cx.theme().button_hover))
.active(|this| this.bg(cx.theme().button_active))
.selected(self.active_tab == 0)
.toggled(self.active_tab == 0)
.when(self.active_tab == 0, |this| {
this.bg(cx.theme().button_active)
})
.on_click(cx.listener(|this, _event, _window, cx| {
this.active_tab = 0;
cx.notify();
})),
)
.child(
Button::new("commits-tab")
.label("Commits")
.selected(self.active_tab == 1)
.toggled(self.active_tab == 1)
BaseButton::new("commits-tab")
.flex()
.items_center()
.h_8()
.px_2()
.gap_2()
.child(
h_flex()
.gap_1()
.text_sm()
.child(Icon::new(CustomIconName::GitCommit).small())
.child("Commits"),
)
.when_some(commits_count, |this, count| {
this.child(
Tag::secondary()
.xsmall()
h_flex()
.justify_center()
.px_1()
.py_0p5()
.min_w_4()
.text_size(px(8.))
.bg(cx.theme().muted)
.text_color(cx.theme().muted_foreground)
.rounded(cx.theme().radius)
.line_height(relative(1.))
.child(SharedString::from(count.to_string())),
)
})
.text_color(cx.theme().button_foreground)
.rounded(cx.theme().radius)
.hover(|this| this.bg(cx.theme().button_hover))
.active(|this| this.bg(cx.theme().button_active))
.selected(self.active_tab == 1)
.when(self.active_tab == 1, |this| {
this.bg(cx.theme().button_active)
})
.on_click(cx.listener(|this, _event, _window, cx| {
this.active_tab = 1;
cx.notify();
@@ -1254,7 +1341,7 @@ impl RepoDetailView {
.maintainers
.iter()
.copied()
.filter(|key| *key != announcement.owner && seen.insert(*key))
.filter(|key| key != &announcement.owner && seen.insert(*key))
.collect();
let owner = profile_store.read(cx).get(&announcement.owner);
@@ -1264,31 +1351,32 @@ impl RepoDetailView {
h_flex()
.w_full()
.gap_3()
.items_center()
.child(
h_flex()
.gap_1()
.items_center()
.child(
Avatar::new()
.name(owner_name.clone())
.when_some(owner_picture, |this, url| this.src(url))
.rounded(cx.theme().radius)
.small(),
)
.child(div().text_xs().whitespace_nowrap().child(owner_name)),
Button::new("maintainers").ghost().child(
h_flex()
.gap_2()
.child(
h_flex()
.gap_1()
.child(
Avatar::new()
.name(owner_name.clone())
.when_some(owner_picture, |this, url| this.src(url))
.rounded(cx.theme().radius)
.small(),
)
.child(div().text_xs().whitespace_nowrap().child(owner_name)),
)
.when(!rest.is_empty(), |this| {
this.child(
div()
.text_xs()
.text_color(cx.theme().muted_foreground)
.child(SharedString::from(format!("+{}", rest.len()))),
)
}),
),
)
.when(!rest.is_empty(), |this| {
this.child(AvatarGroup::new().small().limit(5).ellipsis().children(
rest.into_iter().map(|key| {
let profile = profile_store.read(cx).get(&key);
Avatar::new()
.name(profile.name())
.when_some(profile.picture(), |this, url| this.src(url))
.rounded(cx.theme().radius)
}),
))
})
.into_any_element()
}
}
@@ -435,7 +435,11 @@ impl PullRequestsView {
/// Open the "new pull request" dialog: a title, an optional description and
/// a patch input that submit through [`RepoStore::open_pull_request`] when
/// confirmed.
fn open_new_pull_request_dialog(store: Entity<RepoStore>, window: &mut Window, cx: &mut App) {
pub(super) fn open_new_pull_request_dialog(
store: Entity<RepoStore>,
window: &mut Window,
cx: &mut App,
) {
let subject = cx.new(|cx| InputState::new(window, cx).placeholder("Pull request title"));
let description =
cx.new(|cx| TextareaState::new(window, cx).placeholder("Describe the change…"));