diff --git a/keymaps/git.cson b/keymaps/git.cson index 2cfc2172a4..f2ba12b483 100644 --- a/keymaps/git.cson +++ b/keymaps/git.cson @@ -112,6 +112,7 @@ 'esc': 'tool-panel:unfocus' '.github-Dialog': + 'tab': 'core:focus-next' 'shift-tab': 'core:focus-previous' '.github-Dialog input': @@ -120,18 +121,20 @@ '.github-CoAuthorForm input': 'enter': 'core:confirm' +'body .github-TabbableWrapper': + 'down': 'github:selectbox-down' + 'up': 'github:selectbox-up' + 'enter': 'github:selectbox-enter' + 'tab': 'github:selectbox-tab' + 'backspace': 'github:selectbox-backspace' + 'pageup': 'github:selectbox-pageup' + 'pagedown': 'github:selectbox-pagedown' + 'end': 'github:selectbox-end' + 'home': 'github:selectbox-home' + 'delete': 'github:selectbox-delete' + 'escape': 'github:selectbox-escape' + 'body .github-CommitView-coAuthorEditor': - 'enter': 'github:co-author:enter' - 'down': 'github:co-author:down' - 'up': 'github:co-author:up' - 'tab': 'github:co-author:tab' - 'backspace': 'github:co-author:backspace' - 'escape': 'github:co-author:escape' - 'pageup': 'github:co-author:pageup' - 'pagedown': 'github:co-author:pagedown' - 'home': 'github:co-author:home' - 'end': 'github:co-author:end' - 'delete': 'github:co-author:delete' 'shift-backspace': 'github:co-author-exclude' '.platform-darwin .github-Reviews': diff --git a/lib/atom/atom-text-editor.js b/lib/atom/atom-text-editor.js index 6f1105dde1..7e0c3f7315 100644 --- a/lib/atom/atom-text-editor.js +++ b/lib/atom/atom-text-editor.js @@ -41,6 +41,7 @@ export default class AtomTextEditor extends React.Component { tabIndex: PropTypes.number, refModel: RefHolderPropType, + refElement: RefHolderPropType, children: PropTypes.node, } @@ -62,7 +63,7 @@ export default class AtomTextEditor extends React.Component { this.subs = new CompositeDisposable(); this.refParent = new RefHolder(); - this.refElement = new RefHolder(); + this.refElement = null; this.refModel = null; } @@ -91,7 +92,7 @@ export default class AtomTextEditor extends React.Component { } element.appendChild(editor.getElement()); this.getRefModel().setter(editor); - this.refElement.setter(editor.getElement()); + this.getRefElement().setter(editor.getElement()); this.subs.add( editor.onDidChangeCursorPosition(this.props.didChangeCursorPosition), @@ -136,20 +137,20 @@ export default class AtomTextEditor extends React.Component { observeEmptiness = () => { this.getRefModel().map(editor => { if (editor.isEmpty() && this.props.hideEmptiness) { - this.refElement.map(element => element.classList.add(EMPTY_CLASS)); + this.getRefElement().map(element => element.classList.add(EMPTY_CLASS)); } else { - this.refElement.map(element => element.classList.remove(EMPTY_CLASS)); + this.getRefElement().map(element => element.classList.remove(EMPTY_CLASS)); } return null; }); } contains(element) { - return this.refElement.map(e => e.contains(element)).getOr(false); + return this.getRefElement().map(e => e.contains(element)).getOr(false); } focus() { - this.refElement.map(e => e.focus()); + this.getRefElement().map(e => e.focus()); } getRefModel() { @@ -164,6 +165,18 @@ export default class AtomTextEditor extends React.Component { return this.refModel; } + getRefElement() { + if (this.props.refElement) { + return this.props.refElement; + } + + if (!this.refElement) { + this.refElement = new RefHolder(); + } + + return this.refElement; + } + getModel() { return this.getRefModel().getOr(undefined); } diff --git a/lib/containers/__generated__/createDialogContainerQuery.graphql.js b/lib/containers/__generated__/createDialogContainerQuery.graphql.js new file mode 100644 index 0000000000..e37eaefb04 --- /dev/null +++ b/lib/containers/__generated__/createDialogContainerQuery.graphql.js @@ -0,0 +1,283 @@ +/** + * @flow + * @relayHash 72a9fbd2efed6312f034405f54084c6f + */ + +/* eslint-disable */ + +'use strict'; + +/*:: +import type { ConcreteRequest } from 'relay-runtime'; +type createDialogController_user$ref = any; +export type createDialogContainerQueryVariables = {| + organizationCount: number, + organizationCursor?: ?string, +|}; +export type createDialogContainerQueryResponse = {| + +viewer: {| + +$fragmentRefs: createDialogController_user$ref + |} +|}; +export type createDialogContainerQuery = {| + variables: createDialogContainerQueryVariables, + response: createDialogContainerQueryResponse, +|}; +*/ + + +/* +query createDialogContainerQuery( + $organizationCount: Int! + $organizationCursor: String +) { + viewer { + ...createDialogController_user_12CDS5 + id + } +} + +fragment createDialogController_user_12CDS5 on User { + id + ...repositoryHomeSelectionView_user_12CDS5 +} + +fragment repositoryHomeSelectionView_user_12CDS5 on User { + id + login + avatarUrl(size: 24) + organizations(first: $organizationCount, after: $organizationCursor) { + pageInfo { + hasNextPage + endCursor + } + edges { + cursor + node { + id + login + avatarUrl(size: 24) + viewerCanCreateRepositories + __typename + } + } + } +} +*/ + +const node/*: ConcreteRequest*/ = (function(){ +var v0 = [ + { + "kind": "LocalArgument", + "name": "organizationCount", + "type": "Int!", + "defaultValue": null + }, + { + "kind": "LocalArgument", + "name": "organizationCursor", + "type": "String", + "defaultValue": null + } +], +v1 = { + "kind": "ScalarField", + "alias": null, + "name": "id", + "args": null, + "storageKey": null +}, +v2 = { + "kind": "ScalarField", + "alias": null, + "name": "login", + "args": null, + "storageKey": null +}, +v3 = { + "kind": "ScalarField", + "alias": null, + "name": "avatarUrl", + "args": [ + { + "kind": "Literal", + "name": "size", + "value": 24 + } + ], + "storageKey": "avatarUrl(size:24)" +}, +v4 = [ + { + "kind": "Variable", + "name": "after", + "variableName": "organizationCursor" + }, + { + "kind": "Variable", + "name": "first", + "variableName": "organizationCount" + } +]; +return { + "kind": "Request", + "fragment": { + "kind": "Fragment", + "name": "createDialogContainerQuery", + "type": "Query", + "metadata": null, + "argumentDefinitions": (v0/*: any*/), + "selections": [ + { + "kind": "LinkedField", + "alias": null, + "name": "viewer", + "storageKey": null, + "args": null, + "concreteType": "User", + "plural": false, + "selections": [ + { + "kind": "FragmentSpread", + "name": "createDialogController_user", + "args": [ + { + "kind": "Variable", + "name": "organizationCount", + "variableName": "organizationCount" + }, + { + "kind": "Variable", + "name": "organizationCursor", + "variableName": "organizationCursor" + } + ] + } + ] + } + ] + }, + "operation": { + "kind": "Operation", + "name": "createDialogContainerQuery", + "argumentDefinitions": (v0/*: any*/), + "selections": [ + { + "kind": "LinkedField", + "alias": null, + "name": "viewer", + "storageKey": null, + "args": null, + "concreteType": "User", + "plural": false, + "selections": [ + (v1/*: any*/), + (v2/*: any*/), + (v3/*: any*/), + { + "kind": "LinkedField", + "alias": null, + "name": "organizations", + "storageKey": null, + "args": (v4/*: any*/), + "concreteType": "OrganizationConnection", + "plural": false, + "selections": [ + { + "kind": "LinkedField", + "alias": null, + "name": "pageInfo", + "storageKey": null, + "args": null, + "concreteType": "PageInfo", + "plural": false, + "selections": [ + { + "kind": "ScalarField", + "alias": null, + "name": "hasNextPage", + "args": null, + "storageKey": null + }, + { + "kind": "ScalarField", + "alias": null, + "name": "endCursor", + "args": null, + "storageKey": null + } + ] + }, + { + "kind": "LinkedField", + "alias": null, + "name": "edges", + "storageKey": null, + "args": null, + "concreteType": "OrganizationEdge", + "plural": true, + "selections": [ + { + "kind": "ScalarField", + "alias": null, + "name": "cursor", + "args": null, + "storageKey": null + }, + { + "kind": "LinkedField", + "alias": null, + "name": "node", + "storageKey": null, + "args": null, + "concreteType": "Organization", + "plural": false, + "selections": [ + (v1/*: any*/), + (v2/*: any*/), + (v3/*: any*/), + { + "kind": "ScalarField", + "alias": null, + "name": "viewerCanCreateRepositories", + "args": null, + "storageKey": null + }, + { + "kind": "ScalarField", + "alias": null, + "name": "__typename", + "args": null, + "storageKey": null + } + ] + } + ] + } + ] + }, + { + "kind": "LinkedHandle", + "alias": null, + "name": "organizations", + "args": (v4/*: any*/), + "handle": "connection", + "key": "RepositoryHomeSelectionView_organizations", + "filters": null + } + ] + } + ] + }, + "params": { + "operationKind": "query", + "name": "createDialogContainerQuery", + "id": null, + "text": "query createDialogContainerQuery(\n $organizationCount: Int!\n $organizationCursor: String\n) {\n viewer {\n ...createDialogController_user_12CDS5\n id\n }\n}\n\nfragment createDialogController_user_12CDS5 on User {\n id\n ...repositoryHomeSelectionView_user_12CDS5\n}\n\nfragment repositoryHomeSelectionView_user_12CDS5 on User {\n id\n login\n avatarUrl(size: 24)\n organizations(first: $organizationCount, after: $organizationCursor) {\n pageInfo {\n hasNextPage\n endCursor\n }\n edges {\n cursor\n node {\n id\n login\n avatarUrl(size: 24)\n viewerCanCreateRepositories\n __typename\n }\n }\n }\n}\n", + "metadata": {} + } +}; +})(); +// prettier-ignore +(node/*: any*/).hash = '862b8ec3127c9a52e9a54020afa47792'; +module.exports = node; diff --git a/lib/containers/create-dialog-container.js b/lib/containers/create-dialog-container.js new file mode 100644 index 0000000000..267a811676 --- /dev/null +++ b/lib/containers/create-dialog-container.js @@ -0,0 +1,123 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import {QueryRenderer, graphql} from 'react-relay'; + +import CreateDialogController from '../controllers/create-dialog-controller'; +import ObserveModel from '../views/observe-model'; +import {PAGE_SIZE} from '../views/repository-home-selection-view'; +import RelayNetworkLayerManager from '../relay-network-layer-manager'; +import {getEndpoint} from '../models/endpoint'; +import {GithubLoginModelPropType} from '../prop-types'; + +const DOTCOM = getEndpoint('github.com'); + +export default class CreateDialogContainer extends React.Component { + static propTypes = { + // Model + loginModel: GithubLoginModelPropType.isRequired, + request: PropTypes.object.isRequired, + error: PropTypes.instanceOf(Error), + inProgress: PropTypes.bool.isRequired, + + // Atom environment + currentWindow: PropTypes.object.isRequired, + workspace: PropTypes.object.isRequired, + commands: PropTypes.object.isRequired, + config: PropTypes.object.isRequired, + } + + constructor(props) { + super(props); + + this.lastProps = null; + } + + render() { + return ( + + {this.renderWithToken} + + ); + } + + renderWithToken = token => { + if (!token) { + return null; + } + + const environment = RelayNetworkLayerManager.getEnvironmentForHost(DOTCOM, token); + const query = graphql` + query createDialogContainerQuery( + $organizationCount: Int! + $organizationCursor: String + ) { + viewer { + ...createDialogController_user @arguments( + organizationCount: $organizationCount + organizationCursor: $organizationCursor + ) + } + } + `; + const variables = { + organizationCount: PAGE_SIZE, + organizationCursor: null, + + // Force QueryRenderer to re-render when dialog request state changes + error: this.props.error, + inProgress: this.props.inProgress, + }; + + return ( + + ); + } + + renderWithResult = ({error, props}) => { + if (error) { + return this.renderError(error); + } + + if (!props && !this.lastProps) { + return this.renderLoading(); + } + + const currentProps = props || this.lastProps; + + return ( + + ); + } + + renderError(error) { + return ( + + ); + } + + renderLoading() { + return ( + + ); + } + + fetchToken = loginModel => loginModel.getToken(DOTCOM.getLoginAccount()) +} diff --git a/lib/controllers/__generated__/createDialogController_user.graphql.js b/lib/controllers/__generated__/createDialogController_user.graphql.js new file mode 100644 index 0000000000..a519d455ca --- /dev/null +++ b/lib/controllers/__generated__/createDialogController_user.graphql.js @@ -0,0 +1,75 @@ +/** + * @flow + */ + +/* eslint-disable */ + +'use strict'; + +/*:: +import type { ReaderFragment } from 'relay-runtime'; +type repositoryHomeSelectionView_user$ref = any; +import type { FragmentReference } from "relay-runtime"; +declare export opaque type createDialogController_user$ref: FragmentReference; +declare export opaque type createDialogController_user$fragmentType: createDialogController_user$ref; +export type createDialogController_user = {| + +id: string, + +$fragmentRefs: repositoryHomeSelectionView_user$ref, + +$refType: createDialogController_user$ref, +|}; +export type createDialogController_user$data = createDialogController_user; +export type createDialogController_user$key = { + +$data?: createDialogController_user$data, + +$fragmentRefs: createDialogController_user$ref, +}; +*/ + + +const node/*: ReaderFragment*/ = { + "kind": "Fragment", + "name": "createDialogController_user", + "type": "User", + "metadata": null, + "argumentDefinitions": [ + { + "kind": "LocalArgument", + "name": "organizationCount", + "type": "Int!", + "defaultValue": null + }, + { + "kind": "LocalArgument", + "name": "organizationCursor", + "type": "String", + "defaultValue": null + } + ], + "selections": [ + { + "kind": "ScalarField", + "alias": null, + "name": "id", + "args": null, + "storageKey": null + }, + { + "kind": "FragmentSpread", + "name": "repositoryHomeSelectionView_user", + "args": [ + { + "kind": "Variable", + "name": "organizationCount", + "variableName": "organizationCount" + }, + { + "kind": "Variable", + "name": "organizationCursor", + "variableName": "organizationCursor" + } + ] + } + ] +}; +// prettier-ignore +(node/*: any*/).hash = '729f5d41fc5444c5f12632127f89ed21'; +module.exports = node; diff --git a/lib/controllers/create-dialog-controller.js b/lib/controllers/create-dialog-controller.js new file mode 100644 index 0000000000..7ec8702c19 --- /dev/null +++ b/lib/controllers/create-dialog-controller.js @@ -0,0 +1,208 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import {createFragmentContainer, graphql} from 'react-relay'; +import {TextBuffer} from 'atom'; +import {CompositeDisposable} from 'event-kit'; +import path from 'path'; + +import CreateDialogView from '../views/create-dialog-view'; + +export class BareCreateDialogController extends React.Component { + static propTypes = { + // Relay + user: PropTypes.shape({ + id: PropTypes.string.isRequired, + }), + + // Model + request: PropTypes.shape({ + getParams: PropTypes.func.isRequired, + accept: PropTypes.func.isRequired, + }).isRequired, + error: PropTypes.instanceOf(Error), + isLoading: PropTypes.bool.isRequired, + inProgress: PropTypes.bool.isRequired, + + // Atom environment + currentWindow: PropTypes.object.isRequired, + workspace: PropTypes.object.isRequired, + commands: PropTypes.object.isRequired, + config: PropTypes.object.isRequired, + } + + constructor(props) { + super(props); + + const {localDir} = this.props.request.getParams(); + + this.projectHome = this.props.config.get('core.projectHome'); + this.modified = { + repoName: false, + localPath: false, + }; + + this.repoName = new TextBuffer({ + text: localDir ? path.basename(localDir) : '', + }); + this.localPath = new TextBuffer({ + text: localDir || this.projectHome, + }); + this.sourceRemoteName = new TextBuffer({ + text: this.props.config.get('github.sourceRemoteName'), + }); + + this.subs = new CompositeDisposable( + this.repoName.onDidChange(this.didChangeRepoName), + this.localPath.onDidChange(this.didChangeLocalPath), + this.sourceRemoteName.onDidChange(this.didChangeSourceRemoteName), + this.props.config.onDidChange('github.sourceRemoteName', this.readSourceRemoteNameSetting), + this.props.config.onDidChange('github.remoteFetchProtocol', this.readRemoteFetchProtocolSetting), + ); + + this.state = { + acceptEnabled: this.acceptIsEnabled(), + selectedVisibility: 'PUBLIC', + selectedProtocol: this.props.config.get('github.remoteFetchProtocol'), + selectedOwnerID: this.props.user ? this.props.user.id : '', + }; + } + + render() { + return ( + + ); + } + + componentDidUpdate(prevProps) { + if (this.props.user !== prevProps.user) { + this.recheckAcceptEnablement(); + } + } + + componentWillUnmount() { + this.subs.dispose(); + } + + didChangeRepoName = () => { + this.modified.repoName = true; + if (!this.modified.localPath) { + if (this.localPath.getText() === this.projectHome) { + this.localPath.setText(path.join(this.projectHome, this.repoName.getText())); + } else { + const dirName = path.dirname(this.localPath.getText()); + this.localPath.setText(path.join(dirName, this.repoName.getText())); + } + this.modified.localPath = false; + } + this.recheckAcceptEnablement(); + } + + didChangeOwnerID = ownerID => new Promise(resolve => this.setState({selectedOwnerID: ownerID}, resolve)) + + didChangeLocalPath = () => { + this.modified.localPath = true; + if (!this.modified.repoName) { + this.repoName.setText(path.basename(this.localPath.getText())); + this.modified.repoName = false; + } + this.recheckAcceptEnablement(); + } + + didChangeVisibility = visibility => { + return new Promise(resolve => this.setState({selectedVisibility: visibility}, resolve)); + } + + didChangeSourceRemoteName = () => { + this.writeSourceRemoteNameSetting(); + this.recheckAcceptEnablement(); + } + + didChangeProtocol = async protocol => { + await new Promise(resolve => this.setState({selectedProtocol: protocol}, resolve)); + this.writeRemoteFetchProtocolSetting(protocol); + } + + readSourceRemoteNameSetting = ({newValue}) => { + if (newValue !== this.sourceRemoteName.getText()) { + this.sourceRemoteName.setText(newValue); + } + } + + writeSourceRemoteNameSetting() { + if (this.props.config.get('github.sourceRemoteName') !== this.sourceRemoteName.getText()) { + this.props.config.set('github.sourceRemoteName', this.sourceRemoteName.getText()); + } + } + + readRemoteFetchProtocolSetting = ({newValue}) => { + if (newValue !== this.state.selectedProtocol) { + this.setState({selectedProtocol: newValue}); + } + } + + writeRemoteFetchProtocolSetting(protocol) { + if (this.props.config.get('github.remoteFetchProtocol') !== protocol) { + this.props.config.set('github.remoteFetchProtocol', protocol); + } + } + + acceptIsEnabled() { + return !this.repoName.isEmpty() && + !this.localPath.isEmpty() && + !this.sourceRemoteName.isEmpty() && + this.props.user !== null; + } + + recheckAcceptEnablement() { + const nextEnablement = this.acceptIsEnabled(); + if (nextEnablement !== this.state.acceptEnabled) { + this.setState({acceptEnabled: nextEnablement}); + } + } + + accept = () => { + if (!this.acceptIsEnabled()) { + return Promise.resolve(); + } + + const ownerID = this.state.selectedOwnerID !== '' ? this.state.selectedOwnerID : this.props.user.id; + + return this.props.request.accept({ + ownerID, + name: this.repoName.getText(), + visibility: this.state.selectedVisibility, + localPath: this.localPath.getText(), + protocol: this.state.selectedProtocol, + sourceRemoteName: this.sourceRemoteName.getText(), + }); + } +} + +export default createFragmentContainer(BareCreateDialogController, { + user: graphql` + fragment createDialogController_user on User + @argumentDefinitions( + organizationCount: {type: "Int!"} + organizationCursor: {type: "String"} + ) { + id + ...repositoryHomeSelectionView_user @arguments( + organizationCount: $organizationCount + organizationCursor: $organizationCursor + ) + } + `, +}); diff --git a/lib/controllers/dialogs-controller.js b/lib/controllers/dialogs-controller.js index 6c3a2f845f..edf272c4ff 100644 --- a/lib/controllers/dialogs-controller.js +++ b/lib/controllers/dialogs-controller.js @@ -6,6 +6,8 @@ import CloneDialog from '../views/clone-dialog'; import CredentialDialog from '../views/credential-dialog'; import OpenIssueishDialog from '../views/open-issueish-dialog'; import OpenCommitDialog from '../views/open-commit-dialog'; +import CreateDialog from '../views/create-dialog'; +import {GithubLoginModelPropType} from '../prop-types'; const DIALOG_COMPONENTS = { null: NullDialog, @@ -14,17 +16,21 @@ const DIALOG_COMPONENTS = { credential: CredentialDialog, issueish: OpenIssueishDialog, commit: OpenCommitDialog, + create: CreateDialog, + publish: CreateDialog, }; export default class DialogsController extends React.Component { static propTypes = { // Model + loginModel: GithubLoginModelPropType.isRequired, request: PropTypes.shape({ identifier: PropTypes.string.isRequired, isProgressing: PropTypes.bool.isRequired, }).isRequired, // Atom environment + currentWindow: PropTypes.object.isRequired, workspace: PropTypes.object.isRequired, commands: PropTypes.object.isRequired, config: PropTypes.object.isRequired, @@ -65,12 +71,14 @@ export default class DialogsController extends React.Component { const wrapped = wrapDialogRequest(request, {accept}); return { - config: this.props.config, - commands: this.props.commands, - workspace: this.props.workspace, + loginModel: this.props.loginModel, + request: wrapped, inProgress: this.state.requestInProgress === request, + currentWindow: this.props.currentWindow, + workspace: this.props.workspace, + commands: this.props.commands, + config: this.props.config, error: this.state.requestError[0] === request ? this.state.requestError[1] : null, - request: wrapped, }; } } @@ -151,4 +159,12 @@ export const dialogRequests = { commit() { return new DialogRequest('commit'); }, + + create() { + return new DialogRequest('create'); + }, + + publish({localDir}) { + return new DialogRequest('publish', {localDir}); + }, }; diff --git a/lib/controllers/root-controller.js b/lib/controllers/root-controller.js index d7056b70aa..53ee82a659 100644 --- a/lib/controllers/root-controller.js +++ b/lib/controllers/root-controller.js @@ -5,11 +5,14 @@ import {remote} from 'electron'; import React, {Fragment} from 'react'; import PropTypes from 'prop-types'; import {CompositeDisposable} from 'event-kit'; +import yubikiri from 'yubikiri'; import StatusBar from '../atom/status-bar'; import PaneItem from '../atom/pane-item'; import {openIssueishItem} from '../views/open-issueish-dialog'; import {openCommitDetailItem} from '../views/open-commit-dialog'; +import {createRepository, publishRepository} from '../views/create-dialog'; +import ObserveModel from '../views/observe-model'; import Commands, {Command} from '../atom/commands'; import ChangedFileItem from '../items/changed-file-item'; import IssueishDetailItem from '../items/issueish-detail-item'; @@ -22,9 +25,11 @@ import CommentDecorationsContainer from '../containers/comment-decorations-conta import DialogsController, {dialogRequests} from './dialogs-controller'; import StatusBarTileController from './status-bar-tile-controller'; import RepositoryConflictController from './repository-conflict-controller'; +import RelayNetworkLayerManager from '../relay-network-layer-manager'; import GitCacheView from '../views/git-cache-view'; import GitTimingsView from '../views/git-timings-view'; import Conflict from '../models/conflicts/conflict'; +import {getEndpoint} from '../models/endpoint'; import Switchboard from '../switchboard'; import {WorkdirContextPoolPropType} from '../prop-types'; import {destroyFilePatchPaneItems, destroyEmptyFilePatchPaneItems, autobind} from '../helpers'; @@ -44,6 +49,7 @@ export default class RootController extends React.Component { config: PropTypes.object.isRequired, project: PropTypes.object.isRequired, confirm: PropTypes.func.isRequired, + currentWindow: PropTypes.object.isRequired, // Models loginModel: PropTypes.object.isRequired, @@ -131,37 +137,56 @@ export default class RootController extends React.Component { const devMode = global.atom && global.atom.inDevMode(); return ( - - {devMode && } - - - - - - - - - this.openInitializeDialog()} /> - this.openCloneDialog()} /> - this.openIssueishDialog()} /> - this.openCommitDialog()} /> - - - - - + + + {devMode && } + + + + + + + + + this.openInitializeDialog()} /> + this.openCloneDialog()} /> + this.openIssueishDialog()} /> + this.openCommitDialog()} /> + this.openCreateDialog()} /> + + + + + + + {data => { + if (!data || !data.isPublishable || !data.remotes.filter(r => r.isGithubRepo()).isEmpty()) { + return null; + } + + return ( + + this.openPublishDialog(this.props.repository)} + /> + + ); + }} + + ); } @@ -189,10 +214,13 @@ export default class RootController extends React.Component { renderDialogs() { return ( ); } @@ -396,6 +424,11 @@ export default class RootController extends React.Component { ); } + fetchData = repository => yubikiri({ + isPublishable: repository.isPublishable(), + remotes: repository.getRemotes(), + }); + async openTabs() { if (this.props.startOpen) { await Promise.all([ @@ -577,6 +610,34 @@ export default class RootController extends React.Component { return new Promise(resolve => this.setState({dialogRequest}, resolve)); } + openCreateDialog = () => { + const dialogRequest = dialogRequests.create(); + dialogRequest.onProgressingAccept(async result => { + const dotcom = getEndpoint('github.com'); + const relayEnvironment = RelayNetworkLayerManager.getEnvironmentForHost(dotcom); + + await createRepository(result, {clone: this.props.clone, relayEnvironment}); + await this.closeDialog(); + }); + dialogRequest.onCancel(this.closeDialog); + + return new Promise(resolve => this.setState({dialogRequest}, resolve)); + } + + openPublishDialog = repository => { + const dialogRequest = dialogRequests.publish({localDir: repository.getWorkingDirectoryPath()}); + dialogRequest.onProgressingAccept(async result => { + const dotcom = getEndpoint('github.com'); + const relayEnvironment = RelayNetworkLayerManager.getEnvironmentForHost(dotcom); + + await publishRepository(result, {repository, relayEnvironment}); + await this.closeDialog(); + }); + dialogRequest.onCancel(this.closeDialog); + + return new Promise(resolve => this.setState({dialogRequest}, resolve)); + } + toggleCommitPreviewItem = () => { const workdir = this.props.repository.getWorkingDirectoryPath(); return this.props.workspace.toggle(CommitPreviewItem.buildURI(workdir)); diff --git a/lib/git-shell-out-strategy.js b/lib/git-shell-out-strategy.js index eee64471bf..6fe27f0d91 100644 --- a/lib/git-shell-out-strategy.js +++ b/lib/git-shell-out-strategy.js @@ -886,6 +886,7 @@ export default class GitShellOutStrategy { if (options.noLocal) { args.push('--no-local'); } if (options.bare) { args.push('--bare'); } if (options.recursive) { args.push('--recursive'); } + if (options.sourceRemoteName) { args.push('--origin', options.remoteName); } args.push(remoteUrl, this.workingDir); return this.exec(args, {useGitPromptServer: true, writeOperation: true}); diff --git a/lib/github-package.js b/lib/github-package.js index e49389bdba..3da08abf44 100644 --- a/lib/github-package.js +++ b/lib/github-package.js @@ -32,7 +32,7 @@ export default class GithubPackage { constructor({ workspace, project, commands, notificationManager, tooltips, styles, grammars, keymaps, config, deserializers, - confirm, getLoadSettings, + confirm, getLoadSettings, currentWindow, configDirPath, renderFn, loginModel, }) { @@ -54,6 +54,7 @@ export default class GithubPackage { this.grammars = grammars; this.keymaps = keymaps; this.configPath = path.join(configDirPath, 'github.cson'); + this.currentWindow = currentWindow; this.styleCalculator = new StyleCalculator(this.styles, this.config); this.confirm = confirm; @@ -281,6 +282,7 @@ export default class GithubPackage { config={this.config} project={this.project} confirm={this.confirm} + currentWindow={this.currentWindow} workdirContextPool={this.contextPool} loginModel={this.loginModel} repository={this.getActiveRepository()} @@ -440,16 +442,16 @@ export default class GithubPackage { await this.scheduleActiveContextUpdate(); } - clone = async (remoteUrl, projectPath) => { + clone = async (remoteUrl, projectPath, sourceRemoteName = 'origin') => { const context = this.contextPool.getContext(projectPath); let repository; if (context.isPresent()) { repository = context.getRepository(); - await repository.clone(remoteUrl); + await repository.clone(remoteUrl, sourceRemoteName); repository.destroy(); } else { repository = new Repository(projectPath, null, {pipelineManager: this.pipelineManager}); - await repository.clone(remoteUrl); + await repository.clone(remoteUrl, sourceRemoteName); } this.workdirCache.invalidate(); diff --git a/lib/index.js b/lib/index.js index 814bca9773..3127c286c7 100644 --- a/lib/index.js +++ b/lib/index.js @@ -17,6 +17,7 @@ const entry = { confirm: atom.confirm.bind(atom), getLoadSettings: atom.getLoadSettings.bind(atom), + currentWindow: atom.getCurrentWindow(), configDirPath: atom.getConfigDirPath(), }); diff --git a/lib/models/repository-states/cloning.js b/lib/models/repository-states/cloning.js index 44afb487dd..52d1bc1cbe 100644 --- a/lib/models/repository-states/cloning.js +++ b/lib/models/repository-states/cloning.js @@ -6,14 +6,15 @@ import State from './state'; * Git is asynchronously cloning a repository into this working directory. */ export default class Cloning extends State { - constructor(repository, remoteUrl) { + constructor(repository, remoteUrl, sourceRemoteName) { super(repository); this.remoteUrl = remoteUrl; + this.sourceRemoteName = sourceRemoteName; } async start() { await fs.mkdirs(this.workdir()); - await this.doClone(this.remoteUrl, {recursive: true}); + await this.doClone(this.remoteUrl, {recursive: true, sourceRemoteName: this.sourceRemoteName}); await this.transitionTo('Loading'); } diff --git a/lib/models/repository-states/empty.js b/lib/models/repository-states/empty.js index 116ceae31c..fe8d9e3f24 100644 --- a/lib/models/repository-states/empty.js +++ b/lib/models/repository-states/empty.js @@ -12,8 +12,8 @@ export default class Empty extends State { return this.transitionTo('Initializing'); } - clone(remoteUrl) { - return this.transitionTo('Cloning', remoteUrl); + clone(remoteUrl, sourceRemoteName) { + return this.transitionTo('Cloning', remoteUrl, sourceRemoteName); } showGitTabInit() { diff --git a/lib/models/repository-states/present.js b/lib/models/repository-states/present.js index 710bbc2cfe..31b856dda1 100644 --- a/lib/models/repository-states/present.js +++ b/lib/models/repository-states/present.js @@ -98,6 +98,10 @@ export default class Present extends State { return true; } + isPublishable() { + return true; + } + acceptInvalidation(spec) { this.cache.invalidate(spec()); this.didUpdate(); diff --git a/lib/models/repository-states/state.js b/lib/models/repository-states/state.js index 9e36786062..9152b98e55 100644 --- a/lib/models/repository-states/state.js +++ b/lib/models/repository-states/state.js @@ -89,6 +89,10 @@ export default class State { return true; } + isPublishable() { + return false; + } + // Lifecycle actions ///////////////////////////////////////////////////////////////////////////////////////////////// // These generally default to rejecting a Promise with an error. diff --git a/lib/models/repository.js b/lib/models/repository.js index 65c07298b7..79bfaabc8a 100644 --- a/lib/models/repository.js +++ b/lib/models/repository.js @@ -291,6 +291,7 @@ const delegates = [ 'showGitTabLoading', 'showStatusBarTiles', 'hasDirectory', + 'isPublishable', 'init', 'clone', diff --git a/lib/mutations/__generated__/createRepositoryMutation.graphql.js b/lib/mutations/__generated__/createRepositoryMutation.graphql.js new file mode 100644 index 0000000000..c86ca20d7b --- /dev/null +++ b/lib/mutations/__generated__/createRepositoryMutation.graphql.js @@ -0,0 +1,171 @@ +/** + * @flow + * @relayHash f8963f231e08ebd4d2cffd1223e19770 + */ + +/* eslint-disable */ + +'use strict'; + +/*:: +import type { ConcreteRequest } from 'relay-runtime'; +export type RepositoryVisibility = "INTERNAL" | "PRIVATE" | "PUBLIC" | "%future added value"; +export type CreateRepositoryInput = {| + name: string, + ownerId?: ?string, + description?: ?string, + visibility: RepositoryVisibility, + template?: ?boolean, + homepageUrl?: ?any, + hasWikiEnabled?: ?boolean, + hasIssuesEnabled?: ?boolean, + teamId?: ?string, + clientMutationId?: ?string, +|}; +export type createRepositoryMutationVariables = {| + input: CreateRepositoryInput +|}; +export type createRepositoryMutationResponse = {| + +createRepository: ?{| + +repository: ?{| + +sshUrl: any, + +url: any, + |} + |} +|}; +export type createRepositoryMutation = {| + variables: createRepositoryMutationVariables, + response: createRepositoryMutationResponse, +|}; +*/ + + +/* +mutation createRepositoryMutation( + $input: CreateRepositoryInput! +) { + createRepository(input: $input) { + repository { + sshUrl + url + id + } + } +} +*/ + +const node/*: ConcreteRequest*/ = (function(){ +var v0 = [ + { + "kind": "LocalArgument", + "name": "input", + "type": "CreateRepositoryInput!", + "defaultValue": null + } +], +v1 = [ + { + "kind": "Variable", + "name": "input", + "variableName": "input" + } +], +v2 = { + "kind": "ScalarField", + "alias": null, + "name": "sshUrl", + "args": null, + "storageKey": null +}, +v3 = { + "kind": "ScalarField", + "alias": null, + "name": "url", + "args": null, + "storageKey": null +}; +return { + "kind": "Request", + "fragment": { + "kind": "Fragment", + "name": "createRepositoryMutation", + "type": "Mutation", + "metadata": null, + "argumentDefinitions": (v0/*: any*/), + "selections": [ + { + "kind": "LinkedField", + "alias": null, + "name": "createRepository", + "storageKey": null, + "args": (v1/*: any*/), + "concreteType": "CreateRepositoryPayload", + "plural": false, + "selections": [ + { + "kind": "LinkedField", + "alias": null, + "name": "repository", + "storageKey": null, + "args": null, + "concreteType": "Repository", + "plural": false, + "selections": [ + (v2/*: any*/), + (v3/*: any*/) + ] + } + ] + } + ] + }, + "operation": { + "kind": "Operation", + "name": "createRepositoryMutation", + "argumentDefinitions": (v0/*: any*/), + "selections": [ + { + "kind": "LinkedField", + "alias": null, + "name": "createRepository", + "storageKey": null, + "args": (v1/*: any*/), + "concreteType": "CreateRepositoryPayload", + "plural": false, + "selections": [ + { + "kind": "LinkedField", + "alias": null, + "name": "repository", + "storageKey": null, + "args": null, + "concreteType": "Repository", + "plural": false, + "selections": [ + (v2/*: any*/), + (v3/*: any*/), + { + "kind": "ScalarField", + "alias": null, + "name": "id", + "args": null, + "storageKey": null + } + ] + } + ] + } + ] + }, + "params": { + "operationKind": "mutation", + "name": "createRepositoryMutation", + "id": null, + "text": "mutation createRepositoryMutation(\n $input: CreateRepositoryInput!\n) {\n createRepository(input: $input) {\n repository {\n sshUrl\n url\n id\n }\n }\n}\n", + "metadata": {} + } +}; +})(); +// prettier-ignore +(node/*: any*/).hash = 'e8f154d9f35411a15f77583bb44f7ed5'; +module.exports = node; diff --git a/lib/mutations/create-repository.js b/lib/mutations/create-repository.js new file mode 100644 index 0000000000..857a9b39cf --- /dev/null +++ b/lib/mutations/create-repository.js @@ -0,0 +1,36 @@ +/* istanbul ignore file */ + +import {commitMutation, graphql} from 'react-relay'; + +const mutation = graphql` + mutation createRepositoryMutation($input: CreateRepositoryInput!) { + createRepository(input: $input) { + repository { + sshUrl + url + } + } + } +`; + +export default (environment, {name, ownerID, visibility}) => { + const variables = { + input: { + name, + ownerId: ownerID, + visibility, + }, + }; + + return new Promise((resolve, reject) => { + commitMutation( + environment, + { + mutation, + variables, + onCompleted: resolve, + onError: reject, + }, + ); + }); +}; diff --git a/lib/relay-network-layer-manager.js b/lib/relay-network-layer-manager.js index 846875c862..f97ddd859b 100644 --- a/lib/relay-network-layer-manager.js +++ b/lib/relay-network-layer-manager.js @@ -152,6 +152,10 @@ export default class RelayNetworkLayerManager { let {environment, network} = relayEnvironmentPerURL.get(url) || {}; tokenPerURL.set(url, token); if (!environment) { + if (!token) { + throw new Error(`You must authenticate to ${endpoint.getHost()} first.`); + } + const source = new RecordSource(); const store = new Store(source); network = Network.create(this.getFetchQuery(endpoint, token)); diff --git a/lib/tab-group.js b/lib/tab-group.js new file mode 100644 index 0000000000..7af48f0066 --- /dev/null +++ b/lib/tab-group.js @@ -0,0 +1,80 @@ +export default class TabGroup { + constructor() { + this.nodesByElement = new Map(); + this.lastElement = null; + this.autofocusTarget = null; + } + + appendElement(element, autofocus) { + const lastNode = this.nodesByElement.get(this.lastElement) || {next: element, previous: element}; + const next = lastNode.next; + const previous = this.lastElement || element; + + this.nodesByElement.set(element, {next, previous}); + this.nodesByElement.get(lastNode.next).previous = element; + lastNode.next = element; + + this.lastElement = element; + + if (autofocus && this.autofocusTarget === null) { + this.autofocusTarget = element; + } + } + + removeElement(element) { + const node = this.nodesByElement.get(element); + if (node) { + const beforeNode = this.nodesByElement.get(node.previous); + const afterNode = this.nodesByElement.get(node.next); + + beforeNode.next = node.next; + afterNode.previous = node.previous; + } + this.nodesByElement.delete(element); + } + + after(element) { + const node = this.nodesByElement.get(element) || {next: undefined}; + return node.next; + } + + focusAfter(element) { + const original = this.getCurrentFocus(); + let next = this.after(element); + while (next && next !== element) { + next.focus(); + if (this.getCurrentFocus() !== original) { + return; + } + + next = this.after(next); + } + } + + before(element) { + const node = this.nodesByElement.get(element) || {previous: undefined}; + return node.previous; + } + + focusBefore(element) { + const original = this.getCurrentFocus(); + let previous = this.before(element); + while (previous && previous !== element) { + previous.focus(); + if (this.getCurrentFocus() !== original) { + return; + } + + previous = this.before(previous); + } + } + + autofocus() { + this.autofocusTarget && this.autofocusTarget.focus(); + } + + /* istanbul ignore next */ + getCurrentFocus() { + return document.activeElement; + } +} diff --git a/lib/views/__generated__/repositoryHomeSelectionViewQuery.graphql.js b/lib/views/__generated__/repositoryHomeSelectionViewQuery.graphql.js new file mode 100644 index 0000000000..a946febcdf --- /dev/null +++ b/lib/views/__generated__/repositoryHomeSelectionViewQuery.graphql.js @@ -0,0 +1,310 @@ +/** + * @flow + * @relayHash 7b497054797ead3f15d4ce610e26e24c + */ + +/* eslint-disable */ + +'use strict'; + +/*:: +import type { ConcreteRequest } from 'relay-runtime'; +type repositoryHomeSelectionView_user$ref = any; +export type repositoryHomeSelectionViewQueryVariables = {| + id: string, + organizationCount: number, + organizationCursor?: ?string, +|}; +export type repositoryHomeSelectionViewQueryResponse = {| + +node: ?{| + +$fragmentRefs: repositoryHomeSelectionView_user$ref + |} +|}; +export type repositoryHomeSelectionViewQuery = {| + variables: repositoryHomeSelectionViewQueryVariables, + response: repositoryHomeSelectionViewQueryResponse, +|}; +*/ + + +/* +query repositoryHomeSelectionViewQuery( + $id: ID! + $organizationCount: Int! + $organizationCursor: String +) { + node(id: $id) { + __typename + ... on User { + ...repositoryHomeSelectionView_user_12CDS5 + } + id + } +} + +fragment repositoryHomeSelectionView_user_12CDS5 on User { + id + login + avatarUrl(size: 24) + organizations(first: $organizationCount, after: $organizationCursor) { + pageInfo { + hasNextPage + endCursor + } + edges { + cursor + node { + id + login + avatarUrl(size: 24) + viewerCanCreateRepositories + __typename + } + } + } +} +*/ + +const node/*: ConcreteRequest*/ = (function(){ +var v0 = [ + { + "kind": "LocalArgument", + "name": "id", + "type": "ID!", + "defaultValue": null + }, + { + "kind": "LocalArgument", + "name": "organizationCount", + "type": "Int!", + "defaultValue": null + }, + { + "kind": "LocalArgument", + "name": "organizationCursor", + "type": "String", + "defaultValue": null + } +], +v1 = [ + { + "kind": "Variable", + "name": "id", + "variableName": "id" + } +], +v2 = { + "kind": "ScalarField", + "alias": null, + "name": "__typename", + "args": null, + "storageKey": null +}, +v3 = { + "kind": "ScalarField", + "alias": null, + "name": "id", + "args": null, + "storageKey": null +}, +v4 = { + "kind": "ScalarField", + "alias": null, + "name": "login", + "args": null, + "storageKey": null +}, +v5 = { + "kind": "ScalarField", + "alias": null, + "name": "avatarUrl", + "args": [ + { + "kind": "Literal", + "name": "size", + "value": 24 + } + ], + "storageKey": "avatarUrl(size:24)" +}, +v6 = [ + { + "kind": "Variable", + "name": "after", + "variableName": "organizationCursor" + }, + { + "kind": "Variable", + "name": "first", + "variableName": "organizationCount" + } +]; +return { + "kind": "Request", + "fragment": { + "kind": "Fragment", + "name": "repositoryHomeSelectionViewQuery", + "type": "Query", + "metadata": null, + "argumentDefinitions": (v0/*: any*/), + "selections": [ + { + "kind": "LinkedField", + "alias": null, + "name": "node", + "storageKey": null, + "args": (v1/*: any*/), + "concreteType": null, + "plural": false, + "selections": [ + { + "kind": "InlineFragment", + "type": "User", + "selections": [ + { + "kind": "FragmentSpread", + "name": "repositoryHomeSelectionView_user", + "args": [ + { + "kind": "Variable", + "name": "organizationCount", + "variableName": "organizationCount" + }, + { + "kind": "Variable", + "name": "organizationCursor", + "variableName": "organizationCursor" + } + ] + } + ] + } + ] + } + ] + }, + "operation": { + "kind": "Operation", + "name": "repositoryHomeSelectionViewQuery", + "argumentDefinitions": (v0/*: any*/), + "selections": [ + { + "kind": "LinkedField", + "alias": null, + "name": "node", + "storageKey": null, + "args": (v1/*: any*/), + "concreteType": null, + "plural": false, + "selections": [ + (v2/*: any*/), + (v3/*: any*/), + { + "kind": "InlineFragment", + "type": "User", + "selections": [ + (v4/*: any*/), + (v5/*: any*/), + { + "kind": "LinkedField", + "alias": null, + "name": "organizations", + "storageKey": null, + "args": (v6/*: any*/), + "concreteType": "OrganizationConnection", + "plural": false, + "selections": [ + { + "kind": "LinkedField", + "alias": null, + "name": "pageInfo", + "storageKey": null, + "args": null, + "concreteType": "PageInfo", + "plural": false, + "selections": [ + { + "kind": "ScalarField", + "alias": null, + "name": "hasNextPage", + "args": null, + "storageKey": null + }, + { + "kind": "ScalarField", + "alias": null, + "name": "endCursor", + "args": null, + "storageKey": null + } + ] + }, + { + "kind": "LinkedField", + "alias": null, + "name": "edges", + "storageKey": null, + "args": null, + "concreteType": "OrganizationEdge", + "plural": true, + "selections": [ + { + "kind": "ScalarField", + "alias": null, + "name": "cursor", + "args": null, + "storageKey": null + }, + { + "kind": "LinkedField", + "alias": null, + "name": "node", + "storageKey": null, + "args": null, + "concreteType": "Organization", + "plural": false, + "selections": [ + (v3/*: any*/), + (v4/*: any*/), + (v5/*: any*/), + { + "kind": "ScalarField", + "alias": null, + "name": "viewerCanCreateRepositories", + "args": null, + "storageKey": null + }, + (v2/*: any*/) + ] + } + ] + } + ] + }, + { + "kind": "LinkedHandle", + "alias": null, + "name": "organizations", + "args": (v6/*: any*/), + "handle": "connection", + "key": "RepositoryHomeSelectionView_organizations", + "filters": null + } + ] + } + ] + } + ] + }, + "params": { + "operationKind": "query", + "name": "repositoryHomeSelectionViewQuery", + "id": null, + "text": "query repositoryHomeSelectionViewQuery(\n $id: ID!\n $organizationCount: Int!\n $organizationCursor: String\n) {\n node(id: $id) {\n __typename\n ... on User {\n ...repositoryHomeSelectionView_user_12CDS5\n }\n id\n }\n}\n\nfragment repositoryHomeSelectionView_user_12CDS5 on User {\n id\n login\n avatarUrl(size: 24)\n organizations(first: $organizationCount, after: $organizationCursor) {\n pageInfo {\n hasNextPage\n endCursor\n }\n edges {\n cursor\n node {\n id\n login\n avatarUrl(size: 24)\n viewerCanCreateRepositories\n __typename\n }\n }\n }\n}\n", + "metadata": {} + } +}; +})(); +// prettier-ignore +(node/*: any*/).hash = '67e7843e3ff792e86e979cc948929ea3'; +module.exports = node; diff --git a/lib/views/__generated__/repositoryHomeSelectionView_user.graphql.js b/lib/views/__generated__/repositoryHomeSelectionView_user.graphql.js new file mode 100644 index 0000000000..d94e522482 --- /dev/null +++ b/lib/views/__generated__/repositoryHomeSelectionView_user.graphql.js @@ -0,0 +1,192 @@ +/** + * @flow + */ + +/* eslint-disable */ + +'use strict'; + +/*:: +import type { ReaderFragment } from 'relay-runtime'; +import type { FragmentReference } from "relay-runtime"; +declare export opaque type repositoryHomeSelectionView_user$ref: FragmentReference; +declare export opaque type repositoryHomeSelectionView_user$fragmentType: repositoryHomeSelectionView_user$ref; +export type repositoryHomeSelectionView_user = {| + +id: string, + +login: string, + +avatarUrl: any, + +organizations: {| + +pageInfo: {| + +hasNextPage: boolean, + +endCursor: ?string, + |}, + +edges: ?$ReadOnlyArray, + |}, + +$refType: repositoryHomeSelectionView_user$ref, +|}; +export type repositoryHomeSelectionView_user$data = repositoryHomeSelectionView_user; +export type repositoryHomeSelectionView_user$key = { + +$data?: repositoryHomeSelectionView_user$data, + +$fragmentRefs: repositoryHomeSelectionView_user$ref, +}; +*/ + + +const node/*: ReaderFragment*/ = (function(){ +var v0 = { + "kind": "ScalarField", + "alias": null, + "name": "id", + "args": null, + "storageKey": null +}, +v1 = { + "kind": "ScalarField", + "alias": null, + "name": "login", + "args": null, + "storageKey": null +}, +v2 = { + "kind": "ScalarField", + "alias": null, + "name": "avatarUrl", + "args": [ + { + "kind": "Literal", + "name": "size", + "value": 24 + } + ], + "storageKey": "avatarUrl(size:24)" +}; +return { + "kind": "Fragment", + "name": "repositoryHomeSelectionView_user", + "type": "User", + "metadata": { + "connection": [ + { + "count": "organizationCount", + "cursor": "organizationCursor", + "direction": "forward", + "path": [ + "organizations" + ] + } + ] + }, + "argumentDefinitions": [ + { + "kind": "LocalArgument", + "name": "organizationCount", + "type": "Int!", + "defaultValue": null + }, + { + "kind": "LocalArgument", + "name": "organizationCursor", + "type": "String", + "defaultValue": null + } + ], + "selections": [ + (v0/*: any*/), + (v1/*: any*/), + (v2/*: any*/), + { + "kind": "LinkedField", + "alias": "organizations", + "name": "__RepositoryHomeSelectionView_organizations_connection", + "storageKey": null, + "args": null, + "concreteType": "OrganizationConnection", + "plural": false, + "selections": [ + { + "kind": "LinkedField", + "alias": null, + "name": "pageInfo", + "storageKey": null, + "args": null, + "concreteType": "PageInfo", + "plural": false, + "selections": [ + { + "kind": "ScalarField", + "alias": null, + "name": "hasNextPage", + "args": null, + "storageKey": null + }, + { + "kind": "ScalarField", + "alias": null, + "name": "endCursor", + "args": null, + "storageKey": null + } + ] + }, + { + "kind": "LinkedField", + "alias": null, + "name": "edges", + "storageKey": null, + "args": null, + "concreteType": "OrganizationEdge", + "plural": true, + "selections": [ + { + "kind": "ScalarField", + "alias": null, + "name": "cursor", + "args": null, + "storageKey": null + }, + { + "kind": "LinkedField", + "alias": null, + "name": "node", + "storageKey": null, + "args": null, + "concreteType": "Organization", + "plural": false, + "selections": [ + (v0/*: any*/), + (v1/*: any*/), + (v2/*: any*/), + { + "kind": "ScalarField", + "alias": null, + "name": "viewerCanCreateRepositories", + "args": null, + "storageKey": null + }, + { + "kind": "ScalarField", + "alias": null, + "name": "__typename", + "args": null, + "storageKey": null + } + ] + } + ] + } + ] + } + ] +}; +})(); +// prettier-ignore +(node/*: any*/).hash = '11a1f1d0eac32bff0a3371217c0eede3'; +module.exports = node; diff --git a/lib/views/clone-dialog.js b/lib/views/clone-dialog.js index 32bb97d8d1..3ffe0d3238 100644 --- a/lib/views/clone-dialog.js +++ b/lib/views/clone-dialog.js @@ -5,9 +5,9 @@ import {TextBuffer} from 'atom'; import url from 'url'; import path from 'path'; -import AtomTextEditor from '../atom/atom-text-editor'; -import AutoFocus from '../autofocus'; +import TabGroup from '../tab-group'; import DialogView from './dialog-view'; +import {TabbableTextEditor} from './tabbable'; export default class CloneDialog extends React.Component { static propTypes = { @@ -45,7 +45,7 @@ export default class CloneDialog extends React.Component { this.destinationPath.onDidChange(this.didChangeDestinationPath), ); - this.autofocus = new AutoFocus(); + this.tabGroup = new TabGroup(); } render() { @@ -57,7 +57,7 @@ export default class CloneDialog extends React.Component { acceptText="Clone" accept={this.accept} cancel={this.props.request.cancel} - autofocus={this.autofocus} + tabGroup={this.tabGroup} inProgress={this.props.inProgress} error={this.props.error} workspace={this.props.workspace} @@ -65,19 +65,23 @@ export default class CloneDialog extends React.Component { {params.includeRemember && (