add send patch panel
This commit is contained in:
@@ -44,6 +44,7 @@ mod issues;
|
||||
mod new_pull_request;
|
||||
mod pull_request_detail;
|
||||
mod pull_requests;
|
||||
mod send_patch;
|
||||
|
||||
use about::open_about_dialog;
|
||||
use browser::{
|
||||
@@ -54,8 +55,10 @@ use commits::COMMIT_ROW_HEIGHT;
|
||||
use diff::CommitDiffView;
|
||||
use helpers::{ShareTargets, TreeItemSeed, build_tree_items, is_markdown_path, tree_items};
|
||||
use issues::{IssuesView, open_new_issue_dialog};
|
||||
use new_pull_request::open_new_pull_request_panel;
|
||||
use pull_requests::PullRequestsView;
|
||||
use send_patch::open_send_patch_panel;
|
||||
|
||||
use crate::views::repo_detail::new_pull_request::open_new_pull_panel;
|
||||
|
||||
/// What kind of ref the header selectors switch to.
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
@@ -67,13 +70,17 @@ enum RefKind {
|
||||
}
|
||||
|
||||
/// Header actions dispatched by the dropdown menus of the header buttons.
|
||||
/// `pub(super)`: the pull-request list panel offers the same New-PR / Send-
|
||||
/// patch actions in its own dropdown.
|
||||
#[derive(Clone, Action, PartialEq, Eq)]
|
||||
#[action(namespace = repo_detail, no_json)]
|
||||
enum RepoAction {
|
||||
pub(super) enum RepoAction {
|
||||
/// Open the "new issue" dialog.
|
||||
NewIssue,
|
||||
/// Open the "new pull request" dialog.
|
||||
NewPR,
|
||||
/// Open the "send patch" panel.
|
||||
SendPatch,
|
||||
/// Open the about dialog.
|
||||
About,
|
||||
/// Re-push the repository to its grasp servers.
|
||||
@@ -1363,13 +1370,12 @@ impl RepoDetailView {
|
||||
}
|
||||
RepoAction::NewPR => {
|
||||
if let Some(store) = this.store.clone() {
|
||||
open_new_pull_request_panel(
|
||||
this.dock_area.clone(),
|
||||
store,
|
||||
this.display_name(cx),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
open_new_pull_panel(this.dock_area.clone(), store, window, cx);
|
||||
}
|
||||
}
|
||||
RepoAction::SendPatch => {
|
||||
if let Some(store) = this.store.clone() {
|
||||
open_send_patch_panel(this.dock_area.clone(), store, window, cx);
|
||||
}
|
||||
}
|
||||
RepoAction::About => {
|
||||
@@ -1517,8 +1523,18 @@ impl RepoDetailView {
|
||||
.gap_2()
|
||||
.text_sm()
|
||||
.child(Icon::new(IconName::Plus))
|
||||
.child("New PR")
|
||||
.child("New Pull Request")
|
||||
})
|
||||
.menu_element(
|
||||
Box::new(RepoAction::SendPatch),
|
||||
|_, _| {
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.text_sm()
|
||||
.child(Icon::new(IconName::File))
|
||||
.child("Send Patch")
|
||||
},
|
||||
)
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
|
||||
@@ -93,10 +93,10 @@ impl NewPullRequestView {
|
||||
pub fn new(
|
||||
dock_area: WeakEntity<DockArea>,
|
||||
store: Entity<RepoStore>,
|
||||
repo_name: SharedString,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let repo_name = store.read(cx).name();
|
||||
let pane = cx.new(DiffPane::new);
|
||||
let subject = cx.new(|cx| InputState::new(window, cx).placeholder("Title"));
|
||||
let description = cx.new(|cx| TextareaState::new(window, cx).placeholder("Describe..."));
|
||||
@@ -796,16 +796,14 @@ fn render_ref_trigger(
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
/// Open the "new pull request" panel for `store` in the center dock.
|
||||
pub(super) fn open_new_pull_request_panel(
|
||||
/// Open the "new pull request" panel in the center dock.
|
||||
pub(super) fn open_new_pull_panel(
|
||||
dock_area: WeakEntity<DockArea>,
|
||||
store: Entity<RepoStore>,
|
||||
repo_name: SharedString,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
let panel =
|
||||
cx.new(|cx| NewPullRequestView::new(dock_area.clone(), store, repo_name, window, cx));
|
||||
let panel = cx.new(|cx| NewPullRequestView::new(dock_area.clone(), store, window, cx));
|
||||
|
||||
let _ = dock_area.update(cx, |dock_area, cx| {
|
||||
dock_area.add_panel_view(panel_handle(panel), DockPlacement::Center, None, window, cx);
|
||||
|
||||
@@ -7,18 +7,23 @@ use gpui::{
|
||||
AnyElement, App, Context, Entity, EventEmitter, FocusHandle, Focusable, Pixels, Render,
|
||||
SharedString, Size, WeakEntity, Window, div, px, size,
|
||||
};
|
||||
use gpui_base::Button as BaseButton;
|
||||
use gpui_component::alert::Alert;
|
||||
use gpui_component::scroll::Scrollbar;
|
||||
use gpui_component::{ActiveTheme, Icon, VirtualListScrollHandle, h_flex, v_flex, v_virtual_list};
|
||||
use gpui_component::{
|
||||
ActiveTheme, Icon, IconName, VirtualListScrollHandle, h_flex, v_flex, v_virtual_list,
|
||||
};
|
||||
use nostr::prelude::{EventId, Kind};
|
||||
use signed_core::{RepoStatus, activity_subject};
|
||||
use signed_state::{ProfileStore, RepoStore};
|
||||
use signed_ui::image_cache::{MAX_IMAGES, image_cache};
|
||||
use signed_ui::{SegmentButton, UserAvatar, placeholder, status_badge};
|
||||
use signed_ui::{DropdownButton, SegmentButton, UserAvatar, placeholder, status_badge};
|
||||
use utils::relative_time;
|
||||
|
||||
use super::new_pull_request::open_new_pull_request_panel;
|
||||
use super::RepoAction;
|
||||
use super::new_pull_request::open_new_pull_panel;
|
||||
use super::pull_request_detail::PullRequestDetailView;
|
||||
use super::send_patch::open_send_patch_panel;
|
||||
|
||||
/// Height of one pull request row in the virtual list; same layout as an
|
||||
/// issue row.
|
||||
@@ -36,8 +41,7 @@ enum PullRequestFilter {
|
||||
Closed,
|
||||
/// Pull requests whose resolved status is [`RepoStatus::Draft`].
|
||||
Draft,
|
||||
/// Pull requests whose resolved status is [`RepoStatus::Applied`]
|
||||
/// (i.e. merged).
|
||||
/// Pull requests whose resolved status is [`RepoStatus::Applied`].
|
||||
Merged,
|
||||
}
|
||||
|
||||
@@ -213,7 +217,7 @@ impl PullRequestsView {
|
||||
.child(
|
||||
h_flex()
|
||||
.h_12()
|
||||
.gap_2()
|
||||
.gap_1()
|
||||
.child(
|
||||
SegmentButton::new("all", "All")
|
||||
.icon(Icon::new(CustomIconName::GitPullRequest))
|
||||
@@ -267,18 +271,42 @@ impl PullRequestsView {
|
||||
)
|
||||
.child(div().flex_1())
|
||||
.child(
|
||||
SegmentButton::new("new-pr", "New pull request")
|
||||
.icon(Icon::new(CustomIconName::CirclePlus))
|
||||
.primary()
|
||||
.on_click(cx.listener(|this, _event, window, cx| {
|
||||
open_new_pull_request_panel(
|
||||
this.dock_area.clone(),
|
||||
this.store.clone(),
|
||||
this.repo_name.clone(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
})),
|
||||
h_flex().items_center().child(
|
||||
DropdownButton::new("new-pr-actions")
|
||||
.action(
|
||||
BaseButton::new("new-pr")
|
||||
.child(
|
||||
h_flex()
|
||||
.h_8()
|
||||
.px_2()
|
||||
.gap_1()
|
||||
.rounded(cx.theme().radius)
|
||||
.bg(cx.theme().primary)
|
||||
.hover(|this| this.bg(cx.theme().primary_hover))
|
||||
.text_sm()
|
||||
.text_color(cx.theme().primary_foreground)
|
||||
.child(Icon::new(IconName::Plus))
|
||||
.child("New"),
|
||||
)
|
||||
.on_click(cx.listener(|this, _event, window, cx| {
|
||||
open_new_pull_panel(
|
||||
this.dock_area.clone(),
|
||||
this.store.clone(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
})),
|
||||
)
|
||||
.dropdown_menu(|menu, _, _| {
|
||||
menu.menu_element(Box::new(RepoAction::SendPatch), |_, _| {
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.text_sm()
|
||||
.child(Icon::new(IconName::File))
|
||||
.child("Send Patch")
|
||||
})
|
||||
}),
|
||||
),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
@@ -367,6 +395,11 @@ impl Render for PullRequestsView {
|
||||
v_flex()
|
||||
.size_full()
|
||||
.image_cache(image_cache("pull-requests", MAX_IMAGES))
|
||||
.on_action(cx.listener(|this, action: &RepoAction, window, cx| {
|
||||
if action == &RepoAction::SendPatch {
|
||||
open_send_patch_panel(this.dock_area.clone(), this.store.clone(), window, cx);
|
||||
}
|
||||
}))
|
||||
.child(self.render_header(cx))
|
||||
.when_some(last_warning, |this, warning| {
|
||||
this.child(Alert::warning("pr-warning", warning).banner().on_close({
|
||||
|
||||
@@ -0,0 +1,265 @@
|
||||
use assets::CustomIconName;
|
||||
use dock::{BasePanel, DockArea, DockPlacement, Panel, PanelEvent, panel_handle};
|
||||
use gpui::prelude::*;
|
||||
use gpui::{
|
||||
AnyElement, App, Context, Entity, EventEmitter, FocusHandle, Focusable, Render, SharedString,
|
||||
Subscription, WeakEntity, Window, div, px,
|
||||
};
|
||||
use gpui_component::button::{Button, ButtonVariants};
|
||||
use gpui_component::input::{Input, InputEvent, InputState, Textarea, TextareaState};
|
||||
use gpui_component::{ActiveTheme, Disableable, Icon, IconName, Sizable, h_flex, v_flex};
|
||||
use signed_state::RepoStore;
|
||||
|
||||
pub struct SendPatchView {
|
||||
focus_handle: FocusHandle,
|
||||
/// Dock area the panel lives in.
|
||||
dock_area: WeakEntity<DockArea>,
|
||||
/// Store of the target repository.
|
||||
store: Entity<RepoStore>,
|
||||
/// Display name of the repository, for the panel title.
|
||||
repo_name: SharedString,
|
||||
/// Title input (required).
|
||||
subject: Entity<InputState>,
|
||||
/// Description input (optional).
|
||||
description: Entity<TextareaState>,
|
||||
/// The pasted `git format-patch` output (required).
|
||||
patch: Entity<TextareaState>,
|
||||
/// A submit is in flight.
|
||||
submitting: bool,
|
||||
/// Error of the last submit attempt (keeps the panel open).
|
||||
error: Option<SharedString>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
impl SendPatchView {
|
||||
pub fn new(
|
||||
dock_area: WeakEntity<DockArea>,
|
||||
store: Entity<RepoStore>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let repo_name = store.read(cx).name();
|
||||
let subject = cx.new(|cx| InputState::new(window, cx).placeholder("Title"));
|
||||
let description = cx
|
||||
.new(|cx| TextareaState::new(window, cx).placeholder("Describe the change (optional)"));
|
||||
let patch = cx.new(|cx| {
|
||||
TextareaState::new(window, cx).placeholder("Paste `git format-patch` output here...")
|
||||
});
|
||||
|
||||
// Re-evaluate the Send button's enabled state as the inputs change.
|
||||
let subscriptions = vec![
|
||||
cx.subscribe(&subject, |_this, _state, _event: &InputEvent, cx| {
|
||||
cx.notify();
|
||||
}),
|
||||
cx.subscribe(&patch, |_this, _state, _event: &InputEvent, cx| {
|
||||
cx.notify();
|
||||
}),
|
||||
];
|
||||
|
||||
Self {
|
||||
focus_handle: cx.focus_handle(),
|
||||
dock_area,
|
||||
store,
|
||||
repo_name,
|
||||
subject,
|
||||
description,
|
||||
patch,
|
||||
submitting: false,
|
||||
error: None,
|
||||
_subscriptions: subscriptions,
|
||||
}
|
||||
}
|
||||
|
||||
/// Publish the pull request from the pasted patch. The store validates
|
||||
/// synchronously (patch shape, per-part size, sign-in); on failure the
|
||||
/// panel stays open with the error inline, on success it closes — async
|
||||
/// publish failures surface in the pull request list's banner.
|
||||
fn submit(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if self.submitting {
|
||||
return;
|
||||
}
|
||||
let subject = self.subject.read(cx).value().to_string();
|
||||
let description = self.description.read(cx).value().to_string();
|
||||
let patch = self.patch.read(cx).value().to_string();
|
||||
if patch.is_empty() {
|
||||
return;
|
||||
}
|
||||
let store = self.store.clone();
|
||||
let dock_area = self.dock_area.clone();
|
||||
let entity = cx.entity().clone();
|
||||
|
||||
self.submitting = true;
|
||||
self.error = None;
|
||||
cx.notify();
|
||||
|
||||
// Errors the store detects before publishing are returned
|
||||
// synchronously through `last_error`.
|
||||
let sync_error = store.update(cx, |store, cx| {
|
||||
store.open_pull_request(
|
||||
(!subject.is_empty()).then_some(subject),
|
||||
description,
|
||||
None,
|
||||
patch,
|
||||
false,
|
||||
None,
|
||||
None,
|
||||
cx,
|
||||
);
|
||||
store.last_error.clone()
|
||||
});
|
||||
|
||||
if let Some(error) = sync_error {
|
||||
self.submitting = false;
|
||||
self.error = Some(error.into());
|
||||
cx.notify();
|
||||
return;
|
||||
}
|
||||
|
||||
// Close the panel once the publish is underway.
|
||||
cx.defer_in(window, {
|
||||
let dock_area = dock_area.clone();
|
||||
let entity = entity.clone();
|
||||
move |_, window, cx| {
|
||||
if let Some(dock_area) = dock_area.upgrade() {
|
||||
dock_area.update(cx, |dock, cx| {
|
||||
dock.remove_panel(entity, window, cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
/// Top bar: a short caption and the Send button.
|
||||
fn render_header(&self, cx: &mut Context<Self>) -> AnyElement {
|
||||
let can_submit = !self.submitting
|
||||
&& !self.subject.read(cx).value().is_empty()
|
||||
&& !self.patch.read(cx).value().is_empty();
|
||||
|
||||
h_flex()
|
||||
.px_4()
|
||||
.h_12()
|
||||
.w_full()
|
||||
.gap_2()
|
||||
.items_center()
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().border)
|
||||
.child(
|
||||
h_flex()
|
||||
.flex_1()
|
||||
.min_w_0()
|
||||
.gap_2()
|
||||
.items_center()
|
||||
.text_sm()
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child(Icon::new(IconName::FileText).small().flex_shrink_0())
|
||||
.child(
|
||||
div()
|
||||
.min_w_0()
|
||||
.overflow_hidden()
|
||||
.text_ellipsis()
|
||||
.whitespace_nowrap()
|
||||
.child("Send a patch from `git format-patch` output"),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
Button::new("send-patch")
|
||||
.icon(CustomIconName::CirclePlus)
|
||||
.label("Send patch")
|
||||
.primary()
|
||||
.loading(self.submitting)
|
||||
.disabled(!can_submit)
|
||||
.on_click(cx.listener(|this, _event, window, cx| {
|
||||
this.submit(window, cx);
|
||||
})),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
/// Title and description inputs.
|
||||
fn render_inputs(&self, cx: &mut Context<Self>) -> AnyElement {
|
||||
v_flex()
|
||||
.px_4()
|
||||
.py_2()
|
||||
.w_full()
|
||||
.gap_2()
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().border)
|
||||
.child(Input::new(&self.subject))
|
||||
.child(Textarea::new(&self.description).h(px(64.)))
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
/// The patch textarea, the main content of the panel.
|
||||
fn render_patch(&self, cx: &mut Context<Self>) -> AnyElement {
|
||||
v_flex()
|
||||
.px_4()
|
||||
.py_2()
|
||||
.w_full()
|
||||
.gap_2()
|
||||
.child(
|
||||
div()
|
||||
.text_xs()
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child("Patch — `git format-patch` output"),
|
||||
)
|
||||
.child(Textarea::new(&self.patch).h(px(240.)))
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
|
||||
/// Open the "send patch" panel for `store` in the center dock.
|
||||
pub(super) fn open_send_patch_panel(
|
||||
dock_area: WeakEntity<DockArea>,
|
||||
store: Entity<RepoStore>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
let panel = cx.new(|cx| SendPatchView::new(dock_area.clone(), store, window, cx));
|
||||
|
||||
let _ = dock_area.update(cx, |dock_area, cx| {
|
||||
dock_area.add_panel_view(panel_handle(panel), DockPlacement::Center, None, window, cx);
|
||||
});
|
||||
}
|
||||
|
||||
impl BasePanel for SendPatchView {
|
||||
fn panel_name(&self) -> &'static str {
|
||||
"send-patch"
|
||||
}
|
||||
}
|
||||
|
||||
impl Panel for SendPatchView {
|
||||
fn title(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||
div().child(SharedString::from(format!("{}/send-patch", self.repo_name)))
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<PanelEvent> for SendPatchView {}
|
||||
|
||||
impl Focusable for SendPatchView {
|
||||
fn focus_handle(&self, _cx: &App) -> FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for SendPatchView {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
v_flex()
|
||||
.id("send-patch")
|
||||
.size_full()
|
||||
.child(self.render_header(cx))
|
||||
.child(self.render_inputs(cx))
|
||||
.when_some(self.error.clone(), |this, error| {
|
||||
this.child(
|
||||
h_flex()
|
||||
.px_4()
|
||||
.py_1()
|
||||
.w_full()
|
||||
.text_xs()
|
||||
.text_color(cx.theme().danger)
|
||||
.child(error),
|
||||
)
|
||||
})
|
||||
.child(self.render_patch(cx))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user