Merge pull request #15907 from desktop/pull-push-dropdown

Pull-push-fetch button + dropdown
This commit is contained in:
Sergio Padrino 2023-01-23 09:44:07 +01:00 committed by GitHub
commit 924ab82c45
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 663 additions and 231 deletions

View file

@ -327,6 +327,7 @@ export enum FoldoutType {
Branch,
AppMenu,
AddMenu,
PushPull,
}
export type AppMenuFoldout = {
@ -358,6 +359,7 @@ export type Foldout =
| { type: FoldoutType.AddMenu }
| BranchFoldout
| AppMenuFoldout
| { type: FoldoutType.PushPull }
export enum RepositorySectionTab {
Changes,

View file

@ -122,3 +122,8 @@ export function enableStackedPopups(): boolean {
export function enablePreventClosingWhileUpdating(): boolean {
return enableBetaFeatures()
}
/** Should we enable the new push-pull-fetch dropdown? */
export function enablePushPullFetchDropdown(): boolean {
return enableBetaFeatures()
}

View file

@ -93,10 +93,7 @@ import { RepositoryStateCache } from '../lib/stores/repository-state-cache'
import { PopupType, Popup } from '../models/popup'
import { OversizedFiles } from './changes/oversized-files-warning'
import { PushNeedsPullWarning } from './push-needs-pull'
import {
ForcePushBranchState,
getCurrentBranchForcePushState,
} from '../lib/rebase'
import { getCurrentBranchForcePushState } from '../lib/rebase'
import { Banner, BannerType } from '../models/banner'
import { StashAndSwitchBranch } from './stash-changes/stash-and-switch-branch-dialog'
import { OverwriteStash } from './stash-changes/overwrite-stashed-changes-dialog'
@ -2800,9 +2797,15 @@ export class App extends React.Component<IAppProps, IAppState> {
remoteName = tip.branch.upstreamRemoteName
}
const isForcePush =
getCurrentBranchForcePushState(branchesState, aheadBehind) ===
ForcePushBranchState.Recommended
const currentFoldout = this.state.currentFoldout
const isDropdownOpen =
currentFoldout !== null && currentFoldout.type === FoldoutType.PushPull
const forcePushBranchState = getCurrentBranchForcePushState(
branchesState,
aheadBehind
)
return (
<PushPullButton
@ -2817,10 +2820,12 @@ export class App extends React.Component<IAppProps, IAppState> {
tipState={tip.kind}
pullWithRebase={pullWithRebase}
rebaseInProgress={rebaseInProgress}
isForcePush={isForcePush}
forcePushBranchState={forcePushBranchState}
shouldNudge={
this.state.currentOnboardingTutorialStep === TutorialStep.PushBranch
}
isDropdownOpen={isDropdownOpen}
onDropdownStateChanged={this.onPushPullDropdownStateChanged}
/>
)
}
@ -2884,6 +2889,14 @@ export class App extends React.Component<IAppProps, IAppState> {
this.props.dispatcher.openCreatePullRequestInBrowser(repository, branch)
}
private onPushPullDropdownStateChanged = (newState: DropdownState) => {
if (newState === 'open') {
this.props.dispatcher.showFoldout({ type: FoldoutType.PushPull })
} else {
this.props.dispatcher.closeFoldout(FoldoutType.PushPull)
}
}
private onBranchDropdownStateChanged = (newState: DropdownState) => {
if (newState === 'open') {
this.props.dispatcher.showFoldout({ type: FoldoutType.Branch })

View file

@ -183,7 +183,7 @@ export class BranchDropdown extends React.Component<
const isOpen = this.props.isOpen
const currentState: DropdownState = isOpen && canOpen ? 'open' : 'closed'
const buttonClassName = classNames('nudge-arrow', {
const buttonClassName = classNames('branch-toolbar-button', 'nudge-arrow', {
'nudge-arrow-up': this.props.shouldNudge,
})

View file

@ -13,7 +13,26 @@ import { TooltipTarget } from '../lib/tooltip'
export type DropdownState = 'open' | 'closed'
/** Represents the style of the dropdown */
export enum ToolbarDropdownStyle {
/**
* The dropdown is rendered as a single button and, when expanded, takes the
* full height of the window.
*/
Foldout,
/**
* The dropdown is rendered as two buttons: one is the toolbar button itself,
* and the other one is the expand/collapse button.
* When expanded, it only takes the height of the content.
*/
MultiOption,
}
export interface IToolbarDropdownProps {
/** The style of the dropdown. Default: Foldout */
readonly dropdownStyle?: ToolbarDropdownStyle
/** The primary button text, describing its function */
readonly title?: string
@ -74,6 +93,8 @@ export interface IToolbarDropdownProps {
*/
readonly onContextMenu?: (event: React.MouseEvent<HTMLButtonElement>) => void
readonly onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void
/**
* A function that's called whenever something is dragged over the
* dropdown.
@ -185,7 +206,8 @@ export class ToolbarDropdown extends React.Component<
IToolbarDropdownProps,
IToolbarDropdownState
> {
private innerButton: ToolbarButton | null = null
private innerButton = React.createRef<ToolbarButton>()
private rootDiv = React.createRef<HTMLDivElement>()
private focusTrapOptions: FocusTrapOptions
public constructor(props: IToolbarDropdownProps) {
@ -224,13 +246,25 @@ export class ToolbarDropdown extends React.Component<
}
const state = this.props.dropdownState
return (
const dropdownIcon = (
<Octicon symbol={this.dropdownIcon(state)} className="dropdownArrow" />
)
return this.props.dropdownStyle === ToolbarDropdownStyle.MultiOption ? (
<ToolbarButton
className="toolbar-dropdown-arrow-button"
onClick={this.onToggleDropdownClick}
>
{dropdownIcon}
</ToolbarButton>
) : (
dropdownIcon
)
}
private onClick = (event: React.MouseEvent<HTMLButtonElement>) => {
private onToggleDropdownClick = (
event: React.MouseEvent<HTMLButtonElement>
) => {
const newState: DropdownState =
this.props.dropdownState === 'open' ? 'closed' : 'open'
@ -247,13 +281,22 @@ export class ToolbarDropdown extends React.Component<
this.props.onDropdownStateChanged(newState, source)
}
private onMainButtonClick = (event: React.MouseEvent<HTMLButtonElement>) => {
if (this.props.dropdownStyle === ToolbarDropdownStyle.MultiOption) {
this.props.onClick?.(event)
return
}
this.onToggleDropdownClick(event)
}
private onContextMenu = (event: React.MouseEvent<HTMLButtonElement>) => {
this.props.onContextMenu?.(event)
}
private updateClientRectIfNecessary() {
if (this.props.dropdownState === 'open' && this.innerButton) {
const newRect = this.innerButton.getButtonBoundingClientRect()
if (this.props.dropdownState === 'open' && this.rootDiv.current) {
const newRect = this.rootDiv.current.getBoundingClientRect()
if (newRect) {
const currentRect = this.state.clientRect
@ -268,10 +311,6 @@ export class ToolbarDropdown extends React.Component<
this.updateClientRectIfNecessary()
}
public componentWillUnmount() {
this.innerButton = null
}
public componentDidUpdate() {
this.updateClientRectIfNecessary()
}
@ -305,12 +344,16 @@ export class ToolbarDropdown extends React.Component<
return undefined
}
const heightStyle: React.CSSProperties =
this.props.dropdownStyle === ToolbarDropdownStyle.MultiOption
? { maxHeight: '100%', width: rect.width }
: { height: '100%', minWidth: rect.width }
return {
position: 'absolute',
marginLeft: rect.left,
minWidth: rect.width,
height: '100%',
top: 0,
...heightStyle,
}
}
@ -354,22 +397,21 @@ export class ToolbarDropdown extends React.Component<
)
}
private onRef = (ref: ToolbarButton | null) => {
this.innerButton = ref
}
/**
* Programmatically move keyboard focus to the button element.
*/
public focusButton = () => {
if (this.innerButton) {
this.innerButton.focusButton()
if (this.innerButton.current) {
this.innerButton.current.focusButton()
}
}
public render() {
const className = classNames(
'toolbar-dropdown',
this.props.dropdownStyle === ToolbarDropdownStyle.MultiOption
? 'multi-option-style'
: 'foldout-style',
this.props.dropdownState,
this.props.className
)
@ -383,16 +425,17 @@ export class ToolbarDropdown extends React.Component<
role={this.props.role}
aria-expanded={ariaExpanded}
onDragOver={this.props.onDragOver}
ref={this.rootDiv}
>
{this.renderDropdownContents()}
<ToolbarButton
className={this.props.buttonClassName}
ref={this.onRef}
ref={this.innerButton}
icon={this.props.icon}
title={this.props.title}
description={this.props.description}
tooltip={this.props.tooltip}
onClick={this.onClick}
onClick={this.onMainButtonClick}
onContextMenu={this.onContextMenu}
onMouseEnter={this.props.onMouseEnter}
style={this.props.style}
@ -407,8 +450,11 @@ export class ToolbarDropdown extends React.Component<
isOverflowed={this.props.isOverflowed}
>
{this.props.children}
{this.renderDropdownArrow()}
{this.props.dropdownStyle !== ToolbarDropdownStyle.MultiOption &&
this.renderDropdownArrow()}
</ToolbarButton>
{this.props.dropdownStyle === ToolbarDropdownStyle.MultiOption &&
this.renderDropdownArrow()}
</div>
)
}

View file

@ -0,0 +1,123 @@
import React from 'react'
import { Button } from '../lib/button'
import { Octicon, syncClockwise } from '../octicons'
import {
DropdownItem,
DropdownItemClassName,
DropdownItemType,
forcePushIcon,
} from './push-pull-button'
interface IPushPullButtonDropDownProps {
readonly itemTypes: ReadonlyArray<DropdownItemType>
/** The name of the remote. */
readonly remoteName: string | null
readonly fetch: () => void
readonly forcePushWithLease: () => void
}
export class PushPullButtonDropDown extends React.Component<IPushPullButtonDropDownProps> {
private buttonsContainerRef: HTMLDivElement | null = null
public componentDidMount() {
window.addEventListener('keydown', this.onDropdownKeyDown)
}
public componentWillUnmount() {
window.removeEventListener('keydown', this.onDropdownKeyDown)
}
private onButtonsContainerRef = (ref: HTMLDivElement | null) => {
this.buttonsContainerRef = ref
}
private onDropdownKeyDown = (event: KeyboardEvent) => {
// Allow using Up and Down arrow keys to navigate the dropdown items
// (equivalent to Tab and Shift+Tab)
if (event.key !== 'ArrowDown' && event.key !== 'ArrowUp') {
return
}
event.preventDefault()
const items = this.buttonsContainerRef?.querySelectorAll<HTMLElement>(
`.${DropdownItemClassName}`
)
if (items === undefined) {
return
}
const focusedItem =
this.buttonsContainerRef?.querySelector<HTMLElement>(':focus')
if (!focusedItem) {
return
}
const focusedIndex = Array.from(items).indexOf(focusedItem)
const nextIndex =
event.key === 'ArrowDown' ? focusedIndex + 1 : focusedIndex - 1
// http://javascript.about.com/od/problemsolving/a/modulobug.htm
const nextItem = items[(nextIndex + items.length) % items.length]
nextItem?.focus()
}
private getDropdownItemWithType(type: DropdownItemType): DropdownItem {
const { remoteName } = this.props
switch (type) {
case DropdownItemType.Fetch:
return {
title: `Fetch ${remoteName}`,
description: `Fetch the latest changes from ${remoteName}`,
action: this.props.fetch,
icon: syncClockwise,
}
case DropdownItemType.ForcePush:
return {
title: `Force push ${remoteName}`,
description: (
<>
Overwrite any changes on {remoteName} with your local changes
<br />
<br />
<div className="warning">
<span className="warning-title">Warning:</span> A force push
will rewrite history on the remote. Any collaborators working on
this branch will need to reset their own local branch to match
the history of the remote.
</div>
</>
),
action: this.props.forcePushWithLease,
icon: forcePushIcon,
}
}
}
public renderDropdownItem = (type: DropdownItemType) => {
const item = this.getDropdownItemWithType(type)
return (
<Button
className={DropdownItemClassName}
key={type}
onClick={item.action}
>
<Octicon symbol={item.icon} />
<div className="text-container">
<div className="title">{item.title}</div>
<div className="detail">{item.description}</div>
</div>
</Button>
)
}
public render() {
const { itemTypes } = this.props
return (
<div className="push-pull-dropdown" ref={this.onButtonsContainerRef}>
{itemTypes.map(this.renderDropdownItem)}
</div>
)
}
}

View file

@ -13,6 +13,18 @@ import { RelativeTime } from '../relative-time'
import { ToolbarButton, ToolbarButtonStyle } from './button'
import classNames from 'classnames'
import {
DropdownState,
IToolbarDropdownProps,
ToolbarDropdown,
ToolbarDropdownStyle,
} from './dropdown'
import { FoldoutType } from '../../lib/app-state'
import { ForcePushBranchState } from '../../lib/rebase'
import { PushPullButtonDropDown } from './push-pull-button-dropdown'
import { enablePushPullFetchDropdown } from '../../lib/feature-flag'
export const DropdownItemClassName = 'push-pull-dropdown-item'
interface IPushPullButtonProps {
/**
@ -52,8 +64,8 @@ interface IPushPullButtonProps {
/** Is the detached HEAD state related to a rebase or not? */
readonly rebaseInProgress: boolean
/** If the current branch has been rebased, the user is permitted to force-push */
readonly isForcePush: boolean
/** Force push state of the current branch */
readonly forcePushBranchState: ForcePushBranchState
/** Whether this component should show its onboarding tutorial nudge arrow */
readonly shouldNudge: boolean
@ -62,6 +74,29 @@ interface IPushPullButtonProps {
* The number of tags that would get pushed if the user performed a push.
*/
readonly numTagsToPush: number
/** Whether or not the push-pull dropdown is currently open */
readonly isDropdownOpen: boolean
/**
* An event handler for when the drop down is opened, or closed, by a pointer
* event or by pressing the space or enter key while focused.
*
* @param state - The new state of the drop down
*/
readonly onDropdownStateChanged: (state: DropdownState) => void
}
export enum DropdownItemType {
Fetch = 'fetch',
ForcePush = 'force-push',
}
export type DropdownItem = {
readonly title: string
readonly description: string | JSX.Element
readonly action: () => void
readonly icon: OcticonSymbol.OcticonSymbolType
}
function renderAheadBehind(aheadBehind: IAheadBehind, numTagsToPush: number) {
@ -104,165 +139,11 @@ function renderLastFetched(lastFetched: Date | null): JSX.Element | string {
}
}
/** The common props for all button states */
const defaultProps = {
className: 'push-pull-button',
style: ToolbarButtonStyle.Subtitle,
}
function progressButton(progress: Progress, networkActionInProgress: boolean) {
return (
<ToolbarButton
{...defaultProps}
title={progress.title}
description={progress.description || 'Hang on…'}
progressValue={progress.value}
icon={syncClockwise}
iconClassName={networkActionInProgress ? 'spin' : ''}
tooltip={progress.description}
disabled={true}
/>
)
}
function publishRepositoryButton(onClick: () => void) {
return (
<ToolbarButton
{...defaultProps}
title="Publish repository"
description="Publish this repository to GitHub"
className="push-pull-button"
icon={OcticonSymbol.upload}
style={ToolbarButtonStyle.Subtitle}
onClick={onClick}
/>
)
}
function unbornRepositoryButton() {
return (
<ToolbarButton
{...defaultProps}
title="Publish branch"
description="Cannot publish unborn HEAD"
icon={OcticonSymbol.upload}
disabled={true}
/>
)
}
function detachedHeadButton(rebaseInProgress: boolean) {
const description = rebaseInProgress
? 'Rebase in progress'
: 'Cannot publish detached HEAD'
return (
<ToolbarButton
{...defaultProps}
title="Publish branch"
description={description}
icon={OcticonSymbol.upload}
disabled={true}
/>
)
}
function publishBranchButton(
isGitHub: boolean,
onClick: () => void,
shouldNudge: boolean
) {
const description = isGitHub
? 'Publish this branch to GitHub'
: 'Publish this branch to the remote'
const className = classNames(defaultProps.className, 'nudge-arrow', {
'nudge-arrow-up': shouldNudge,
})
return (
<ToolbarButton
{...defaultProps}
title="Publish branch"
description={description}
icon={OcticonSymbol.upload}
onClick={onClick}
className={className}
/>
)
}
function fetchButton(
remoteName: string,
aheadBehind: IAheadBehind,
numTagsToPush: number,
lastFetched: Date | null,
onClick: () => void
) {
const title = `Fetch ${remoteName}`
return (
<ToolbarButton
{...defaultProps}
title={title}
description={renderLastFetched(lastFetched)}
icon={syncClockwise}
onClick={onClick}
>
{renderAheadBehind(aheadBehind, numTagsToPush)}
</ToolbarButton>
)
}
function pullButton(
remoteName: string,
aheadBehind: IAheadBehind,
numTagsToPush: number,
lastFetched: Date | null,
pullWithRebase: boolean,
onClick: () => void
) {
const title = pullWithRebase
? `Pull ${remoteName} with rebase`
: `Pull ${remoteName}`
return (
<ToolbarButton
{...defaultProps}
title={title}
description={renderLastFetched(lastFetched)}
icon={OcticonSymbol.arrowDown}
onClick={onClick}
>
{renderAheadBehind(aheadBehind, numTagsToPush)}
</ToolbarButton>
)
}
function pushButton(
remoteName: string,
aheadBehind: IAheadBehind,
numTagsToPush: number,
lastFetched: Date | null,
onClick: () => void
) {
return (
<ToolbarButton
{...defaultProps}
title={`Push ${remoteName}`}
description={renderLastFetched(lastFetched)}
icon={OcticonSymbol.arrowUp}
onClick={onClick}
>
{renderAheadBehind(aheadBehind, numTagsToPush)}
</ToolbarButton>
)
}
/**
* This represents the "double arrow" icon used to show a force-push, and is a
* less complicated icon than the generated Octicon from the `octicons` package.
*/
const forcePushIcon: OcticonSymbol.OcticonSymbolType = {
export const forcePushIcon: OcticonSymbol.OcticonSymbolType = {
w: 10,
h: 16,
d:
@ -273,51 +154,80 @@ const forcePushIcon: OcticonSymbol.OcticonSymbolType = {
fr: 'evenodd',
}
function forcePushButton(
remoteName: string,
aheadBehind: IAheadBehind,
numTagsToPush: number,
lastFetched: Date | null,
onClick: () => void
) {
return (
<ToolbarButton
{...defaultProps}
title={`Force push ${remoteName}`}
description={renderLastFetched(lastFetched)}
icon={forcePushIcon}
onClick={onClick}
>
{renderAheadBehind(aheadBehind, numTagsToPush)}
</ToolbarButton>
)
}
/**
* A button which pushes, pulls, or updates depending on the state of the
* repository.
*/
export class PushPullButton extends React.Component<IPushPullButtonProps, {}> {
export class PushPullButton extends React.Component<IPushPullButtonProps> {
/** The common props for all button states */
private defaultButtonProps() {
return {
className: 'push-pull-button',
style: ToolbarButtonStyle.Subtitle,
}
}
/** The common props for all dropdown states */
private defaultDropdownProps(): Omit<
IToolbarDropdownProps,
'dropdownContentRenderer'
> {
return {
buttonClassName: 'push-pull-button',
style: ToolbarButtonStyle.Subtitle,
dropdownStyle: ToolbarDropdownStyle.MultiOption,
dropdownState: this.props.isDropdownOpen ? 'open' : 'closed',
onDropdownStateChanged: this.props.onDropdownStateChanged,
}
}
private closeDropdown() {
this.props.dispatcher.closeFoldout(FoldoutType.PushPull)
}
private push = () => {
this.closeDropdown()
this.props.dispatcher.push(this.props.repository)
}
private forcePushWithLease = () => {
this.closeDropdown()
this.props.dispatcher.confirmOrForcePush(this.props.repository)
}
private pull = () => {
this.closeDropdown()
this.props.dispatcher.pull(this.props.repository)
}
private fetch = () => {
this.closeDropdown()
this.props.dispatcher.fetch(
this.props.repository,
FetchType.UserInitiatedTask
)
}
private getDropdownContentRenderer(
itemTypes: ReadonlyArray<DropdownItemType>
) {
return () => {
return (
<PushPullButtonDropDown
itemTypes={itemTypes}
remoteName={this.props.remoteName}
fetch={this.fetch}
forcePushWithLease={this.forcePushWithLease}
/>
)
}
}
public render() {
return this.renderButton()
}
private renderButton() {
const {
progress,
networkActionInProgress,
@ -329,28 +239,28 @@ export class PushPullButton extends React.Component<IPushPullButtonProps, {}> {
rebaseInProgress,
lastFetched,
pullWithRebase,
isForcePush,
forcePushBranchState,
} = this.props
if (progress !== null) {
return progressButton(progress, networkActionInProgress)
return this.progressButton(progress, networkActionInProgress)
}
if (remoteName === null) {
return publishRepositoryButton(this.push)
return this.publishRepositoryButton(this.push)
}
if (tipState === TipState.Unborn) {
return unbornRepositoryButton()
return this.unbornRepositoryButton()
}
if (tipState === TipState.Detached) {
return detachedHeadButton(rebaseInProgress)
return this.detachedHeadButton(rebaseInProgress)
}
if (aheadBehind === null) {
const isGitHubRepository = repository.gitHubRepository !== null
return publishBranchButton(
return this.publishBranchButton(
isGitHubRepository,
this.push,
this.props.shouldNudge
@ -360,17 +270,11 @@ export class PushPullButton extends React.Component<IPushPullButtonProps, {}> {
const { ahead, behind } = aheadBehind
if (ahead === 0 && behind === 0 && numTagsToPush === 0) {
return fetchButton(
remoteName,
aheadBehind,
numTagsToPush,
lastFetched,
this.fetch
)
return this.fetchButton(remoteName, lastFetched, this.fetch)
}
if (isForcePush) {
return forcePushButton(
if (forcePushBranchState === ForcePushBranchState.Recommended) {
return this.forcePushButton(
remoteName,
aheadBehind,
numTagsToPush,
@ -380,17 +284,18 @@ export class PushPullButton extends React.Component<IPushPullButtonProps, {}> {
}
if (behind > 0) {
return pullButton(
return this.pullButton(
remoteName,
aheadBehind,
numTagsToPush,
lastFetched,
pullWithRebase || false,
forcePushBranchState,
this.pull
)
}
return pushButton(
return this.pushButton(
remoteName,
aheadBehind,
numTagsToPush,
@ -398,4 +303,254 @@ export class PushPullButton extends React.Component<IPushPullButtonProps, {}> {
this.push
)
}
private progressButton(progress: Progress, networkActionInProgress: boolean) {
return (
<ToolbarButton
{...this.defaultButtonProps()}
title={progress.title}
description={progress.description || 'Hang on…'}
progressValue={progress.value}
icon={syncClockwise}
iconClassName={networkActionInProgress ? 'spin' : ''}
tooltip={progress.description}
disabled={true}
/>
)
}
private publishRepositoryButton(onClick: () => void) {
return (
<ToolbarButton
{...this.defaultButtonProps()}
title="Publish repository"
description="Publish this repository to GitHub"
className="push-pull-button"
icon={OcticonSymbol.upload}
style={ToolbarButtonStyle.Subtitle}
onClick={onClick}
/>
)
}
private unbornRepositoryButton() {
return (
<ToolbarButton
{...this.defaultButtonProps()}
title="Publish branch"
description="Cannot publish unborn HEAD"
icon={OcticonSymbol.upload}
disabled={true}
/>
)
}
private detachedHeadButton(rebaseInProgress: boolean) {
const description = rebaseInProgress
? 'Rebase in progress'
: 'Cannot publish detached HEAD'
return (
<ToolbarButton
{...this.defaultButtonProps()}
title="Publish branch"
description={description}
icon={OcticonSymbol.upload}
disabled={true}
/>
)
}
private publishBranchButton(
isGitHub: boolean,
onClick: () => void,
shouldNudge: boolean
) {
const description = isGitHub
? 'Publish this branch to GitHub'
: 'Publish this branch to the remote'
if (!enablePushPullFetchDropdown()) {
const className = classNames(
this.defaultButtonProps().className,
'nudge-arrow',
{
'nudge-arrow-up': shouldNudge,
}
)
return (
<ToolbarButton
{...this.defaultButtonProps()}
title="Publish branch"
description={description}
icon={OcticonSymbol.upload}
onClick={onClick}
className={className}
/>
)
}
const className = classNames(
this.defaultDropdownProps().className,
'nudge-arrow',
{
'nudge-arrow-up': shouldNudge,
}
)
return (
<ToolbarDropdown
{...this.defaultDropdownProps()}
title="Publish branch"
description={description}
icon={OcticonSymbol.upload}
onClick={onClick}
className={className}
dropdownContentRenderer={this.getDropdownContentRenderer([
DropdownItemType.Fetch,
])}
/>
)
}
private fetchButton(
remoteName: string,
lastFetched: Date | null,
onClick: () => void
) {
const title = `Fetch ${remoteName}`
return (
<ToolbarButton
{...this.defaultButtonProps()}
title={title}
description={renderLastFetched(lastFetched)}
icon={syncClockwise}
onClick={onClick}
/>
)
}
private pullButton(
remoteName: string,
aheadBehind: IAheadBehind,
numTagsToPush: number,
lastFetched: Date | null,
pullWithRebase: boolean,
forcePushBranchState: ForcePushBranchState,
onClick: () => void
) {
const title = pullWithRebase
? `Pull ${remoteName} with rebase`
: `Pull ${remoteName}`
const dropdownItemTypes = [DropdownItemType.Fetch]
if (forcePushBranchState !== ForcePushBranchState.NotAvailable) {
dropdownItemTypes.push(DropdownItemType.ForcePush)
}
if (!enablePushPullFetchDropdown()) {
return (
<ToolbarButton
{...this.defaultButtonProps()}
title={title}
description={renderLastFetched(lastFetched)}
icon={OcticonSymbol.arrowDown}
onClick={onClick}
>
{renderAheadBehind(aheadBehind, numTagsToPush)}
</ToolbarButton>
)
}
return (
<ToolbarDropdown
{...this.defaultDropdownProps()}
title={title}
description={renderLastFetched(lastFetched)}
icon={OcticonSymbol.arrowDown}
onClick={onClick}
dropdownContentRenderer={this.getDropdownContentRenderer(
dropdownItemTypes
)}
>
{renderAheadBehind(aheadBehind, numTagsToPush)}
</ToolbarDropdown>
)
}
private pushButton(
remoteName: string,
aheadBehind: IAheadBehind,
numTagsToPush: number,
lastFetched: Date | null,
onClick: () => void
) {
if (!enablePushPullFetchDropdown()) {
return (
<ToolbarButton
{...this.defaultButtonProps()}
title={`Push ${remoteName}`}
description={renderLastFetched(lastFetched)}
icon={OcticonSymbol.arrowUp}
onClick={onClick}
>
{renderAheadBehind(aheadBehind, numTagsToPush)}
</ToolbarButton>
)
}
return (
<ToolbarDropdown
{...this.defaultDropdownProps()}
title={`Push ${remoteName}`}
description={renderLastFetched(lastFetched)}
icon={OcticonSymbol.arrowUp}
onClick={onClick}
dropdownContentRenderer={this.getDropdownContentRenderer([
DropdownItemType.Fetch,
])}
>
{renderAheadBehind(aheadBehind, numTagsToPush)}
</ToolbarDropdown>
)
}
private forcePushButton(
remoteName: string,
aheadBehind: IAheadBehind,
numTagsToPush: number,
lastFetched: Date | null,
onClick: () => void
) {
if (!enablePushPullFetchDropdown()) {
return (
<ToolbarButton
{...this.defaultButtonProps()}
title={`Force push ${remoteName}`}
description={renderLastFetched(lastFetched)}
icon={forcePushIcon}
onClick={onClick}
>
{renderAheadBehind(aheadBehind, numTagsToPush)}
</ToolbarButton>
)
}
return (
<ToolbarDropdown
{...this.defaultDropdownProps()}
title={`Force push ${remoteName}`}
description={renderLastFetched(lastFetched)}
icon={forcePushIcon}
onClick={onClick}
dropdownContentRenderer={this.getDropdownContentRenderer([
DropdownItemType.Fetch,
])}
>
{renderAheadBehind(aheadBehind, numTagsToPush)}
</ToolbarDropdown>
)
}
}

View file

@ -21,6 +21,7 @@
@import 'ui/toolbar/toolbar';
@import 'ui/toolbar/button';
@import 'ui/toolbar/dropdown';
@import 'ui/toolbar/push-pull-button';
@import 'ui/tab-bar';
@import 'ui/panel';
@import 'ui/popup';

View file

@ -248,6 +248,8 @@ $overlay-background-color: rgba(0, 0, 0, 0.4);
--toolbar-button-focus-progress-color: #{$gray-700};
--toolbar-button-hover-progress-color: #{$gray-700};
--toolbar-dropdown-open-progress-color: #{$gray-200};
--toolbar-dropdown-text-warning-color: #{$yellow-800};
--toolbar-dropdown-text-hover-color: var(--box-hover-text-color);
/**
* App menu bar colors (Windows/Linux only)

View file

@ -174,6 +174,8 @@ body.theme-dark {
--toolbar-button-focus-progress-color: #{$gray-700};
--toolbar-button-hover-progress-color: #{$gray-700};
--toolbar-dropdown-open-progress-color: #{$gray-200};
--toolbar-dropdown-text-warning-color: #{$yellow-700};
--toolbar-dropdown-text-hover-color: #{$white};
/**
* App menu bar colors (Windows/Linux only)

View file

@ -1,4 +1,8 @@
.toolbar-button {
display: flex;
flex-direction: row;
align-items: stretch;
// Make sure the contents shrink beyond their intrinsic width
// See https://css-tricks.com/flexbox-truncated-text/
min-width: 0;
@ -11,6 +15,11 @@
// above all the other content.
position: relative;
.toolbar-dropdown-button {
width: 40px;
height: 49px;
}
// General button behavior, mostly resets.
// For the button content styling see second button style. Note that we
// explicitly use > here to only target the direct descendant button since

View file

@ -3,13 +3,21 @@
// See https://css-tricks.com/flexbox-truncated-text/
min-width: 0;
display: flex;
flex-direction: row;
& > .toolbar-button {
width: 100%;
height: 100%;
}
& .toolbar-dropdown-arrow-button {
width: 39px;
}
&.open {
& > .toolbar-button > button {
&.foldout-style > .toolbar-button > button,
&.multi-option-style > .toolbar-dropdown-arrow-button > button {
color: var(--toolbar-button-active-color);
background-color: var(--toolbar-button-active-background-color);

View file

@ -0,0 +1,66 @@
.push-pull-dropdown {
display: flex;
flex-direction: column;
margin-top: 1px;
z-index: 0;
max-width: 100%;
.push-pull-dropdown-item {
display: flex;
flex-direction: row;
height: fit-content;
padding: 10px;
gap: 10px;
color: var(--text-color);
background-color: var(--box-background-color);
white-space: normal;
// Unset styles from Button component
text-align: unset;
border: unset;
border-radius: unset;
box-shadow: unset !important;
.octicon {
height: 16px;
width: 16px;
}
// Override background on focus to keep the default color
&:focus {
background-color: var(--box-background-color);
}
&:hover {
background-color: var(--box-hover-background-color) !important;
color: var(--toolbar-dropdown-text-hover-color) !important;
}
&:not(:last-child) {
// Enforce this bottom border style even in focused state
border-bottom: 1px solid var(--box-border-color) !important;
}
.text-container {
display: flex;
flex-direction: column;
row-gap: 3px;
.title {
font-weight: 600;
}
.detail {
color: var(--text-secondary-color);
.warning {
color: var(--toolbar-dropdown-text-warning-color);
.warning-title {
font-weight: 600;
}
}
}
}
}
}

View file

@ -37,16 +37,16 @@
}
}
.toolbar-dropdown {
&.branch-button {
.toolbar-button {
&.branch-toolbar-button {
width: 230px;
}
}
.toolbar-button {
&.revert-progress {
width: 230px;
}
&.toolbar-dropdown-arrow-button {
width: 39px;
}
}
}