Merge pull request #709 from desktop/the-component-revolution

The People's Glorious Component Revolution
This commit is contained in:
Markus Olsson 2016-12-06 11:40:00 +01:00 committed by GitHub
commit 92ddb95a3b
41 changed files with 501 additions and 300 deletions

View file

@ -3,6 +3,10 @@ import * as React from 'react'
import { Dispatcher } from '../../lib/dispatcher'
import { initGitRepository, isGitRepository } from '../../lib/git'
import { Button } from '../lib/button'
import { Form } from '../lib/form'
import { TextBox } from '../lib/text-box'
import { Row } from '../lib/row'
const untildify: (str: string) => string = require('untildify')
@ -28,27 +32,21 @@ export class AddExistingRepository extends React.Component<IAddExistingRepositor
public render() {
const disabled = this.state.path.length === 0 || this.state.isGitRepository == null
return (
<div id='add-existing-repository'>
<div className='add-repo-form'>
<label>Local Path</label>
<Form onSubmit={this.addRepository}>
<Row>
<TextBox
value={this.state.path}
label='Local Path'
placeholder='repository path'
onChange={this.onPathChanged}
onKeyDown={this.onKeyDown}/>
<Button onClick={this.showFilePicker}>Choose</Button>
</Row>
<div className='file-picker'>
<input value={this.state.path}
type='text'
placeholder='repository path'
onChange={this.onPathChanged}
onKeyDown={this.onKeyDown}/>
<button onClick={this.showFilePicker}>Choose</button>
</div>
</div>
<div className='popup-actions'>
<button disabled={disabled} onClick={this.addRepository}>
{this.state.isGitRepository ? 'Add Repository' : 'Create & Add Repository'}
</button>
</div>
</div>
<Button disabled={disabled} type='submit'>
{this.state.isGitRepository ? 'Add Repository' : 'Create & Add Repository'}
</Button>
</Form>
)
}

View file

@ -7,6 +7,10 @@ import * as FS from 'fs'
import { Dispatcher } from '../../lib/dispatcher'
import { initGitRepository } from '../../lib/git'
import { sanitizedRepositoryName } from './sanitized-repository-name'
import { Form } from '../lib/form'
import { TextBox } from '../lib/text-box'
import { Button } from '../lib/button'
import { Row } from '../lib/row'
interface ICreateRepositoryProps {
readonly dispatcher: Dispatcher
@ -73,33 +77,30 @@ export class CreateRepository extends React.Component<ICreateRepositoryProps, IC
public render() {
const disabled = this.state.path.length === 0 || this.state.name.length === 0
return (
<div id='create-repository' className='panel'>
<div>
<label>Name
<input value={this.state.name}
placeholder='repository name'
onChange={this.onNameChanged}/>
</label>
</div>
<Form>
<TextBox
value={this.state.name}
label='Name'
placeholder='repository name'
onChange={this.onNameChanged}/>
{this.renderError()}
<div className='file-picker'>
<label>Local Path
<input value={this.state.path}
placeholder='repository path'
onChange={this.onPathChanged}/>
</label>
<button onClick={this.showFilePicker}>Choose</button>
</div>
<Row>
<TextBox
value={this.state.path}
label='Local Path'
placeholder='repository path'
onChange={this.onPathChanged}/>
<Button onClick={this.showFilePicker}>Choose</Button>
</Row>
<hr/>
<button disabled={disabled} onClick={this.createRepository}>
<Button type='submit' disabled={disabled} onClick={this.createRepository}>
Create Repository
</button>
</div>
</Button>
</Form>
)
}
}

View file

@ -30,6 +30,7 @@ import { Welcome } from './welcome'
import { AppMenu } from './app-menu'
import { UpdateAvailable } from './updates'
import { shouldRenderApplicationMenu } from './lib/features'
import { Button } from './lib/button'
/** The interval at which we should check for updates. */
const UpdateCheckInterval = 1000 * 60 * 60 * 4
@ -415,7 +416,7 @@ export class App extends React.Component<IAppProps, IAppState> {
{msgs.map((msg, i) => <pre className='popup-error-output' key={i}>{msg}</pre>)}
<div className='popup-actions'>
<button onClick={this.clearErrors}>OK</button>
<Button onClick={this.clearErrors}>OK</Button>
</div>
</Popuppy>
)

View file

@ -2,6 +2,7 @@ import * as React from 'react'
import { List } from '../list'
import { IAutocompletionProvider } from './index'
import { fatalError } from '../../lib/fatal-error'
import * as classNames from 'classnames'
interface IPosition {
readonly top: number
@ -192,8 +193,13 @@ export abstract class AutocompletingTextInput<ElementType extends HTMLInputEleme
}
public render() {
const tagName = this.getElementTagName()
const className = classNames('autocompletion-container', this.props.className, {
'text-box-component': tagName === 'input',
'text-area-component': tagName === 'textarea',
})
return (
<div className={`autocompletion-container ${this.props.className || ''}`}>
<div className={className}>
{this.renderAutocompletions()}
{this.renderTextInput()}

View file

@ -51,6 +51,7 @@ export class Checkbox extends React.Component<ICheckboxProps, void> {
public render() {
return (
<input
className='checkbox-component'
tabIndex={this.props.tabIndex}
type='checkbox'
onChange={this.onChange}

View file

@ -8,6 +8,7 @@ import { CommitIdentity } from '../../models/commit-identity'
import { ICommitMessage } from '../../lib/app-state'
import { Dispatcher } from '../../lib/dispatcher'
import { Repository } from '../../models/repository'
import { Button } from '../lib/button'
interface ICommitMessageProps {
readonly onCreateCommit: (message: ICommitMessage) => void
@ -111,9 +112,8 @@ export class CommitMessage extends React.Component<ICommitMessageProps, ICommitM
})
}
private handleSubmit = (event: React.MouseEvent<HTMLButtonElement>) => {
private handleSubmit = () => {
this.createCommit()
event.preventDefault()
}
private createCommit() {
@ -159,16 +159,12 @@ export class CommitMessage extends React.Component<ICommitMessageProps, ICommitM
)
}
private onFormSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.stopPropagation()
}
public render() {
const branchName = this.props.branch ? this.props.branch : 'master'
const buttonEnabled = this.canCommit()
return (
<form id='commit-message' onSubmit={this.onFormSubmit}>
<div id='commit-message'>
<div className='summary'>
{this.renderAvatar()}
@ -187,10 +183,10 @@ export class CommitMessage extends React.Component<ICommitMessageProps, ICommitM
onKeyDown={this.onKeyDown}
autocompletionProviders={this.props.autocompletionProviders}/>
<button className='button commit-button' onClick={this.handleSubmit} disabled={!buttonEnabled}>
<Button type='submit' className='commit-button' onClick={this.handleSubmit} disabled={!buttonEnabled}>
<div>Commit to <strong>{branchName}</strong></div>
</button>
</form>
</Button>
</div>
)
}
}

View file

@ -3,6 +3,7 @@ import * as React from 'react'
import { Commit } from '../../models/commit'
import { EmojiText } from '../lib/emoji-text'
import { RelativeTime } from '../relative-time'
import { Button } from '../lib/button'
interface IUndoCommitProps {
/** The function to call when the Undo button is clicked. */
@ -25,7 +26,7 @@ export class UndoCommit extends React.Component<IUndoCommitProps, void> {
<EmojiText emoji={this.props.emoji} className='summary'>{this.props.commit.summary}</EmojiText>
</div>
<div className='actions'>
<button className='button' onClick={this.props.onUndo}>Undo</button>
<Button type='submit' onClick={this.props.onUndo}>Undo</Button>
</div>
</div>
)

View file

@ -4,6 +4,10 @@ import { Repository } from '../../models/repository'
import { Dispatcher } from '../../lib/dispatcher'
import { sanitizedBranchName } from './sanitized-branch-name'
import { Branch } from '../../models/branch'
import { Form } from '../lib/form'
import { TextBox } from '../lib/text-box'
import { Button } from '../lib/button'
import { Select } from '../lib/select'
interface ICreateBranchProps {
readonly repository: Repository
@ -50,29 +54,30 @@ export class CreateBranch extends React.Component<ICreateBranchProps, ICreateBra
const disabled = !proposedName.length || !!this.state.currentError
const currentBranch = this.props.currentBranch
return (
<form id='create-branch' className='panel' onSubmit={this.createBranch}>
<Form onSubmit={this.createBranch}>
<div className='header'>Create New Branch</div>
<hr/>
<label>Name
<input type='text'
autoFocus={true}
onChange={this.onBranchNameChange}
onKeyDown={this.onKeyDown}/>
</label>
<TextBox
label='Name'
autoFocus={true}
onChange={this.onBranchNameChange}
onKeyDown={this.onKeyDown}/>
{this.renderError()}
<label>From
<select onChange={this.onBaseBranchChange}
defaultValue={currentBranch ? currentBranch.name : undefined}>
{this.props.branches.map(branch => <option key={branch.name} value={branch.name}>{branch.name}</option>)}
</select>
</label>
<Select
label='From'
onChange={this.onBaseBranchChange}
defaultValue={currentBranch ? currentBranch.name : undefined}>
{this.props.branches.map(branch =>
<option key={branch.name} value={branch.name}>{branch.name}</option>
)}
</Select>
<hr/>
<button type='submit' disabled={disabled}>Create Branch</button>
</form>
<Button type='submit' disabled={disabled}>Create Branch</Button>
</Form>
)
}
@ -110,9 +115,7 @@ export class CreateBranch extends React.Component<ICreateBranchProps, ICreateBra
})
}
private createBranch = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault()
private createBranch = () => {
const name = this.state.sanitizedName
const baseBranch = this.state.baseBranch
if (name.length > 0 && baseBranch) {

View file

@ -3,6 +3,8 @@ import * as React from 'react'
import { Dispatcher } from '../../lib/dispatcher'
import { Repository } from '../../models/repository'
import { Branch } from '../../models/branch'
import { Form } from '../lib/form'
import { Button } from '../lib/button'
interface IDeleteBranchProps {
readonly dispatcher: Dispatcher
@ -13,19 +15,17 @@ interface IDeleteBranchProps {
export class DeleteBranch extends React.Component<IDeleteBranchProps, void> {
public render() {
return (
<form className='panel' onSubmit={this.cancel}>
<Form onSubmit={this.cancel}>
<div>Delete branch "{this.props.branch.name}"?</div>
<div>This cannot be undone.</div>
<button type='submit'>Cancel</button>
<button onClick={this.deleteBranch}>Delete</button>
</form>
<Button type='submit'>Cancel</Button>
<Button onClick={this.deleteBranch}>Delete</Button>
</Form>
)
}
private cancel = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault()
private cancel = () => {
this.props.dispatcher.closePopup()
}

View file

@ -3,6 +3,8 @@ import * as React from 'react'
import { Repository } from '../../models/repository'
import { Dispatcher } from '../../lib/dispatcher'
import { WorkingDirectoryFileChange } from '../../models/status'
import { Form } from '../lib/form'
import { Button } from '../lib/button'
interface IDiscardChangesProps {
readonly repository: Repository
@ -15,19 +17,17 @@ export class DiscardChanges extends React.Component<IDiscardChangesProps, void>
public render() {
const paths = this.props.files.map(f => f.path).join(', ')
return (
<form className='panel' onSubmit={this.cancel}>
<Form onSubmit={this.cancel}>
<div>Confirm Discard Changes</div>
<div>Are you sure you want to discard all changes to {paths}?</div>
<button type='submit'>Cancel</button>
<button onClick={this.discard}>Discard Changes</button>
</form>
<Button type='submit'>Cancel</Button>
<Button onClick={this.discard}>Discard Changes</Button>
</Form>
)
}
private cancel = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault()
private cancel = () => {
this.props.dispatcher.closePopup()
}

View file

@ -1,6 +1,5 @@
import * as React from 'react'
import { LinkButton } from '../lib/link-button'
import { Button } from '../lib/button'
import { Octicon, OcticonSymbol } from '../octicons'
import {
createAuthorization,
@ -13,6 +12,10 @@ import { User } from '../../models/user'
import { assertNever } from '../../lib/fatal-error'
import { askUserToOAuth } from '../../lib/oauth'
import { Loading } from './loading'
import { Form } from './form'
import { Button } from './button'
import { TextBox } from './text-box'
import { Errors } from './errors'
interface IAuthenticationFormProps {
/** The endpoint against which the user is authenticating. */
@ -49,13 +52,13 @@ export class AuthenticationForm extends React.Component<IAuthenticationFormProps
public render() {
return (
<form className='sign-in-form' onSubmit={this.signIn}>
<Form className='sign-in-form' onSubmit={this.signIn}>
{this.renderUsernamePassword()}
{this.renderError()}
{this.renderSignInWithBrowser()}
</form>
</Form>
)
}
@ -65,15 +68,22 @@ export class AuthenticationForm extends React.Component<IAuthenticationFormProps
const disabled = this.state.loading
return (
<div>
<div className='field-group'>
<label htmlFor='sign-in-name'>Username or email address</label>
<input id='sign-in-name' className='text-field sign-in-field' disabled={disabled} autoFocus={true} onChange={this.onUsernameChange}/>
</div>
<TextBox
label='Username or email address'
disabled={disabled}
autoFocus={true}
onChange={this.onUsernameChange}/>
<div className='field-group'>
<label htmlFor='sign-in-password'>Password</label>
<input id='sign-in-password' className='sign-in-field' type='password' disabled={disabled} onChange={this.onPasswordChange}/>
<LinkButton className='forgot-password-link' uri={this.getForgotPasswordURL()}>Forgot password?</LinkButton>
<div className='password-container'>
<TextBox
label='Password'
secure={true}
disabled={disabled}
onChange={this.onPasswordChange}/>
<LinkButton className='forgot-password-link' uri={this.getForgotPasswordURL()}>
Forgot password?
</LinkButton>
</div>
{this.renderActions()}
@ -115,13 +125,13 @@ export class AuthenticationForm extends React.Component<IAuthenticationFormProps
if (!response) { return null }
switch (response.kind) {
case AuthorizationResponseKind.Failed: return <div className='form-errors'>The username or password are incorrect.</div>
case AuthorizationResponseKind.Failed: return <Errors>The username or password are incorrect.</Errors>
case AuthorizationResponseKind.Error: {
const error = response.response.error
if (error) {
return <div className='form-errors'>An error occurred: {error.message}</div>
return <Errors>An error occurred: {error.message}</Errors>
} else {
return <div className='form-errors'>An unknown error occurred: {response.response.statusCode}: {response.response.body}</div>
return <Errors>An unknown error occurred: {response.response.statusCode}: {response.response.body}</Errors>
}
}
case AuthorizationResponseKind.TwoFactorAuthenticationRequired: return null
@ -157,9 +167,7 @@ export class AuthenticationForm extends React.Component<IAuthenticationFormProps
this.props.onDidSignIn(user)
}
private signIn = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault()
private signIn = async () => {
const username = this.state.username
const password = this.state.password
this.setState({

View file

@ -16,19 +16,29 @@ interface IButtonProps {
/** CSS class names */
readonly className?: string
/**
* The `ref` for the underlying <button> element.
*
* Ideally this would be named `ref`, but TypeScript seems to special-case its
* handling of the `ref` type into some ungodly monstrosity. Hopefully someday
* this will be unnecessary.
*/
readonly reference?: React.Ref<HTMLButtonElement>
}
/** A button component. */
export class Button extends React.Component<IButtonProps, void> {
public render() {
const className = classNames('button', this.props.className)
const className = classNames('button-component', this.props.className)
return (
<button
className={className}
disabled={this.props.disabled}
onClick={this.onClick}
type={this.props.type}>
type={this.props.type}
ref={this.props.reference}>
{this.props.children}
</button>
)

View file

@ -1,8 +1,11 @@
import * as React from 'react'
import { Button } from './button'
import { getEnterpriseAPIURL, fetchMetadata } from '../../lib/api'
import { Loading } from './loading'
import { validateURL, InvalidURLErrorName, InvalidProtocolErrorName } from './enterprise-validate-url'
import { Form } from './form'
import { TextBox } from './text-box'
import { Button } from './button'
import { Errors } from './errors'
/** The authentication methods server allows. */
export enum AuthenticationMethods {
@ -38,20 +41,19 @@ export class EnterpriseServerEntry extends React.Component<IEnterpriseServerEntr
const disableEntry = this.state.loading
const disableSubmission = !this.state.serverAddress.length || this.state.loading
return (
<form className='sign-in-form' id='enterprise-server-entry' onSubmit={this.onSubmit}>
<div className='field-group'>
<label htmlFor='enterprise-address'>Enterprise server address</label>
<input id='enterprise-address' className='text-field sign-in-field' autoFocus={true} disabled={disableEntry} onChange={this.onServerAddressChanged}/>
</div>
<Form onSubmit={this.onSubmit}>
<TextBox
label='Enterprise server address'
autoFocus={true}
disabled={disableEntry}
onChange={this.onServerAddressChanged}/>
<div className='actions'>
<Button type='submit' disabled={disableSubmission}>Continue</Button>
</div>
<Button type='submit' disabled={disableSubmission}>Continue</Button>
{this.state.loading ? <Loading/> : null}
<div>{this.state.error ? this.state.error.message : null }</div>
</form>
{this.state.error ? <Errors>{this.state.error.message}</Errors> : null}
</Form>
)
}
@ -82,9 +84,7 @@ export class EnterpriseServerEntry extends React.Component<IEnterpriseServerEntr
}
}
private onSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault()
private onSubmit = async () => {
const userEnteredAddress = this.state.serverAddress
let address: string
try {

22
app/src/ui/lib/errors.tsx Normal file
View file

@ -0,0 +1,22 @@
import * as React from 'react'
import * as classNames from 'classnames'
interface IErrorsProps {
/** The class name for the internal element. */
readonly className?: string
/** The children to be displayed as an error message. */
readonly children?: ReadonlyArray<JSX.Element>
}
/** An Errors element with app-standard styles. */
export class Errors extends React.Component<IErrorsProps, void> {
public render() {
const className = classNames('errors-component', this.props.className)
return (
<div className={className}>
{this.props.children}
</div>
)
}
}

30
app/src/ui/lib/form.tsx Normal file
View file

@ -0,0 +1,30 @@
import * as React from 'react'
import * as classNames from 'classnames'
interface IFormProps {
/** The class name for the form. */
readonly className?: string
/** Called when the form is submitted. */
readonly onSubmit?: () => void
}
/** A form element with app-standard styles. */
export class Form extends React.Component<IFormProps, void> {
public render() {
const className = classNames('form-component', this.props.className)
return (
<form className={className} onSubmit={this.onSubmit}>
{this.props.children}
</form>
)
}
private onSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault()
if (this.props.onSubmit) {
this.props.onSubmit()
}
}
}

22
app/src/ui/lib/row.tsx Normal file
View file

@ -0,0 +1,22 @@
import * as React from 'react'
import * as classNames from 'classnames'
interface IRowProps {
/** The class name for the internal element. */
readonly className?: string
/** The children to be displayed in a row. */
readonly children?: ReadonlyArray<JSX.Element>
}
/** A horizontal row element with app-standard styles. */
export class Row extends React.Component<IRowProps, void> {
public render() {
const className = classNames('row-component', this.props.className)
return (
<div className={className}>
{this.props.children}
</div>
)
}
}

36
app/src/ui/lib/select.tsx Normal file
View file

@ -0,0 +1,36 @@
import * as React from 'react'
interface ISelectProps {
/** The label for the select control. */
readonly label?: string
/** The value of the select control. */
readonly value?: string
/** The default value of the select control. */
readonly defaultValue?: string
/** Called when the user changes the selected valued. */
readonly onChange?: (event: React.FormEvent<HTMLSelectElement>) => void
/** The <option>'s for the select control. */
readonly children?: ReadonlyArray<JSX.Element>
}
/** A select element with app-standard styles. */
export class Select extends React.Component<ISelectProps, void> {
public render() {
return (
<label className='select-component'>
{this.props.label}
<select
onChange={this.props.onChange}
value={this.props.value}
defaultValue={this.props.defaultValue}>
{this.props.children}
</select>
</label>
)
}
}

View file

@ -0,0 +1,56 @@
import * as React from 'react'
import * as classNames from 'classnames'
interface ITextBoxProps {
/** The label for the input field. */
readonly label?: string
/** The class name for the label. */
readonly labelClassName?: string
/** The class name for the input field. */
readonly inputClassName?: string
/** The placeholder for the input field. */
readonly placeholder?: string
/** The current value of the input field. */
readonly value?: string
/** Whether the input field should be for secure entry. */
readonly secure?: boolean
/** Whether the input field should auto focus when mounted. */
readonly autoFocus?: boolean
/** Whether the input field is disabled. */
readonly disabled?: boolean
/** Called when the user changes the value in the input field. */
readonly onChange?: (event: React.FormEvent<HTMLInputElement>) => void
/** Called on key down. */
readonly onKeyDown?: (event: React.KeyboardEvent<HTMLInputElement>) => void
}
/** An input element with app-standard styles. */
export class TextBox extends React.Component<ITextBoxProps, void> {
public render() {
const className = classNames('text-box-component', this.props.labelClassName)
return (
<label className={className}>
{this.props.label}
<input
autoFocus={this.props.autoFocus}
className={this.props.inputClassName}
disabled={this.props.disabled}
type={!this.props.secure ? 'text' : 'password'}
placeholder={this.props.placeholder}
value={this.props.value}
onChange={this.props.onChange}
onKeyDown={this.props.onKeyDown}/>
</label>
)
}
}

View file

@ -1,9 +1,12 @@
import * as React from 'react'
import { createAuthorization, AuthorizationResponse, fetchUser, AuthorizationResponseKind } from '../../lib/api'
import { User } from '../../models/user'
import { Button } from './button'
import { assertNever } from '../../lib/fatal-error'
import { Loading } from './loading'
import { Button } from './button'
import { TextBox } from './text-box'
import { Form } from './form'
import { Errors } from './errors'
interface ITwoFactorAuthenticationProps {
/** The endpoint to authenticate against. */
@ -43,20 +46,19 @@ export class TwoFactorAuthentication extends React.Component<ITwoFactorAuthentic
authentication code and verify your identity.
</p>
<form id='2fa-form' className='sign-in-form' onSubmit={this.signIn}>
<div className='field-group'>
<label htmlFor='two-factor-code'>Authentication code</label>
<input id='two-factor-code' className='text-field sign-in-field' disabled={textEntryDisabled} autoFocus={true} onChange={this.onOTPChange}/>
</div>
<Form onSubmit={this.signIn}>
<TextBox
label='Authentication code'
disabled={textEntryDisabled}
autoFocus={true}
onChange={this.onOTPChange}/>
{this.renderError()}
<div className='actions'>
<Button type='submit' disabled={signInDisabled}>Verify</Button>
<Button type='submit' disabled={signInDisabled}>Verify</Button>
{this.state.loading ? <Loading/> : null}
</div>
</form>
{this.state.loading ? <Loading/> : null}
</Form>
</div>
)
}
@ -67,14 +69,14 @@ export class TwoFactorAuthentication extends React.Component<ITwoFactorAuthentic
switch (response.kind) {
case AuthorizationResponseKind.Authorized: return null
case AuthorizationResponseKind.Failed: return <div className='form-errors'>Failed</div>
case AuthorizationResponseKind.TwoFactorAuthenticationRequired: return <div className='form-errors'>2fa</div>
case AuthorizationResponseKind.Failed: return <Errors>Failed</Errors>
case AuthorizationResponseKind.TwoFactorAuthenticationRequired: return <Errors>2fa</Errors>
case AuthorizationResponseKind.Error: {
const error = response.response.error
if (error) {
return <div className='form-errors'>An error occurred: {error.message}</div>
return <Errors>An error occurred: {error.message}</Errors>
} else {
return <div className='form-errors'>An unknown error occurred: {response.response.statusCode}: {response.response.body}</div>
return <Errors>An unknown error occurred: {response.response.statusCode}: {response.response.body}</Errors>
}
}
default: return assertNever(response, `Unknown response: ${response}`)
@ -89,7 +91,7 @@ export class TwoFactorAuthentication extends React.Component<ITwoFactorAuthentic
})
}
private signIn = async (event: React.FormEvent<HTMLFormElement>) => {
private signIn = async () => {
event.preventDefault()
this.setState({

View file

@ -3,6 +3,10 @@ import { Dispatcher } from '../../lib/dispatcher'
import { Repository } from '../../models/repository'
import { User } from '../../models/user'
import { API, IAPIUser, getDotComAPIEndpoint } from '../../lib/api'
import { Form } from '../lib/form'
import { TextBox } from '../lib/text-box'
import { Button } from '../lib/button'
import { Select } from '../lib/select'
interface IPublishRepositoryProps {
readonly dispatcher: Dispatcher
@ -107,9 +111,7 @@ export class PublishRepository extends React.Component<IPublishRepositoryProps,
return this.state.selectedUser
}
private publishRepository = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault()
private publishRepository = () => {
const owningAccount = this.findOwningUserForSelectedUser()!
this.props.dispatcher.publishRepository(this.props.repository, this.state.name, this.state.description, this.state.private, owningAccount, this.selectedOrg)
this.props.dispatcher.closePopup()
@ -147,25 +149,19 @@ export class PublishRepository extends React.Component<IPublishRepositoryProps,
const value = JSON.stringify(this.state.selectedUser)
return (
<select value={value} onChange={this.onAccountChange}>
<Select label='Account' value={value} onChange={this.onAccountChange}>
{optionGroups}
</select>
</Select>
)
}
public render() {
const disabled = !this.state.name.length
return (
<form id='publish-repository' className='panel' onSubmit={this.publishRepository}>
<label>
Name:
<input type='text' value={this.state.name} autoFocus={true} onChange={this.onNameChange}/>
</label>
<Form onSubmit={this.publishRepository}>
<TextBox label='Name' value={this.state.name} autoFocus={true} onChange={this.onNameChange}/>
<label>
Description:
<input type='text' value={this.state.description} onChange={this.onDescriptionChange}/>
</label>
<TextBox label='Description' value={this.state.description} onChange={this.onDescriptionChange}/>
<hr/>
@ -174,13 +170,10 @@ export class PublishRepository extends React.Component<IPublishRepositoryProps,
<input type='checkbox' checked={this.state.private} onChange={this.onPrivateChange}/>
</label>
<label>
Account:
{this.renderAccounts()}
</label>
{this.renderAccounts()}
<button type='submit' disabled={disabled}>Publish Repository</button>
</form>
<Button type='submit' disabled={disabled}>Publish Repository</Button>
</Form>
)
}
}

View file

@ -4,6 +4,9 @@ import { Dispatcher } from '../../lib/dispatcher'
import { Repository } from '../../models/repository'
import { Branch } from '../../models/branch'
import { sanitizedBranchName } from '../create-branch/sanitized-branch-name'
import { Form } from '../lib/form'
import { TextBox } from '../lib/text-box'
import { Button } from '../lib/button'
interface IRenameBranchProps {
readonly dispatcher: Dispatcher
@ -34,19 +37,19 @@ export class RenameBranch extends React.Component<IRenameBranchProps, IRenameBra
public render() {
const disabled = !this.state.newName.length
return (
<form className='panel' onSubmit={this.renameBranch}>
<label>
Name <input value={this.state.newName}
autoFocus={true}
onChange={this.onNameChange}
onKeyDown={this.onKeyDown}/>
</label>
<Form onSubmit={this.renameBranch}>
<TextBox
label='Name'
autoFocus={true}
value={this.state.newName}
onChange={this.onNameChange}
onKeyDown={this.onKeyDown}/>
{this.renderError()}
<button onClick={this.cancel}>Cancel</button>
<button type='submit' disabled={disabled}>Rename {this.props.branch.name}</button>
</form>
<Button onClick={this.cancel}>Cancel</Button>
<Button type='submit' disabled={disabled}>Rename {this.props.branch.name}</Button>
</Form>
)
}
@ -64,9 +67,7 @@ export class RenameBranch extends React.Component<IRenameBranchProps, IRenameBra
this.props.dispatcher.closePopup()
}
private renameBranch = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault()
private renameBranch = () => {
const name = sanitizedBranchName(this.state.newName)
this.props.dispatcher.renameBranch(this.props.repository, this.props.branch, name)
this.props.dispatcher.closePopup()

View file

@ -2,6 +2,7 @@ import * as React from 'react'
import { Octicon, OcticonSymbol } from '../octicons'
import * as classNames from 'classnames'
import { assertNever } from '../../lib/fatal-error'
import { Button } from '../lib/button'
/** The button style. */
export enum ToolbarButtonStyle {
@ -77,11 +78,11 @@ export class ToolbarButton extends React.Component<IToolbarButtonProps, void> {
return (
<div className={className}>
{preContent}
<button onClick={this.onClick} ref={this.onButtonRef}>
<Button onClick={this.onClick} reference={this.onButtonRef}>
{icon}
{this.renderText()}
{this.props.children}
</button>
</Button>
</div>
)
}

View file

@ -4,6 +4,9 @@ import { Commit } from '../../models/commit'
import { getGlobalConfigValue, setGlobalConfigValue } from '../../lib/git/config'
import { CommitListItem } from '../history/commit-list-item'
import { User } from '../../models/user'
import { Form } from '../lib/form'
import { Button } from '../lib/button'
import { TextBox } from '../lib/text-box'
import { CommitIdentity } from '../../models/commit-identity'
interface IConfigureGitProps {
@ -71,22 +74,16 @@ export class ConfigureGit extends React.Component<IConfigureGitProps, IConfigure
This is used to identify the commits you create. Anyone will be able to see this information if you publish commits.
</p>
<form className='sign-in-form' onSubmit={this.continue}>
<div className='field-group'>
<label htmlFor='git-name'>Name</label>
<input id='git-name' className='sign-in-field text-field' placeholder='Hubot' value={this.state.name} onChange={this.onNameChange}/>
</div>
<Form className='sign-in-form' onSubmit={this.continue}>
<TextBox label='Name' placeholder='Hubot' value={this.state.name} onChange={this.onNameChange}/>
<div className='field-group'>
<label htmlFor='git-email'>Email</label>
<input id='git-email' className='sign-in-field text-field' placeholder='hubot@github.com' value={this.state.email} onChange={this.onEmailChange}/>
</div>
<TextBox label='Email' placeholder='hubot@github.com' value={this.state.email} onChange={this.onEmailChange}/>
<div className='actions'>
<button type='submit'>Continue</button>
<button className='secondary-button' onClick={this.cancel}>Cancel</button>
<Button type='submit'>Continue</Button>
<Button onClick={this.cancel}>Cancel</Button>
</div>
</form>
</Form>
<div id='commit-list' className='commit-list-example'>
<CommitListItem commit={dummyCommit1} emoji={emoji} avatarURL={null}/>
@ -121,9 +118,7 @@ export class ConfigureGit extends React.Component<IConfigureGitProps, IConfigure
return matchingUser ? matchingUser.avatarURL : null
}
private continue = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault()
private continue = async () => {
this.props.done()
const name = this.state.name
@ -137,9 +132,7 @@ export class ConfigureGit extends React.Component<IConfigureGitProps, IConfigure
}
}
private cancel = (event: React.FormEvent<HTMLButtonElement>) => {
event.preventDefault()
private cancel = () => {
this.props.advance(WelcomeStep.Start)
}
}

View file

@ -22,7 +22,7 @@ export class SignInDotCom extends React.Component<ISignInDotComProps, void> {
endpoint={getDotComAPIEndpoint()}
supportsBasicAuth={true}
additionalButtons={[
<Button className='secondary-button' key='cancel' onClick={this.cancel}>Cancel</Button>,
<Button key='cancel' onClick={this.cancel}>Cancel</Button>,
]}
onDidSignIn={this.onDidSignIn}/>
</div>

View file

@ -52,7 +52,7 @@ export class SignInEnterprise extends React.Component<ISignInEnterpriseProps, IS
endpoint={step.endpoint}
supportsBasicAuth={step.authMethods.has(AuthenticationMethods.BasicAuth)}
additionalButtons={[
<Button className='secondary-button' key='cancel' onClick={this.cancel}>Cancel</Button>,
<Button key='cancel' onClick={this.cancel}>Cancel</Button>,
]}
onDidSignIn={this.onDidSignIn}/>
} else {

View file

@ -1,6 +1,7 @@
import * as React from 'react'
import { WelcomeStep } from './welcome'
import { LinkButton } from '../lib/link-button'
import { Button } from '../lib/button'
const CreateAccountURL = 'https://github.com/join?source=github-desktop'
@ -21,8 +22,8 @@ export class Start extends React.Component<IStartProps, void> {
<h1 className='welcome-title'>Welcome to GitHub Desktop</h1>
<h2 className='welcome-text'>Get started by signing into GitHub.com or your GitHub Enterprise server.</h2>
<div className='actions'>
<button className='button welcome-button' onClick={this.signInToDotCom}>GitHub.com</button>
<button className='button welcome-button' onClick={this.signInToEnterprise}>GitHub Enterprise</button>
<Button type='submit' className='welcome-button' onClick={this.signInToDotCom}>GitHub.com</Button>
<Button type='submit' className='welcome-button' onClick={this.signInToEnterprise}>GitHub Enterprise</Button>
</div>
<div>

View file

@ -1,7 +1,5 @@
@import "ui/app";
@import "ui/app-menu";
@import "ui/forms";
@import "ui/buttons";
@import "ui/scroll";
@import "ui/window/title-bar";
@import "ui/file-list";
@ -21,12 +19,17 @@
@import "ui/popup";
@import "ui/progress";
@import "ui/branches";
@import "ui/add-existing-repository";
@import "ui/create-repository";
@import "ui/publish-repository";
@import "ui/emoji";
@import "ui/ui-view";
@import "ui/autocompletion";
@import "ui/welcome";
@import "ui/foldout";
@import "ui/update-available";
@import "ui/form";
@import "ui/text-box";
@import "ui/button";
@import "ui/select";
@import "ui/row";
@import "ui/text-area";
@import "ui/checkbox";
@import "ui/errors";

View file

@ -28,6 +28,8 @@
--text-secondary-color: #8A9499;
--background-color: $white;
--button-height: 28px;
--button-background: $blue;
--button-hover-background: lighten($blue, 5%);
--button-text-color: $white;

View file

@ -1,15 +0,0 @@
#add-existing-repository {
.file-picker {
display: flex;
flex-direction: row;
input {
flex-grow: 2;
margin-right: var(--spacing);
}
}
.add-repo-form {
margin-bottom: var(--spacing);
}
}

View file

@ -1,9 +1,4 @@
button,
input[type='submit'],
.button {
background-color: var(--button-background);
color: var(--button-text-color);
.button-component {
// Chrome on Windows ignores the body element
// font-family and uses Arial so we redefine
// it here
@ -13,9 +8,13 @@ input[type='submit'],
padding: var(--spacing-half) var(--spacing);
border: none;
height: var(--button-height);
color: var(--secondary-button-text-color);
background-color: var(--secondary-button-background);
&:not(:disabled):hover {
background-color: var(--button-hover-background);
background-color: var(--secondary-button-hover-background);
}
&:focus {
@ -26,7 +25,16 @@ input[type='submit'],
&:disabled { opacity: 0.3; }
}
a.link-button-component {
.button-component[type='submit'] {
background-color: var(--button-background);
color: var(--button-text-color);
&:not(:disabled):hover {
background-color: var(--button-hover-background);
}
}
.link-button-component {
color: var(--link-button-color);
text-decoration: none;
@ -34,12 +42,3 @@ a.link-button-component {
text-decoration: underline;
}
}
.secondary-button {
color: var(--secondary-button-text-color);
background-color: var(--secondary-button-background);
&:not(:disabled):hover {
background-color: var(--secondary-button-hover-background);
}
}

View file

@ -0,0 +1,8 @@
.checkbox-component {
&:focus {
// Don't know why but on Windows this is necessary to
// not have a 1px gap around the checkbox on the right
// hand side. I'm guessing it's the same on mac?
outline-offset: -1px;
}
}

View file

@ -1,16 +0,0 @@
#create-repository {
.file-picker {
display: flex;
flex-direction: row;
}
label {
display: flex;
align-items: center;
flex: 1;
input {
flex: 1;
}
}
}

View file

@ -0,0 +1,8 @@
.errors-component {
background: var(--form-error-background);
border: 1px solid var(--form-error-border-color);
border-radius: var(--border-radius);
color: var(--error-color);
font-size: var(--font-size-sm);
padding: var(--spacing-half);
}

17
app/styles/ui/_form.scss Normal file
View file

@ -0,0 +1,17 @@
.form-component {
display: flex;
flex-direction: column;
margin: var(--spacing-half) 0;
& > *:not(:last-child) {
margin-bottom: var(--spacing);
}
hr {
width: 100%;
border: none;
height: 1px;
border-bottom: var(--base-border);
}
}

View file

@ -1,53 +0,0 @@
input[type='text'],
input[type='date'],
input[type='email'],
input[type='range'],
input[type='search'],
input[type='password'],
textarea,
.text-field {
border: 1px solid var(--box-border-color);
border-radius: var(--border-radius);
color: currentColor;
font-size: var(--font-size);
font-family: var(--font-family-sans-serif);
padding: var(--spacing-half);
resize: none;
&:focus {
outline: none;
border-color: var(--focus-color);
}
}
input[type='checkbox'] {
&:focus {
// Don't know why but on Windows this is necessary to
// not have a 1px gap around the checkbox on the right
// hand side. I'm guessing it's the same on mac?
outline-offset: -1px;
}
}
textarea {
min-height: 100px;
}
.field-group {
margin: var(--spacing-half) 0;
// Setting the position to relative allows us to
// absolutely position things based on `.field-group` box boundaries.
// (See the `.forgot-password-link` styles in _welcome.scss for an example)
position: relative;
}
.form-errors {
background: var(--form-error-background);
border: 1px solid var(--form-error-border-color);
border-radius: var(--border-radius);
color: var(--error-color);
font-size: var(--font-size-sm);
padding: var(--spacing-half);
}

View file

@ -1,3 +0,0 @@
#publish-repository {
}

12
app/styles/ui/_row.scss Normal file
View file

@ -0,0 +1,12 @@
.row-component {
display: flex;
flex-direction: row;
& > *:not(:last-child) {
margin-right: var(--spacing);
}
.button-component {
align-self: flex-end;
}
}

View file

@ -0,0 +1,8 @@
.select-component {
display: flex;
flex-direction: column;
select {
flex: 1;
}
}

View file

@ -0,0 +1,19 @@
.text-area-component {
textarea {
border: 1px solid var(--box-border-color);
border-radius: var(--border-radius);
color: currentColor;
font-size: var(--font-size);
font-family: var(--font-family-sans-serif);
padding: var(--spacing-half);
resize: none;
min-height: 100px;
&:focus {
outline: none;
border-color: var(--focus-color);
}
}
}

View file

@ -0,0 +1,26 @@
.text-box-component {
display: flex;
flex-direction: column;
flex: 1;
input {
display: flex;
flex-direction: row;
flex: 1;
border: 1px solid var(--box-border-color);
border-radius: var(--border-radius);
color: currentColor;
font-size: var(--font-size);
font-family: var(--font-family-sans-serif);
padding: var(--spacing-half);
margin-right: var(--spacing);
resize: none;
&:focus {
outline: none;
border-color: var(--focus-color);
}
}
}

View file

@ -91,6 +91,10 @@
width: 200px;
}
.password-container {
position: relative;
}
.forgot-password-link {
position: absolute;
top: 0;