From 1a8f96b3bad10763fdf7c8473bee9840f9782699 Mon Sep 17 00:00:00 2001 From: Ren Amamiya Date: Thu, 27 Aug 2026 14:52:50 +0700 Subject: [PATCH] update repo panel --- crates/assets/assets/icons/chevron-down.svg | 4 +- crates/assets/assets/icons/git-commit.svg | 3 + crates/assets/assets/icons/git-file.svg | 3 + crates/assets/assets/icons/plus.svg | 4 +- crates/assets/src/lib.rs | 4 + .../workspace/src/views/repo_detail/issues.rs | 13 +- crates/workspace/src/views/repo_detail/mod.rs | 230 ++++++++++++------ .../src/views/repo_detail/pull_requests.rs | 6 +- 8 files changed, 184 insertions(+), 83 deletions(-) create mode 100644 crates/assets/assets/icons/git-commit.svg create mode 100644 crates/assets/assets/icons/git-file.svg diff --git a/crates/assets/assets/icons/chevron-down.svg b/crates/assets/assets/icons/chevron-down.svg index 7a0a041..36be93e 100644 --- a/crates/assets/assets/icons/chevron-down.svg +++ b/crates/assets/assets/icons/chevron-down.svg @@ -1 +1,3 @@ - \ No newline at end of file + + + diff --git a/crates/assets/assets/icons/git-commit.svg b/crates/assets/assets/icons/git-commit.svg new file mode 100644 index 0000000..931e995 --- /dev/null +++ b/crates/assets/assets/icons/git-commit.svg @@ -0,0 +1,3 @@ + + + diff --git a/crates/assets/assets/icons/git-file.svg b/crates/assets/assets/icons/git-file.svg new file mode 100644 index 0000000..c98599d --- /dev/null +++ b/crates/assets/assets/icons/git-file.svg @@ -0,0 +1,3 @@ + + + diff --git a/crates/assets/assets/icons/plus.svg b/crates/assets/assets/icons/plus.svg index 7e2e5b8..c4f4bd7 100644 --- a/crates/assets/assets/icons/plus.svg +++ b/crates/assets/assets/icons/plus.svg @@ -1 +1,3 @@ - \ No newline at end of file + + + diff --git a/crates/assets/src/lib.rs b/crates/assets/src/lib.rs index 3a16192..658af48 100644 --- a/crates/assets/src/lib.rs +++ b/crates/assets/src/lib.rs @@ -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", diff --git a/crates/workspace/src/views/repo_detail/issues.rs b/crates/workspace/src/views/repo_detail/issues.rs index 7a4e9a1..fda3ec6 100644 --- a/crates/workspace/src/views/repo_detail/issues.rs +++ b/crates/workspace/src/views/repo_detail/issues.rs @@ -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>>, - /// 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, window: &mut Window, cx: &mut App) { +pub(super) fn open_new_issue_dialog(store: Entity, 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…")); diff --git a/crates/workspace/src/views/repo_detail/mod.rs b/crates/workspace/src/views/repo_detail/mod.rs index dbad492..f9ff6cf 100644 --- a/crates/workspace/src/views/repo_detail/mod.rs +++ b/crates/workspace/src/views/repo_detail/mod.rs @@ -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) -> 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() } } diff --git a/crates/workspace/src/views/repo_detail/pull_requests.rs b/crates/workspace/src/views/repo_detail/pull_requests.rs index fc72b91..02c29d4 100644 --- a/crates/workspace/src/views/repo_detail/pull_requests.rs +++ b/crates/workspace/src/views/repo_detail/pull_requests.rs @@ -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, window: &mut Window, cx: &mut App) { +pub(super) fn open_new_pull_request_dialog( + store: Entity, + 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…"));