From b33c0d52d609664ee8f06c44e7d7fc90e1131d93 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Fri, 31 Jan 2020 10:58:38 -0500 Subject: [PATCH 01/36] Restore deriving contexts from the active pane item Introduce a context lock to prevent this from happening when we don't want it to. --- lib/github-package.js | 195 ++++++++++++++++++++++++++++++++---------- 1 file changed, 151 insertions(+), 44 deletions(-) diff --git a/lib/github-package.js b/lib/github-package.js index f2fb23f72d..f9cd267f83 100644 --- a/lib/github-package.js +++ b/lib/github-package.js @@ -26,6 +26,8 @@ import {reporterProxy} from './reporter-proxy'; const defaultState = { newProject: true, + activeContextPath: null, + contextLocked: false, }; export default class GithubPackage { @@ -71,6 +73,7 @@ export default class GithubPackage { this.activeContextQueue = new AsyncQueue(); this.guessedContext = WorkdirContext.guess(criteria, this.pipelineManager); this.activeContext = this.guessedContext; + this.lockedContext = null; this.workdirCache = new WorkdirCache(); this.contextPool = new WorkdirContextPool({ window, @@ -159,10 +162,10 @@ export default class GithubPackage { } async activate(state = {}) { - this.savedState = {...defaultState, ...state}; + const savedState = {...defaultState, ...state}; const firstRun = !await fileExists(this.configPath); - const newProject = this.savedState.firstRun !== undefined ? this.savedState.firstRun : this.savedState.newProject; + const newProject = savedState.firstRun !== undefined ? savedState.firstRun : savedState.newProject; this.startOpen = firstRun || newProject; this.startRevealed = firstRun && !this.config.get('welcome.showOnStartup'); @@ -175,14 +178,9 @@ export default class GithubPackage { return !!event.target.closest('.github-FilePatchListView').querySelector('.is-selected'); }; - const handleProjectPathsChange = () => { - const activeRepository = this.getActiveRepository(); - const activeRepositoryPath = activeRepository ? activeRepository.getWorkingDirectoryPath() : null; - this.scheduleActiveContextUpdate({activeRepositoryPath}); - }; - this.subscriptions.add( - this.project.onDidChangePaths(handleProjectPathsChange), + this.workspace.getCenter().onDidChangeActivePaneItem(this.handleActivePaneItemChange), + this.project.onDidChangePaths(this.handleProjectPathsChange), this.styleCalculator.startWatching( 'github-package-styles', ['editor.fontSize', 'editor.fontFamily', 'editor.lineHeight', 'editor.tabLength'], @@ -243,16 +241,33 @@ export default class GithubPackage { ); this.activated = true; - this.scheduleActiveContextUpdate(this.savedState); + this.scheduleActiveContextUpdate({ + usePath: savedState.activeContextPath, + lock: savedState.contextLocked, + }); this.rerender(); } - serialize() { - const activeRepository = this.getActiveRepository(); - const activeRepositoryPath = activeRepository ? activeRepository.getWorkingDirectoryPath() : null; + handleActivePaneItemChange = () => { + if (this.lockedContext) { + return; + } + + const itemPath = pathForPaneItem(this.workspace.getCenter().getActivePaneItem()); + this.scheduleActiveContextUpdate({ + usePath: itemPath, + lock: false, + }); + } + + handleProjectPathsChange = () => { + this.scheduleActiveContextUpdate(); + } + serialize() { return { - activeRepositoryPath, + activeContextPath: this.getActiveRepositoryPath(), + contextLocked: Boolean(this.lockedContext), newProject: false, }; } @@ -485,6 +500,14 @@ export default class GithubPackage { return this.activeContext.getRepository(); } + getActiveRepositoryPath() { + const activeRepository = this.activeContext.getRepository(); + if (!activeRepository) { + return null; + } + return activeRepository.getWorkingDirectoryPath(); + } + getActiveResolutionProgress() { return this.activeContext.getResolutionProgress(); } @@ -497,65 +520,117 @@ export default class GithubPackage { return this.switchboard; } - async scheduleActiveContextUpdate(savedState = {}) { + /** + * Enqueue a request to modify the active context. + * + * options: + * usePath - Path of the context to use as the next context, if it is present in the pool. + * lock - True or false to lock the ultimately chosen context. Omit to preserve the current lock state. + * + * This method returns a Promise that resolves when the requested context update has completed. Note that it's + * *possible* for the active context after resolution to differ from a requested `usePath`, if the workdir + * containing `usePath` is no longer a viable option, such as if it belongs to a project that is no longer present. + */ + async scheduleActiveContextUpdate(options = {}) { this.switchboard.didScheduleActiveContextUpdate(); - await this.activeContextQueue.push(this.updateActiveContext.bind(this, savedState), {parallel: false}); + await this.activeContextQueue.push(this.updateActiveContext.bind(this, options), {parallel: false}); } /** * Derive the git working directory context that should be used for the package's git operations based on the current * state of the Atom workspace. In priority, this prefers: * - * - The preferred git working directory set by the user (This is also the working directory that was active when the - * package was last serialized). - * - A git working directory corresponding to "first" Project, whether or not there is a single project or multiple. + * - When activating: the working directory that was active when the package was last serialized, if it still a viable + * option. (usePath) + * - The working directory chosen by the user from the context tile on the git or GitHub tabs. (usePath) + * - The working directory containing the path of the active pane item. + * - A git working directory corresponding to "first" project, if any projects are open. * - The current context, unchanged, which may be a `NullWorkdirContext`. * * First updates the pool of resident contexts to match all git working directories that correspond to open - * projects. + * projects and pane items. */ - async getNextContext(savedState) { + async getNextContext(usePath = null) { + // Identify paths that *could* contribute a git working directory to the pool. This is drawn from + // the roots of open projects, the currently locked context if one is present, and the path of the + // open workspace item. + const candidatePaths = new Set(this.project.getPaths()); + if (this.lockedContext) { + const lockedRepo = this.lockedContext.getRepository(); + if (lockedRepo) { + candidatePaths.add(lockedRepo.getWorkingDirectoryPath()); + } + } + const activeItemPath = pathForPaneItem(this.workspace.getCenter().getActivePaneItem()); + if (activeItemPath) { + candidatePaths.add(activeItemPath); + } + + let activeItemWorkdir = null; + let firstProjectWorkdir = null; + + // Convert the candidate paths into the set of viable git working directories, by means of a cached + // `git rev-parse` call. Candidate paths that are not contained within a git working directory will + // be preserved as-is within the pool, to allow users to initialize them. const workdirs = new Set( await Promise.all( - this.project.getPaths().map(async projectPath => { - const workdir = await this.workdirCache.find(projectPath); - return workdir || projectPath; + Array.from(candidatePaths, async candidatePath => { + const workdir = (await this.workdirCache.find(candidatePath)) || candidatePath; + // Note the workdirs associated with the active pane item and the first open project so we can + // prefer them later. + if (candidatePath === activeItemPath) { + activeItemWorkdir = workdir; + } else if (candidatePath === this.project.getPaths()[0]) { + firstProjectWorkdir = workdir; + } + return workdir; }), ), ); - // Update pool with the open projects - this.contextPool.set(workdirs, savedState); + // Update pool with the identified projects. + this.contextPool.set(workdirs); - if (savedState.activeRepositoryPath) { - // Preferred git directory (the preferred directory or the last serialized directory). - const stateContext = this.contextPool.getContext(savedState.activeRepositoryPath); - // If the context exists chose it, else continue. + // 1 - Explicitly requested workdir. This is either selected by the user from a context tile or + // deserialized from package state. Choose this context only if it still exists in the pool. + if (usePath) { + const stateContext = this.contextPool.getContext(usePath); if (stateContext.isPresent()) { return stateContext; } } - const projectPaths = this.project.getPaths(); + // 2 - Follow the active workspace pane item. + if (activeItemWorkdir) { + return this.contextPool.getContext(activeItemWorkdir); + } - if (projectPaths.length >= 1) { - // Single or multiple projects (just choose the first, the user can select after) - const projectPath = projectPaths[0]; - const activeWorkingDir = await this.workdirCache.find(projectPath); - return this.contextPool.getContext(activeWorkingDir || projectPath); + // 3 - The first open project. + if (firstProjectWorkdir) { + return this.contextPool.getContext(firstProjectWorkdir); } - if (projectPaths.length === 0 && !this.activeContext.getRepository().isUndetermined()) { - // No projects. Revert to the absent context unless we've guessed that more projects are on the way. + // No projects. Revert to the absent context unless we've guessed that more projects are on the way. + if (this.project.getPaths().length === 0 && !this.activeContext.getRepository().isUndetermined()) { return WorkdirContext.absent({pipelineManager: this.pipelineManager}); } - // It is only possible to reach here if there there was no preferred directory, there are no project paths and the - // the active context's repository is not undetermined. + // It is only possible to reach here if there there was no preferred directory, there are no project paths, and the + // the active context's repository is not undetermined. Preserve the existing active context. return this.activeContext; } - setActiveContext(nextActiveContext) { + /** + * Modify the active context and re-render the React tree. This should only be done as part of the + * context update queue; use scheduleActiveContextUpdate() to do this. + * + * nextActiveContext - The WorkdirContext to make active next, as derived from the current workspace + * state by getNextContext(). This may be absent or undetermined. + * lock - If true, also set this context as the "locked" one and engage the context lock if it isn't + * already. If false, clear any existing context lock. If null or undefined, leave the lock in its + * existing state. + */ + setActiveContext(nextActiveContext, lock) { if (nextActiveContext !== this.activeContext) { if (this.activeContext === this.guessedContext) { this.guessedContext.destroy(); @@ -569,17 +644,30 @@ export default class GithubPackage { } else { this.switchboard.didFinishActiveContextUpdate(); } + + if (lock === true) { + this.lockedContext = this.activeContext; + } else if (lock === false) { + this.lockedContext = null; + } } - async updateActiveContext(savedState = {}) { + /** + * Derive the next active context with getNextContext(), then enact the context change with setActiveContext(). + * + * options: + * usePath - Path of the context to use as the next context, if it is present in the pool. + * lock - True or false to lock the ultimately chosen context. Omit to preserve the current lock state. + */ + async updateActiveContext(options = {}) { if (this.workspace.isDestroyed()) { return; } this.switchboard.didBeginActiveContextUpdate(); - const nextActiveContext = await this.getNextContext(savedState); - this.setActiveContext(nextActiveContext); + const nextActiveContext = await this.getNextContext(options.usePath); + this.setActiveContext(nextActiveContext, options.lock); } async refreshAtomGitRepository(workdir) { @@ -594,3 +682,22 @@ export default class GithubPackage { } } } + +function pathForPaneItem(paneItem) { + if (!paneItem) { + return null; + } + + // Likely GitHub package provided pane item + if (typeof paneItem.getWorkingDirectory === 'function') { + return paneItem.getWorkingDirectory(); + } + + // TextEditor-like + if (typeof paneItem.getPath === 'function') { + return paneItem.getPath(); + } + + // Oh well + return null; +} From 946a8962d4ad9b460243fc1085f1c6f624b68f00 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Fri, 31 Jan 2020 15:41:16 -0500 Subject: [PATCH 02/36] Pass context lock and setter method into React tree --- lib/github-package.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/github-package.js b/lib/github-package.js index f9cd267f83..3e30be43f5 100644 --- a/lib/github-package.js +++ b/lib/github-package.js @@ -290,7 +290,11 @@ export default class GithubPackage { } const changeWorkingDirectory = workingDirectory => { - this.scheduleActiveContextUpdate({activeRepositoryPath: workingDirectory}); + this.scheduleActiveContextUpdate({usePath: workingDirectory}); + }; + + const setContextLock = (workingDirectory, lock) => { + this.scheduleActiveContextUpdate({usePath: workingDirectory, lock}); }; this.renderFn( @@ -319,7 +323,9 @@ export default class GithubPackage { startRevealed={this.startRevealed} removeFilePatchItem={this.removeFilePatchItem} currentWorkDir={this.getActiveWorkdir()} + contextLocked={this.lockedContext !== null} changeWorkingDirectory={changeWorkingDirectory} + setContextLock={setContextLock} />, this.element, callback, ); } From 77626abe78d75a631297fad5fc2145cc81cb40e7 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Fri, 31 Jan 2020 15:41:41 -0500 Subject: [PATCH 03/36] Pass context props to Git and GitHub tab items --- lib/controllers/root-controller.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/controllers/root-controller.js b/lib/controllers/root-controller.js index 708fdc3561..7ba6779ae3 100644 --- a/lib/controllers/root-controller.js +++ b/lib/controllers/root-controller.js @@ -67,7 +67,9 @@ export default class RootController extends React.Component { clone: PropTypes.func.isRequired, // Control + contextLocked: PropTypes.bool.isRequired, changeWorkingDirectory: PropTypes.func.isRequired, + setContextLock: PropTypes.func.isRequired, startOpen: PropTypes.bool, startRevealed: PropTypes.bool, } @@ -294,7 +296,9 @@ export default class RootController extends React.Component { currentWorkDir={this.props.currentWorkDir} getCurrentWorkDirs={getCurrentWorkDirs} onDidChangeWorkDirs={onDidChangeWorkDirs} + contextLocked={this.props.contextLocked} changeWorkingDirectory={this.props.changeWorkingDirectory} + setContextLock={this.props.setContextLock} /> )} @@ -311,7 +315,9 @@ export default class RootController extends React.Component { currentWorkDir={this.props.currentWorkDir} getCurrentWorkDirs={getCurrentWorkDirs} onDidChangeWorkDirs={onDidChangeWorkDirs} + contextLocked={this.props.contextLocked} changeWorkingDirectory={this.props.changeWorkingDirectory} + setContextLock={this.props.setContextLock} openCreateDialog={this.openCreateDialog} openPublishDialog={this.openPublishDialog} openCloneDialog={this.openCloneDialog} From a9f35b098af247afa85ba1c1ddfcf38d68c6aea3 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Fri, 31 Jan 2020 15:42:05 -0500 Subject: [PATCH 04/36] Drill context methods into Git tab --- lib/controllers/git-tab-controller.js | 4 ++ lib/controllers/git-tab-header-controller.js | 59 ++++++++++++++++++-- lib/views/git-tab-header-view.js | 9 +++ lib/views/git-tab-view.js | 4 ++ 4 files changed, 70 insertions(+), 6 deletions(-) diff --git a/lib/controllers/git-tab-controller.js b/lib/controllers/git-tab-controller.js index 3b3a93e553..070abd08e5 100644 --- a/lib/controllers/git-tab-controller.js +++ b/lib/controllers/git-tab-controller.js @@ -51,7 +51,9 @@ export default class GitTabController extends React.Component { openFiles: PropTypes.func.isRequired, openInitializeDialog: PropTypes.func.isRequired, controllerRef: RefHolderPropType, + contextLocked: PropTypes.bool.isRequired, changeWorkingDirectory: PropTypes.func.isRequired, + setContextLock: PropTypes.func.isRequired, onDidChangeWorkDirs: PropTypes.func.isRequired, getCurrentWorkDirs: PropTypes.func.isRequired, }; @@ -122,7 +124,9 @@ export default class GitTabController extends React.Component { openFiles={this.props.openFiles} discardWorkDirChangesForPaths={this.props.discardWorkDirChangesForPaths} undoLastDiscard={this.props.undoLastDiscard} + contextLocked={this.props.contextLocked} changeWorkingDirectory={this.props.changeWorkingDirectory} + setContextLock={this.props.setContextLock} getCurrentWorkDirs={this.props.getCurrentWorkDirs} onDidChangeWorkDirs={this.props.onDidChangeWorkDirs} diff --git a/lib/controllers/git-tab-header-controller.js b/lib/controllers/git-tab-header-controller.js index b0a6831531..40870550af 100644 --- a/lib/controllers/git-tab-header-controller.js +++ b/lib/controllers/git-tab-header-controller.js @@ -11,9 +11,11 @@ export default class GitTabHeaderController extends React.Component { // Workspace currentWorkDir: PropTypes.string, getCurrentWorkDirs: PropTypes.func.isRequired, + changeWorkingDirectory: PropTypes.func.isRequired, + contextLocked: PropTypes.bool.isRequired, + setContextLock: PropTypes.func.isRequired, // Event Handlers - handleWorkDirSelect: PropTypes.func.isRequired, onDidChangeWorkDirs: PropTypes.func.isRequired, onDidUpdateRepo: PropTypes.func.isRequired, } @@ -21,11 +23,16 @@ export default class GitTabHeaderController extends React.Component { constructor(props) { super(props); this._isMounted = false; - this.state = {currentWorkDirs: [], committer: nullAuthor}; + this.state = { + currentWorkDirs: [], + committer: nullAuthor, + changingLock: null, + changingWorkDir: null, + }; this.disposable = new CompositeDisposable(); } - static getDerivedStateFromProps(props, state) { + static getDerivedStateFromProps(props) { return { currentWorkDirs: props.getCurrentWorkDirs(), }; @@ -59,17 +66,49 @@ export default class GitTabHeaderController extends React.Component { committer={this.state.committer} // Workspace - workdir={this.props.currentWorkDir} + workdir={this.getWorkDir()} workdirs={this.state.currentWorkDirs} + contextLocked={this.getLocked()} + changingWorkDir={this.state.changingWorkDir !== null} + changingLock={this.state.changingLock !== null} // Event Handlers - handleWorkDirSelect={this.props.handleWorkDirSelect} + handleWorkDirSelect={this.handleWorkDirSelect} + handleLockToggle={this.handleLockToggle} /> ); } + handleLockToggle = async () => { + if (this.state.changingLock !== null) { + return; + } + + const nextLock = !this.props.contextLocked; + try { + this.setState({changingLock: nextLock}); + await this.props.setContextLock(this.state.changingWorkDir || this.props.currentWorkDir, nextLock); + } finally { + await new Promise(resolve => this.setState({changingLock: null}, resolve)); + } + } + + handleWorkDirSelect = async e => { + if (this.state.changingWorkDir !== null) { + return; + } + + const nextWorkDir = e.target.value; + try { + this.setState({changingWorkDir: nextWorkDir}); + await this.props.changeWorkingDirectory(nextWorkDir); + } finally { + await new Promise(resolve => this.setState({changingWork: null}, resolve)); + } + } + resetWorkDirs = () => { - this.setState((state, props) => ({ + this.setState(() => ({ currentWorkDirs: [], })); } @@ -81,6 +120,14 @@ export default class GitTabHeaderController extends React.Component { } } + getWorkDir() { + return this.state.changeWorkDir !== null ? this.state.changingWorkDir : this.props.currentWorkDir; + } + + getLocked() { + return this.state.changingLock !== null ? this.state.changingLock : this.props.contextLocked; + } + componentWillUnmount() { this._isMounted = false; this.disposable.dispose(); diff --git a/lib/views/git-tab-header-view.js b/lib/views/git-tab-header-view.js index 78a71a990c..9933b6bab7 100644 --- a/lib/views/git-tab-header-view.js +++ b/lib/views/git-tab-header-view.js @@ -1,7 +1,9 @@ import React from 'react'; import PropTypes from 'prop-types'; import path from 'path'; + import {AuthorPropType} from '../prop-types'; +import Octicon from '../atom/octicon'; export default class GitTabHeaderView extends React.Component { static propTypes = { @@ -10,9 +12,13 @@ export default class GitTabHeaderView extends React.Component { // Workspace workdir: PropTypes.string, workdirs: PropTypes.shape({[Symbol.iterator]: PropTypes.func.isRequired}).isRequired, + contextLocked: PropTypes.bool.isRequired, + changingWorkDir: PropTypes.bool.isRequired, + changingLock: PropTypes.bool.isRequired, // Event Handlers handleWorkDirSelect: PropTypes.func, + handleLockToggle: PropTypes.func, } render() { @@ -24,6 +30,9 @@ export default class GitTabHeaderView extends React.Component { onChange={this.props.handleWorkDirSelect ? this.props.handleWorkDirSelect : () => {}}> {this.renderWorkDirs()} + ); } diff --git a/lib/views/git-tab-view.js b/lib/views/git-tab-view.js index a5f10cc687..c6004e2a23 100644 --- a/lib/views/git-tab-view.js +++ b/lib/views/git-tab-view.js @@ -61,7 +61,9 @@ export default class GitTabView extends React.Component { attemptFileStageOperation: PropTypes.func.isRequired, discardWorkDirChangesForPaths: PropTypes.func.isRequired, openFiles: PropTypes.func.isRequired, + contextLocked: PropTypes.bool.isRequired, changeWorkingDirectory: PropTypes.func.isRequired, + setContextLock: PropTypes.func.isRequired, onDidChangeWorkDirs: PropTypes.func.isRequired, getCurrentWorkDirs: PropTypes.func.isRequired, }; @@ -126,11 +128,13 @@ export default class GitTabView extends React.Component { // Workspace currentWorkDir={this.props.workingDirectoryPath} getCurrentWorkDirs={this.props.getCurrentWorkDirs} + contextLocked={this.props.contextLocked} // Event Handlers onDidChangeWorkDirs={this.props.onDidChangeWorkDirs} onDidUpdateRepo={repository.onDidUpdate.bind(repository)} handleWorkDirSelect={e => this.props.changeWorkingDirectory(e.target.value)} + setContextLock={this.props.setContextLock} /> ); } From bf8cbd162fb2076913d494cbae3ec1861e7868d5 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Fri, 31 Jan 2020 15:42:21 -0500 Subject: [PATCH 05/36] Drill context props into GitHub tab components --- lib/containers/github-tab-header-container.js | 9 ++- lib/controllers/github-tab-controller.js | 4 ++ .../github-tab-header-controller.js | 58 ++++++++++++++++--- lib/views/github-tab-header-view.js | 13 +++-- lib/views/github-tab-view.js | 11 +++- 5 files changed, 79 insertions(+), 16 deletions(-) diff --git a/lib/containers/github-tab-header-container.js b/lib/containers/github-tab-header-container.js index c421ef68b0..101a5c16dd 100644 --- a/lib/containers/github-tab-header-container.js +++ b/lib/containers/github-tab-header-container.js @@ -17,10 +17,12 @@ export default class GithubTabHeaderContainer extends React.Component { // Workspace currentWorkDir: PropTypes.string, + contextLocked: PropTypes.bool.isRequired, + changeWorkingDirectory: PropTypes.func.isRequired, + setContextLock: PropTypes.func.isRequired, getCurrentWorkDirs: PropTypes.func.isRequired, // Event Handlers - handleWorkDirSelect: PropTypes.func, onDidChangeWorkDirs: PropTypes.func, } @@ -81,7 +83,6 @@ export default class GithubTabHeaderContainer extends React.Component { getCurrentWorkDirs={this.props.getCurrentWorkDirs} // Event Handlers - handleWorkDirSelect={this.props.handleWorkDirSelect} onDidChangeWorkDirs={this.props.onDidChangeWorkDirs} /> ); @@ -94,10 +95,12 @@ export default class GithubTabHeaderContainer extends React.Component { // Workspace currentWorkDir={this.props.currentWorkDir} + contextLocked={this.props.contextLocked} + changeWorkingDirectory={this.props.changeWorkingDirectory} + setContextLock={this.props.setContextLock} getCurrentWorkDirs={this.props.getCurrentWorkDirs} // Event Handlers - handleWorkDirSelect={this.props.handleWorkDirSelect} onDidChangeWorkDirs={this.props.onDidChangeWorkDirs} /> ); diff --git a/lib/controllers/github-tab-controller.js b/lib/controllers/github-tab-controller.js index 320d48b328..ea63951897 100644 --- a/lib/controllers/github-tab-controller.js +++ b/lib/controllers/github-tab-controller.js @@ -24,6 +24,8 @@ export default class GitHubTabController extends React.Component { currentWorkDir: PropTypes.string, changeWorkingDirectory: PropTypes.func.isRequired, + setContextLock: PropTypes.func.isRequired, + contextLocked: PropTypes.bool.isRequired, onDidChangeWorkDirs: PropTypes.func.isRequired, getCurrentWorkDirs: PropTypes.func.isRequired, openCreateDialog: PropTypes.func.isRequired, @@ -54,6 +56,7 @@ export default class GitHubTabController extends React.Component { rootHolder={this.props.rootHolder} workingDirectory={this.props.workingDirectory || this.props.currentWorkDir} + contextLocked={this.props.contextLocked} repository={this.props.repository} branches={this.props.branches} currentBranch={currentBranch} @@ -67,6 +70,7 @@ export default class GitHubTabController extends React.Component { handlePushBranch={this.handlePushBranch} handleRemoteSelect={this.handleRemoteSelect} changeWorkingDirectory={this.props.changeWorkingDirectory} + setContextLock={this.props.setContextLock} getCurrentWorkDirs={this.props.getCurrentWorkDirs} onDidChangeWorkDirs={this.props.onDidChangeWorkDirs} openCreateDialog={this.props.openCreateDialog} diff --git a/lib/controllers/github-tab-header-controller.js b/lib/controllers/github-tab-header-controller.js index 78f4fba875..a084bf7e8c 100644 --- a/lib/controllers/github-tab-header-controller.js +++ b/lib/controllers/github-tab-header-controller.js @@ -9,19 +9,26 @@ export default class GithubTabHeaderController extends React.Component { // Workspace currentWorkDir: PropTypes.string, + contextLocked: PropTypes.bool.isRequired, + changeWorkingDirectory: PropTypes.func.isRequired, + setContextLock: PropTypes.func.isRequired, getCurrentWorkDirs: PropTypes.func.isRequired, // Event Handlers - handleWorkDirSelect: PropTypes.func.isRequired, onDidChangeWorkDirs: PropTypes.func.isRequired, } constructor(props) { super(props); - this.state = {currentWorkDirs: []}; + + this.state = { + currentWorkDirs: [], + changingLock: null, + changingWorkDir: null, + }; } - static getDerivedStateFromProps(props, state) { + static getDerivedStateFromProps(props) { return { currentWorkDirs: props.getCurrentWorkDirs(), }; @@ -46,21 +53,58 @@ export default class GithubTabHeaderController extends React.Component { user={this.props.user} // Workspace - workdir={this.props.currentWorkDir} workdirs={this.state.currentWorkDirs} + workdir={this.getWorkDir()} + contextLocked={this.getContextLocked()} - // Event Handlers - handleWorkDirSelect={this.props.handleWorkDirSelect} + handleWorkDirChange={this.handleWorkDirChange} + handleLockToggle={this.handleLockToggle} /> ); } resetWorkDirs = () => { - this.setState((state, props) => ({ + this.setState(() => ({ currentWorkDirs: [], })); } + handleLockToggle = async () => { + if (this.state.changingLock !== null) { + return; + } + + const nextLock = !this.props.contextLocked; + try { + this.setState({changingLock: nextLock}); + await this.props.setContextLock(this.state.changingWorkDir || this.props.currentWorkDir, nextLock); + } finally { + await new Promise(resolve => this.setState({changingLock: null}, resolve)); + } + } + + handleWorkDirChange = async e => { + if (this.state.changingWorkDir !== null) { + return; + } + + const nextWorkDir = e.target.value; + try { + this.setState({changingWorkDir: nextWorkDir}); + await this.props.changeWorkingDirectory(nextWorkDir); + } finally { + await new Promise(resolve => this.setState({changingWork: null}, resolve)); + } + } + + getWorkDir() { + return this.state.changingWorkDir !== null ? this.state.changingWorkDir : this.props.currentWorkDir; + } + + getContextLocked() { + return this.state.changingLock !== null ? this.state.changingLock : this.props.contextLocked; + } + componentWillUnmount() { this.disposable.dispose(); } diff --git a/lib/views/github-tab-header-view.js b/lib/views/github-tab-header-view.js index fdcfa69b8a..dd6827fff6 100644 --- a/lib/views/github-tab-header-view.js +++ b/lib/views/github-tab-header-view.js @@ -1,7 +1,9 @@ import React from 'react'; import PropTypes from 'prop-types'; import path from 'path'; + import {AuthorPropType} from '../prop-types'; +import Octicon from '../atom/octicon'; export default class GithubTabHeaderView extends React.Component { static propTypes = { @@ -10,9 +12,9 @@ export default class GithubTabHeaderView extends React.Component { // Workspace workdir: PropTypes.string, workdirs: PropTypes.shape({[Symbol.iterator]: PropTypes.func.isRequired}).isRequired, - - // Event Handlers - handleWorkDirSelect: PropTypes.func, + contextLocked: PropTypes.bool.isRequired, + handleWorkDirChange: PropTypes.func.isRequired, + handleLockToggle: PropTypes.func.isRequired, } render() { @@ -21,9 +23,12 @@ export default class GithubTabHeaderView extends React.Component { {this.renderUser()} + ); } diff --git a/lib/views/github-tab-view.js b/lib/views/github-tab-view.js index a0add89b17..3aaced7a74 100644 --- a/lib/views/github-tab-view.js +++ b/lib/views/github-tab-view.js @@ -28,6 +28,8 @@ export default class GitHubTabView extends React.Component { workingDirectory: PropTypes.string, getCurrentWorkDirs: PropTypes.func.isRequired, changeWorkingDirectory: PropTypes.func.isRequired, + contextLocked: PropTypes.bool.isRequired, + setContextLock: PropTypes.func.isRequired, repository: PropTypes.object.isRequired, // Remotes @@ -137,10 +139,13 @@ export default class GitHubTabView extends React.Component { // Workspace currentWorkDir={this.props.workingDirectory} + contextLocked={this.props.contextLocked} + changeWorkingDirectory={this.props.changeWorkingDirectory} + setContextLock={this.props.setContextLock} getCurrentWorkDirs={this.props.getCurrentWorkDirs} // Event Handlers - handleWorkDirSelect={e => this.props.changeWorkingDirectory(e.target.value)} + // handleWorkDirSelect={e => this.props.changeWorkingDirectory(e.target.value)} onDidChangeWorkDirs={this.props.onDidChangeWorkDirs} /> ); @@ -151,10 +156,12 @@ export default class GitHubTabView extends React.Component { // Workspace currentWorkDir={this.props.workingDirectory} + contextLocked={this.props.contextLocked} + changeWorkingDirectory={this.props.changeWorkingDirectory} + setContextLock={this.props.setContextLock} getCurrentWorkDirs={this.props.getCurrentWorkDirs} // Event Handlers - handleWorkDirSelect={e => this.props.changeWorkingDirectory(e.target.value)} onDidChangeWorkDirs={this.props.onDidChangeWorkDirs} /> ); From 03e3b3fb7254804f36920f60569edfa1dc3b2a61 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Sat, 1 Feb 2020 20:10:52 -0500 Subject: [PATCH 06/36] Make the lock button borderless and backgroundless --- styles/project.less | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/styles/project.less b/styles/project.less index e425cba4e6..d88b86d2d9 100644 --- a/styles/project.less +++ b/styles/project.less @@ -17,4 +17,9 @@ border-radius: @component-border-radius; } + &-lock.btn { + border: none; + background-color: transparent; + background-image: none; + } } From 7411a35fe6e8d161a0e9f943a5e5537712ffac39 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Sun, 2 Feb 2020 11:44:42 -0500 Subject: [PATCH 07/36] Return context update promises --- lib/github-package.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/github-package.js b/lib/github-package.js index 3e30be43f5..547c2b6b4b 100644 --- a/lib/github-package.js +++ b/lib/github-package.js @@ -290,11 +290,11 @@ export default class GithubPackage { } const changeWorkingDirectory = workingDirectory => { - this.scheduleActiveContextUpdate({usePath: workingDirectory}); + return this.scheduleActiveContextUpdate({usePath: workingDirectory}); }; const setContextLock = (workingDirectory, lock) => { - this.scheduleActiveContextUpdate({usePath: workingDirectory, lock}); + return this.scheduleActiveContextUpdate({usePath: workingDirectory, lock}); }; this.renderFn( From 9335d9e390f77fe05d4057005d2f73eb27c08c6e Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Sun, 2 Feb 2020 11:45:02 -0500 Subject: [PATCH 08/36] Remove some unused parameters --- lib/containers/github-tab-header-container.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/containers/github-tab-header-container.js b/lib/containers/github-tab-header-container.js index 101a5c16dd..c8d2ad36c9 100644 --- a/lib/containers/github-tab-header-container.js +++ b/lib/containers/github-tab-header-container.js @@ -61,12 +61,12 @@ export default class GithubTabHeaderContainer extends React.Component { environment={environment} variables={{}} query={query} - render={result => this.renderWithResult(result, token)} + render={result => this.renderWithResult(result)} /> ); } - renderWithResult({error, props, retry}, token) { + renderWithResult({error, props}) { if (error || props === null) { return this.renderNoResult(); } From cd6b12863f2b54878f22398105ff8f7be95350a0 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Sun, 2 Feb 2020 11:45:19 -0500 Subject: [PATCH 09/36] Modify lockedContext before re-rendering --- lib/github-package.js | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/lib/github-package.js b/lib/github-package.js index 547c2b6b4b..5d8867af70 100644 --- a/lib/github-package.js +++ b/lib/github-package.js @@ -643,6 +643,23 @@ export default class GithubPackage { this.guessedContext = null; } this.activeContext = nextActiveContext; + if (lock === true) { + this.lockedContext = this.activeContext; + } else if (lock === false) { + this.lockedContext = null; + } + + this.rerender(() => { + this.switchboard.didFinishContextChangeRender(); + this.switchboard.didFinishActiveContextUpdate(); + }); + } else if ((lock === true || lock === false) && lock !== (this.lockedContext !== null)) { + if (lock) { + this.lockedContext = this.activeContext; + } else { + this.lockedContext = null; + } + this.rerender(() => { this.switchboard.didFinishContextChangeRender(); this.switchboard.didFinishActiveContextUpdate(); @@ -650,12 +667,6 @@ export default class GithubPackage { } else { this.switchboard.didFinishActiveContextUpdate(); } - - if (lock === true) { - this.lockedContext = this.activeContext; - } else if (lock === false) { - this.lockedContext = null; - } } /** From 000a0690dc7980d01254428a2e2547ee3cb7599a Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Sun, 2 Feb 2020 11:45:34 -0500 Subject: [PATCH 10/36] Pass missing props --- lib/containers/github-tab-header-container.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/containers/github-tab-header-container.js b/lib/containers/github-tab-header-container.js index c8d2ad36c9..8bab9d4e8f 100644 --- a/lib/containers/github-tab-header-container.js +++ b/lib/containers/github-tab-header-container.js @@ -80,7 +80,10 @@ export default class GithubTabHeaderContainer extends React.Component { // Workspace currentWorkDir={this.props.currentWorkDir} + contextLocked={this.props.contextLocked} getCurrentWorkDirs={this.props.getCurrentWorkDirs} + changeWorkingDirectory={this.props.changeWorkingDirectory} + setContextLock={this.props.setContextLock} // Event Handlers onDidChangeWorkDirs={this.props.onDidChangeWorkDirs} From 51aa5a7bd6b8774528fb79d54a3cbc60a6963da5 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Sun, 2 Feb 2020 11:45:51 -0500 Subject: [PATCH 11/36] Correct getWorkDir() typo --- lib/controllers/git-tab-header-controller.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/controllers/git-tab-header-controller.js b/lib/controllers/git-tab-header-controller.js index 40870550af..1d85fa8375 100644 --- a/lib/controllers/git-tab-header-controller.js +++ b/lib/controllers/git-tab-header-controller.js @@ -87,7 +87,7 @@ export default class GitTabHeaderController extends React.Component { const nextLock = !this.props.contextLocked; try { this.setState({changingLock: nextLock}); - await this.props.setContextLock(this.state.changingWorkDir || this.props.currentWorkDir, nextLock); + await this.props.setContextLock(this.getWorkDir(), nextLock); } finally { await new Promise(resolve => this.setState({changingLock: null}, resolve)); } @@ -121,7 +121,7 @@ export default class GitTabHeaderController extends React.Component { } getWorkDir() { - return this.state.changeWorkDir !== null ? this.state.changingWorkDir : this.props.currentWorkDir; + return this.state.changingWorkDir !== null ? this.state.changingWorkDir : this.props.currentWorkDir; } getLocked() { From 0006b364dc845969e350abeda9ff49f5773ffd56 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Sun, 2 Feb 2020 11:46:03 -0500 Subject: [PATCH 12/36] Pass correct props --- lib/views/git-tab-view.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/views/git-tab-view.js b/lib/views/git-tab-view.js index c6004e2a23..93c7fa46a0 100644 --- a/lib/views/git-tab-view.js +++ b/lib/views/git-tab-view.js @@ -129,12 +129,12 @@ export default class GitTabView extends React.Component { currentWorkDir={this.props.workingDirectoryPath} getCurrentWorkDirs={this.props.getCurrentWorkDirs} contextLocked={this.props.contextLocked} + changeWorkingDirectory={this.props.changeWorkingDirectory} + setContextLock={this.props.setContextLock} // Event Handlers onDidChangeWorkDirs={this.props.onDidChangeWorkDirs} onDidUpdateRepo={repository.onDidUpdate.bind(repository)} - handleWorkDirSelect={e => this.props.changeWorkingDirectory(e.target.value)} - setContextLock={this.props.setContextLock} /> ); } From 2ab6f7e8cae4356d1f5813a44a23b0da51da5844 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Sun, 2 Feb 2020 11:46:26 -0500 Subject: [PATCH 13/36] Context tile button/select enablement --- lib/views/git-tab-header-view.js | 9 ++++++--- lib/views/github-tab-header-view.js | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/lib/views/git-tab-header-view.js b/lib/views/git-tab-header-view.js index 9933b6bab7..1ecb0f1d61 100644 --- a/lib/views/git-tab-header-view.js +++ b/lib/views/git-tab-header-view.js @@ -27,11 +27,14 @@ export default class GitTabHeaderView extends React.Component { {this.renderCommitter()} - ); diff --git a/lib/views/github-tab-header-view.js b/lib/views/github-tab-header-view.js index dd6827fff6..e161ea3888 100644 --- a/lib/views/github-tab-header-view.js +++ b/lib/views/github-tab-header-view.js @@ -27,7 +27,7 @@ export default class GithubTabHeaderView extends React.Component { {this.renderWorkDirs()} ); From 47e4e61ddb4251f6a5b8307573d725a691d082f6 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Sun, 2 Feb 2020 11:49:30 -0500 Subject: [PATCH 14/36] State key typo --- lib/controllers/git-tab-header-controller.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/controllers/git-tab-header-controller.js b/lib/controllers/git-tab-header-controller.js index 1d85fa8375..78d4b8f5b7 100644 --- a/lib/controllers/git-tab-header-controller.js +++ b/lib/controllers/git-tab-header-controller.js @@ -103,7 +103,7 @@ export default class GitTabHeaderController extends React.Component { this.setState({changingWorkDir: nextWorkDir}); await this.props.changeWorkingDirectory(nextWorkDir); } finally { - await new Promise(resolve => this.setState({changingWork: null}, resolve)); + await new Promise(resolve => this.setState({changingWorkDir: null}, resolve)); } } From 46266795824375836895516011ab9f059e83b105 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Sun, 2 Feb 2020 12:06:59 -0500 Subject: [PATCH 15/36] GitTabHeaderView tests --- test/views/git-tab-header-view.test.js | 48 +++++++++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/test/views/git-tab-header-view.test.js b/test/views/git-tab-header-view.test.js index 5e0d9e8f7b..23fe60303e 100644 --- a/test/views/git-tab-header-view.test.js +++ b/test/views/git-tab-header-view.test.js @@ -15,7 +15,13 @@ describe('GitTabHeaderView', function() { function build(options = {}) { const props = { committer: nullAuthor, + workdir: null, workdirs: createWorkdirs([]), + contextLocked: false, + changingWorkDir: false, + changingLock: false, + handleWorkDirSelect: () => {}, + handleLockToggle: () => {}, ...options, }; return shallow(); @@ -29,7 +35,11 @@ describe('GitTabHeaderView', function() { beforeEach(function() { select = sinon.spy(); - wrapper = build({handleWorkDirSelect: select, workdirs: createWorkdirs(paths), workdir: path2}); + wrapper = build({ + handleWorkDirSelect: select, + workdirs: createWorkdirs(paths), + workdir: path2, + }); }); it('renders an option for all given working directories', function() { @@ -49,6 +59,42 @@ describe('GitTabHeaderView', function() { }); }); + describe('context lock control', function() { + it('renders locked when the lock is engaged', function() { + const wrapper = build({contextLocked: true}); + + assert.isTrue(wrapper.exists('.icon-lock')); + }); + + it('renders unlocked when the lock is disengaged', function() { + const wrapper = build({contextLocked: false}); + + assert.isTrue(wrapper.exists('.icon-globe')); + }); + + it('calls handleLockToggle when the lock is clicked', function() { + const handleLockToggle = sinon.spy(); + const wrapper = build({handleLockToggle}); + + wrapper.find('button').simulate('click'); + assert.isTrue(handleLockToggle.called); + }); + }); + + describe('when changes are in progress', function() { + it('disables the workdir select while the workdir is changing', function() { + const wrapper = build({changingWorkDir: true}); + + assert.isTrue(wrapper.find('select').prop('disabled')); + }); + + it('disables the context lock toggle while the context lock is changing', function() { + const wrapper = build({changingLock: true}); + + assert.isTrue(wrapper.find('button').prop('disabled')); + }) + }); + describe('with falsish props', function() { let wrapper; From 610edb16e7349cea42cc63eee637f11c20140170 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Sun, 2 Feb 2020 12:09:08 -0500 Subject: [PATCH 16/36] Right shallow renderer --- test/views/git-tab-header-view.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/views/git-tab-header-view.test.js b/test/views/git-tab-header-view.test.js index 23fe60303e..23d0523eee 100644 --- a/test/views/git-tab-header-view.test.js +++ b/test/views/git-tab-header-view.test.js @@ -63,13 +63,13 @@ describe('GitTabHeaderView', function() { it('renders locked when the lock is engaged', function() { const wrapper = build({contextLocked: true}); - assert.isTrue(wrapper.exists('.icon-lock')); + assert.isTrue(wrapper.exists('Octicon[icon="lock"]')); }); it('renders unlocked when the lock is disengaged', function() { const wrapper = build({contextLocked: false}); - assert.isTrue(wrapper.exists('.icon-globe')); + assert.isTrue(wrapper.exists('Octicon[icon="globe"]')); }); it('calls handleLockToggle when the lock is clicked', function() { From 59a9daaec7d2e07cf49772fbf0284432fd998514 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Sun, 2 Feb 2020 14:47:14 -0500 Subject: [PATCH 17/36] GitTabHeaderController test coverage --- .../git-tab-header-controller.test.js | 69 +++++++++++++++++-- 1 file changed, 62 insertions(+), 7 deletions(-) diff --git a/test/controllers/git-tab-header-controller.test.js b/test/controllers/git-tab-header-controller.test.js index 5c66d545b9..92d66d9e88 100644 --- a/test/controllers/git-tab-header-controller.test.js +++ b/test/controllers/git-tab-header-controller.test.js @@ -15,17 +15,16 @@ describe('GitTabHeaderController', function() { function buildApp(overrides) { const props = { getCommitter: () => nullAuthor, + currentWorkDir: null, getCurrentWorkDirs: () => createWorkdirs([]), - onDidUpdateRepo: () => new Disposable(), + changeWorkingDirectory: () => {}, + contextLocked: false, + setContextLock: () => {}, onDidChangeWorkDirs: () => new Disposable(), - handleWorkDirSelect: () => null, + onDidUpdateRepo: () => new Disposable(), ...overrides, }; - return ( - - ); + return ; } it('get currentWorkDirs initializes workdirs state', function() { @@ -112,6 +111,62 @@ describe('GitTabHeaderController', function() { assert.isTrue(getCommitter.calledOnce); }); + it('handles a lock toggle', async function() { + let resolveLockChange; + const setContextLock = sinon.stub().returns(new Promise(resolve => { + resolveLockChange = resolve; + })) + const wrapper = shallow(buildApp({currentWorkDir: 'the/workdir', contextLocked: false, setContextLock})); + + assert.isFalse(wrapper.find('GitTabHeaderView').prop('contextLocked')); + assert.isFalse(wrapper.find('GitTabHeaderView').prop('changingLock')); + + const handlerPromise = wrapper.find('GitTabHeaderView').prop('handleLockToggle')(); + wrapper.update(); + + assert.isTrue(wrapper.find('GitTabHeaderView').prop('contextLocked')); + assert.isTrue(wrapper.find('GitTabHeaderView').prop('changingLock')); + assert.isTrue(setContextLock.calledWith('the/workdir', true)); + + // Ignored while in-progress + wrapper.find('GitTabHeaderView').prop('handleLockToggle')(); + + resolveLockChange(); + await handlerPromise; + + assert.isFalse(wrapper.find('GitTabHeaderView').prop('changingLock')); + }); + + it('handles a workdir selection', async function() { + let resolveWorkdirChange; + const changeWorkingDirectory = sinon.stub().returns(new Promise(resolve => { + resolveWorkdirChange = resolve; + })) + const wrapper = shallow(buildApp({currentWorkDir: 'original', changeWorkingDirectory})); + + assert.strictEqual(wrapper.find('GitTabHeaderView').prop('workdir'), 'original'); + assert.isFalse(wrapper.find('GitTabHeaderView').prop('changingWorkDir')); + + const handlerPromise = wrapper.find('GitTabHeaderView').prop('handleWorkDirSelect')({ + target: {value: 'work/dir'}, + }); + wrapper.update(); + + assert.strictEqual(wrapper.find('GitTabHeaderView').prop('workdir'), 'work/dir'); + assert.isTrue(wrapper.find('GitTabHeaderView').prop('changingWorkDir')); + assert.isTrue(changeWorkingDirectory.calledWith('work/dir')); + + // Ignored while in-progress + wrapper.find('GitTabHeaderView').prop('handleWorkDirSelect')({ + target: {value: 'ig/nored'}, + }); + + resolveWorkdirChange(); + await handlerPromise; + + assert.isFalse(wrapper.find('GitTabHeaderView').prop('changingWorkDir')); + }); + it('unmounts without error', function() { const wrapper = shallow(buildApp()); wrapper.unmount(); From 0832dfbbaac58b6413e06adbe7b094b6ab867a89 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Sun, 2 Feb 2020 14:57:38 -0500 Subject: [PATCH 18/36] GitTabView test coverage --- test/fixtures/props/git-tab-props.js | 4 +++- test/views/git-tab-view.test.js | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/test/fixtures/props/git-tab-props.js b/test/fixtures/props/git-tab-props.js index 5a8cf2d72b..a8f2eabebc 100644 --- a/test/fixtures/props/git-tab-props.js +++ b/test/fixtures/props/git-tab-props.js @@ -102,9 +102,11 @@ export async function gitTabViewProps(atomEnv, repository, overrides = {}) { discardWorkDirChangesForPaths: () => {}, openFiles: () => {}, + contextLocked: false, changeWorkingDirectory: () => {}, + setContextLock: () => {}, onDidChangeWorkDirs: () => ({dispose: () => {}}), - getCurrentWorkDirs: () => [], + getCurrentWorkDirs: () => new Set(), onDidUpdateRepo: () => ({dispose: () => {}}), getCommitter: () => nullAuthor, diff --git a/test/views/git-tab-view.test.js b/test/views/git-tab-view.test.js index 05671f2f4b..1acc2c2732 100644 --- a/test/views/git-tab-view.test.js +++ b/test/views/git-tab-view.test.js @@ -286,7 +286,7 @@ describe('GitTabView', function() { it('calls changeWorkingDirectory when a project is selected', async function() { const changeWorkingDirectory = sinon.spy(); const wrapper = shallow(await buildApp({changeWorkingDirectory})); - wrapper.find('GitTabHeaderController').prop('handleWorkDirSelect')({target: {value: 'some-path'}}); + wrapper.find('GitTabHeaderController').prop('changeWorkingDirectory')('some-path'); assert.isTrue(changeWorkingDirectory.calledWith('some-path')); }); }); From 72e9effa965657a98ddc3f0bcd276547cd43e650 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Sun, 2 Feb 2020 15:03:56 -0500 Subject: [PATCH 19/36] Fill in missing props --- test/fixtures/props/git-tab-props.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/fixtures/props/git-tab-props.js b/test/fixtures/props/git-tab-props.js index a8f2eabebc..8628000ecf 100644 --- a/test/fixtures/props/git-tab-props.js +++ b/test/fixtures/props/git-tab-props.js @@ -27,8 +27,10 @@ export function gitTabItemProps(atomEnv, repository, overrides = {}) { openFiles: noop, openInitializeDialog: noop, changeWorkingDirectory: noop, + contextLocked: false, + setContextLock: () => {}, onDidChangeWorkDirs: () => ({dispose: () => {}}), - getCurrentWorkDirs: () => [], + getCurrentWorkDirs: () => new Set(), ...overrides }; } From 5d6ea5ac92904b1a55c88ac02bbbc8b43a778d6c Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Sun, 2 Feb 2020 16:35:49 -0500 Subject: [PATCH 20/36] GithubTabHeaderView tests --- lib/views/github-tab-header-view.js | 10 ++++-- test/views/github-tab-header-view.test.js | 44 ++++++++++++++++++++++- 2 files changed, 51 insertions(+), 3 deletions(-) diff --git a/lib/views/github-tab-header-view.js b/lib/views/github-tab-header-view.js index e161ea3888..aa5708b63e 100644 --- a/lib/views/github-tab-header-view.js +++ b/lib/views/github-tab-header-view.js @@ -13,6 +13,8 @@ export default class GithubTabHeaderView extends React.Component { workdir: PropTypes.string, workdirs: PropTypes.shape({[Symbol.iterator]: PropTypes.func.isRequired}).isRequired, contextLocked: PropTypes.bool.isRequired, + changingWorkDir: PropTypes.bool.isRequired, + changingLock: PropTypes.bool.isRequired, handleWorkDirChange: PropTypes.func.isRequired, handleLockToggle: PropTypes.func.isRequired, } @@ -22,11 +24,15 @@ export default class GithubTabHeaderView extends React.Component {
{this.renderUser()} -
diff --git a/test/views/github-tab-header-view.test.js b/test/views/github-tab-header-view.test.js index 43cf57d060..c60e832f18 100644 --- a/test/views/github-tab-header-view.test.js +++ b/test/views/github-tab-header-view.test.js @@ -15,7 +15,13 @@ describe('GithubTabHeaderView', function() { function build(options = {}) { const props = { user: nullAuthor, + workdir: null, workdirs: createWorkdirs([]), + contextLocked: false, + changingWorkDir: false, + changingLock: false, + handleWorkDirChange: () => {}, + handleLockToggle: () => {}, ...options, }; return shallow(); @@ -29,7 +35,7 @@ describe('GithubTabHeaderView', function() { beforeEach(function() { select = sinon.spy(); - wrapper = build({handleWorkDirSelect: select, workdirs: createWorkdirs(paths), workdir: path2}); + wrapper = build({handleWorkDirChange: select, workdirs: createWorkdirs(paths), workdir: path2}); }); it('renders an option for all given working directories', function() { @@ -49,6 +55,42 @@ describe('GithubTabHeaderView', function() { }); }); + describe('context lock control', function() { + it('renders locked when the lock is engaged', function() { + const wrapper = build({contextLocked: true}); + + assert.isTrue(wrapper.exists('Octicon[icon="lock"]')); + }); + + it('renders unlocked when the lock is disengaged', function() { + const wrapper = build({contextLocked: false}); + + assert.isTrue(wrapper.exists('Octicon[icon="globe"]')); + }); + + it('calls handleLockToggle when the lock is clicked', function() { + const handleLockToggle = sinon.spy(); + const wrapper = build({handleLockToggle}); + + wrapper.find('button').simulate('click'); + assert.isTrue(handleLockToggle.called); + }); + }); + + describe('when changes are in progress', function() { + it('disables the workdir select while the workdir is changing', function() { + const wrapper = build({changingWorkDir: true}); + + assert.isTrue(wrapper.find('select').prop('disabled')); + }); + + it('disables the context lock toggle while the context lock is changing', function() { + const wrapper = build({changingLock: true}); + + assert.isTrue(wrapper.find('button').prop('disabled')); + }) + }); + describe('with falsish props', function() { let wrapper; From 7fe471e94a07f3e715b504b55682d097f3ebe794 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Sun, 2 Feb 2020 16:57:08 -0500 Subject: [PATCH 21/36] GithubTabHeaderController tests --- .../github-tab-header-controller.js | 6 +- .../github-tab-header-controller.test.js | 62 ++++++++++++++++++- 2 files changed, 64 insertions(+), 4 deletions(-) diff --git a/lib/controllers/github-tab-header-controller.js b/lib/controllers/github-tab-header-controller.js index a084bf7e8c..566a54cf3b 100644 --- a/lib/controllers/github-tab-header-controller.js +++ b/lib/controllers/github-tab-header-controller.js @@ -53,9 +53,11 @@ export default class GithubTabHeaderController extends React.Component { user={this.props.user} // Workspace - workdirs={this.state.currentWorkDirs} workdir={this.getWorkDir()} + workdirs={this.state.currentWorkDirs} contextLocked={this.getContextLocked()} + changingWorkDir={this.state.changingWorkDir !== null} + changingLock={this.state.changingLock !== null} handleWorkDirChange={this.handleWorkDirChange} handleLockToggle={this.handleLockToggle} @@ -93,7 +95,7 @@ export default class GithubTabHeaderController extends React.Component { this.setState({changingWorkDir: nextWorkDir}); await this.props.changeWorkingDirectory(nextWorkDir); } finally { - await new Promise(resolve => this.setState({changingWork: null}, resolve)); + await new Promise(resolve => this.setState({changingWorkDir: null}, resolve)); } } diff --git a/test/controllers/github-tab-header-controller.test.js b/test/controllers/github-tab-header-controller.test.js index 1e4f8529f1..9ebb8d6017 100644 --- a/test/controllers/github-tab-header-controller.test.js +++ b/test/controllers/github-tab-header-controller.test.js @@ -15,10 +15,12 @@ describe('GithubTabHeaderController', function() { function buildApp(overrides) { const props = { user: nullAuthor, + currentWorkDir: null, + contextLocked: false, + changeWorkingDirectory: () => {}, + setContextLock: () => {}, getCurrentWorkDirs: () => createWorkdirs([]), - onDidUpdateRepo: () => new Disposable(), onDidChangeWorkDirs: () => new Disposable(), - handleWorkDirSelect: () => null, ...overrides, }; return ( @@ -73,6 +75,62 @@ describe('GithubTabHeaderController', function() { assert.isTrue(getCurrentWorkDirs.calledTwice); }); + it('handles a lock toggle', async function() { + let resolveLockChange; + const setContextLock = sinon.stub().returns(new Promise(resolve => { + resolveLockChange = resolve; + })) + const wrapper = shallow(buildApp({currentWorkDir: 'the/workdir', contextLocked: false, setContextLock})); + + assert.isFalse(wrapper.find('GithubTabHeaderView').prop('contextLocked')); + assert.isFalse(wrapper.find('GithubTabHeaderView').prop('changingLock')); + + const handlerPromise = wrapper.find('GithubTabHeaderView').prop('handleLockToggle')(); + wrapper.update(); + + assert.isTrue(wrapper.find('GithubTabHeaderView').prop('contextLocked')); + assert.isTrue(wrapper.find('GithubTabHeaderView').prop('changingLock')); + assert.isTrue(setContextLock.calledWith('the/workdir', true)); + + // Ignored while in-progress + wrapper.find('GithubTabHeaderView').prop('handleLockToggle')(); + + resolveLockChange(); + await handlerPromise; + + assert.isFalse(wrapper.find('GithubTabHeaderView').prop('changingLock')); + }); + + it('handles a workdir selection', async function() { + let resolveWorkdirChange; + const changeWorkingDirectory = sinon.stub().returns(new Promise(resolve => { + resolveWorkdirChange = resolve; + })) + const wrapper = shallow(buildApp({currentWorkDir: 'original', changeWorkingDirectory})); + + assert.strictEqual(wrapper.find('GithubTabHeaderView').prop('workdir'), 'original'); + assert.isFalse(wrapper.find('GithubTabHeaderView').prop('changingWorkDir')); + + const handlerPromise = wrapper.find('GithubTabHeaderView').prop('handleWorkDirChange')({ + target: {value: 'work/dir'}, + }); + wrapper.update(); + + assert.strictEqual(wrapper.find('GithubTabHeaderView').prop('workdir'), 'work/dir'); + assert.isTrue(wrapper.find('GithubTabHeaderView').prop('changingWorkDir')); + assert.isTrue(changeWorkingDirectory.calledWith('work/dir')); + + // Ignored while in-progress + wrapper.find('GithubTabHeaderView').prop('handleWorkDirChange')({ + target: {value: 'ig/nored'}, + }); + + resolveWorkdirChange(); + await handlerPromise; + + assert.isFalse(wrapper.find('GithubTabHeaderView').prop('changingWorkDir')); + }); + it('disposes on unmount', function() { const disposeSpy = sinon.spy(); const onDidChangeWorkDirs = () => ({dispose: disposeSpy}); From 78b1c9c134b6d2871600240782454b2707907df1 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Sun, 2 Feb 2020 17:00:48 -0500 Subject: [PATCH 22/36] GithubTabHeaderContainer tests --- test/containers/github-tab-header-container.test.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/test/containers/github-tab-header-container.test.js b/test/containers/github-tab-header-container.test.js index 15eb6eeb9b..db68e86492 100644 --- a/test/containers/github-tab-header-container.test.js +++ b/test/containers/github-tab-header-container.test.js @@ -27,7 +27,12 @@ describe('GithubTabHeaderContainer', function() { null} + currentWorkDir={null} + contextLocked={false} + changeWorkingDirectory={() => {}} + setContextLock={() => {}} + getCurrentWorkDirs={() => new Set()} + onDidChangeWorkDirs={() => {}} {...overrideProps} /> ); From 8b5aa6159aefba00cc0cb83c15922a3f456c2759 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Sun, 2 Feb 2020 17:03:39 -0500 Subject: [PATCH 23/36] GithubTabController tests --- test/controllers/github-tab-controller.test.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/controllers/github-tab-controller.test.js b/test/controllers/github-tab-controller.test.js index ad5e3c2fd2..c7be628195 100644 --- a/test/controllers/github-tab-controller.test.js +++ b/test/controllers/github-tab-controller.test.js @@ -45,6 +45,8 @@ describe('GitHubTabController', function() { currentWorkDir={repo.getWorkingDirectoryPath()} changeWorkingDirectory={() => {}} + setContextLock={() => {}} + contextLocked={false} onDidChangeWorkDirs={() => {}} getCurrentWorkDirs={() => []} openCreateDialog={() => {}} From cd28dd599955d1e77dc57ba6c51049e3dd82e4b0 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Sun, 2 Feb 2020 17:11:11 -0500 Subject: [PATCH 24/36] GithubTabView tests --- lib/views/github-tab-view.js | 2 +- test/views/github-tab-view.test.js | 21 +++++++++++++-------- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/lib/views/github-tab-view.js b/lib/views/github-tab-view.js index 3aaced7a74..266bf4c93a 100644 --- a/lib/views/github-tab-view.js +++ b/lib/views/github-tab-view.js @@ -42,7 +42,7 @@ export default class GitHubTabView extends React.Component { aheadCount: PropTypes.number, pushInProgress: PropTypes.bool.isRequired, - // Event Handelrs + // Event Handlers handleWorkDirSelect: PropTypes.func, handlePushBranch: PropTypes.func.isRequired, handleRemoteSelect: PropTypes.func.isRequired, diff --git a/test/views/github-tab-view.test.js b/test/views/github-tab-view.test.js index d78ea5b44b..e507264c49 100644 --- a/test/views/github-tab-view.test.js +++ b/test/views/github-tab-view.test.js @@ -31,26 +31,31 @@ describe('GitHubTabView', function() { return ( []} + changeWorkingDirectory={() => {}} + contextLocked={false} + setContextLock={() => {}} repository={repo} - branches={new BranchSet()} - currentBranch={nullBranch} + remotes={new RemoteSet()} currentRemote={nullRemote} manyRemotesAvailable={false} - pushInProgress={false} isLoading={false} + branches={new BranchSet()} + currentBranch={nullBranch} + pushInProgress={false} + handleWorkDirSelect={() => {}} handlePushBranch={() => {}} handleRemoteSelect={() => {}} - changeWorkingDirectory={() => {}} onDidChangeWorkDirs={() => {}} - getCurrentWorkDirs={() => []} openCreateDialog={() => {}} openBoundPublishDialog={() => {}} openCloneDialog={() => {}} @@ -125,7 +130,7 @@ describe('GitHubTabView', function() { const currentRemote = new Remote('aaa', 'git@github.com:aaa/bbb.git'); const changeWorkingDirectory = sinon.spy(); const wrapper = shallow(buildApp({currentRemote, changeWorkingDirectory})); - wrapper.find('GithubTabHeaderContainer').prop('handleWorkDirSelect')({target: {value: 'some-path'}}); + wrapper.find('GithubTabHeaderContainer').prop('changeWorkingDirectory')('some-path'); assert.isTrue(changeWorkingDirectory.calledWith('some-path')); }); }); From 8e7c537b6107b7e5ef2bb8be6ae3b8a5f297cffc Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Sun, 2 Feb 2020 20:41:38 -0500 Subject: [PATCH 25/36] Pass missing props --- test/controllers/root-controller.test.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/test/controllers/root-controller.test.js b/test/controllers/root-controller.test.js index c2eb72e890..9d61f815ca 100644 --- a/test/controllers/root-controller.test.js +++ b/test/controllers/root-controller.test.js @@ -58,13 +58,13 @@ describe('RootController', function() { workspace={workspace} commands={commands} deserializers={deserializers} - grammars={grammars} notificationManager={notificationManager} tooltips={tooltips} + keymaps={atomEnv.keymaps} + grammars={grammars} config={config} - confirm={confirm} project={project} - keymaps={atomEnv.keymaps} + confirm={confirm} currentWindow={atomEnv.getCurrentWindow()} loginModel={loginModel} @@ -72,9 +72,14 @@ describe('RootController', function() { repository={absentRepository} resolutionProgress={emptyResolutionProgress} + currentWorkDir={null} + initialize={() => {}} clone={() => {}} + contextLocked={false} + changeWorkingDirectory={() => {}} + setContextLock={() => {}} startOpen={false} startRevealed={false} /> From 550fb8c8b4ac4a2cf4687a57a7ab692573b95121 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 3 Feb 2020 10:11:29 -0500 Subject: [PATCH 26/36] Keep the serialized state as `activeRepositoryPath` --- lib/github-package.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/github-package.js b/lib/github-package.js index 5d8867af70..bd7c38c003 100644 --- a/lib/github-package.js +++ b/lib/github-package.js @@ -26,7 +26,7 @@ import {reporterProxy} from './reporter-proxy'; const defaultState = { newProject: true, - activeContextPath: null, + activeRepositoryPath: null, contextLocked: false, }; @@ -242,7 +242,7 @@ export default class GithubPackage { this.activated = true; this.scheduleActiveContextUpdate({ - usePath: savedState.activeContextPath, + usePath: savedState.activeRepositoryPath, lock: savedState.contextLocked, }); this.rerender(); @@ -266,7 +266,7 @@ export default class GithubPackage { serialize() { return { - activeContextPath: this.getActiveRepositoryPath(), + activeRepositoryPath: this.getActiveRepositoryPath(), contextLocked: Boolean(this.lockedContext), newProject: false, }; From 39df687f2398b8d1f46ee95ac576b9db35325b8b Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 3 Feb 2020 10:11:48 -0500 Subject: [PATCH 27/36] scheduleActiveContextUpdate() argument keywords --- test/github-package.test.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/github-package.test.js b/test/github-package.test.js index dfb0308011..038cfb6556 100644 --- a/test/github-package.test.js +++ b/test/github-package.test.js @@ -455,7 +455,7 @@ describe('GithubPackage', function() { let resolutionMergeConflict; beforeEach(async function() { await githubPackage.scheduleActiveContextUpdate({ - activeRepositoryPath: workdirMergeConflict, + usePath: workdirMergeConflict, }); resolutionMergeConflict = contextPool.getContext(workdirMergeConflict).getResolutionProgress(); }); @@ -477,7 +477,7 @@ describe('GithubPackage', function() { let resolutionNoConflict; beforeEach(async function() { await githubPackage.scheduleActiveContextUpdate({ - activeRepositoryPath: workdirNoConflict, + usePath: workdirNoConflict, }); resolutionNoConflict = contextPool.getContext(workdirNoConflict).getResolutionProgress(); }); @@ -494,7 +494,7 @@ describe('GithubPackage', function() { describe('when opening a non-repository project', function() { beforeEach(async function() { await githubPackage.scheduleActiveContextUpdate({ - activeRepositoryPath: nonRepositoryPath, + usePath: nonRepositoryPath, }); }); @@ -515,7 +515,7 @@ describe('GithubPackage', function() { project.setPaths([workdirPath1, workdirPath2]); await githubPackage.scheduleActiveContextUpdate({ - activeRepositoryPath: workdirPath3, + usePath: workdirPath3, }); context1 = contextPool.getContext(workdirPath1); }); @@ -538,7 +538,7 @@ describe('GithubPackage', function() { project.setPaths([workdirPath1]); await githubPackage.scheduleActiveContextUpdate({ - activeRepositoryPath: workdirPath2, + usePath: workdirPath2, }); context1 = contextPool.getContext(workdirPath1); }); @@ -561,7 +561,7 @@ describe('GithubPackage', function() { project.setPaths([workdirPath1, workdirPath2]); await githubPackage.scheduleActiveContextUpdate({ - activeRepositoryPath: workdirPath2, + usePath: workdirPath2, }); }); From 4183766737c3d4fb84d94d7c466c73e7b313b3de Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 3 Feb 2020 10:18:14 -0500 Subject: [PATCH 28/36] Linty lint --- test/controllers/git-tab-header-controller.test.js | 4 ++-- test/controllers/github-tab-header-controller.test.js | 4 ++-- test/views/git-tab-header-view.test.js | 2 +- test/views/github-tab-header-view.test.js | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/test/controllers/git-tab-header-controller.test.js b/test/controllers/git-tab-header-controller.test.js index 92d66d9e88..d6619fedde 100644 --- a/test/controllers/git-tab-header-controller.test.js +++ b/test/controllers/git-tab-header-controller.test.js @@ -115,7 +115,7 @@ describe('GitTabHeaderController', function() { let resolveLockChange; const setContextLock = sinon.stub().returns(new Promise(resolve => { resolveLockChange = resolve; - })) + })); const wrapper = shallow(buildApp({currentWorkDir: 'the/workdir', contextLocked: false, setContextLock})); assert.isFalse(wrapper.find('GitTabHeaderView').prop('contextLocked')); @@ -141,7 +141,7 @@ describe('GitTabHeaderController', function() { let resolveWorkdirChange; const changeWorkingDirectory = sinon.stub().returns(new Promise(resolve => { resolveWorkdirChange = resolve; - })) + })); const wrapper = shallow(buildApp({currentWorkDir: 'original', changeWorkingDirectory})); assert.strictEqual(wrapper.find('GitTabHeaderView').prop('workdir'), 'original'); diff --git a/test/controllers/github-tab-header-controller.test.js b/test/controllers/github-tab-header-controller.test.js index 9ebb8d6017..994f1b8752 100644 --- a/test/controllers/github-tab-header-controller.test.js +++ b/test/controllers/github-tab-header-controller.test.js @@ -79,7 +79,7 @@ describe('GithubTabHeaderController', function() { let resolveLockChange; const setContextLock = sinon.stub().returns(new Promise(resolve => { resolveLockChange = resolve; - })) + })); const wrapper = shallow(buildApp({currentWorkDir: 'the/workdir', contextLocked: false, setContextLock})); assert.isFalse(wrapper.find('GithubTabHeaderView').prop('contextLocked')); @@ -105,7 +105,7 @@ describe('GithubTabHeaderController', function() { let resolveWorkdirChange; const changeWorkingDirectory = sinon.stub().returns(new Promise(resolve => { resolveWorkdirChange = resolve; - })) + })); const wrapper = shallow(buildApp({currentWorkDir: 'original', changeWorkingDirectory})); assert.strictEqual(wrapper.find('GithubTabHeaderView').prop('workdir'), 'original'); diff --git a/test/views/git-tab-header-view.test.js b/test/views/git-tab-header-view.test.js index 23d0523eee..cfd4ca3de7 100644 --- a/test/views/git-tab-header-view.test.js +++ b/test/views/git-tab-header-view.test.js @@ -92,7 +92,7 @@ describe('GitTabHeaderView', function() { const wrapper = build({changingLock: true}); assert.isTrue(wrapper.find('button').prop('disabled')); - }) + }); }); describe('with falsish props', function() { diff --git a/test/views/github-tab-header-view.test.js b/test/views/github-tab-header-view.test.js index c60e832f18..c1c65ab630 100644 --- a/test/views/github-tab-header-view.test.js +++ b/test/views/github-tab-header-view.test.js @@ -88,7 +88,7 @@ describe('GithubTabHeaderView', function() { const wrapper = build({changingLock: true}); assert.isTrue(wrapper.find('button').prop('disabled')); - }) + }); }); describe('with falsish props', function() { From 62f5f71a8b06275f88c9b95a203ec0952e101296 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 3 Feb 2020 14:50:32 -0500 Subject: [PATCH 29/36] Use existing getActiveWorkdir() method --- lib/github-package.js | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/lib/github-package.js b/lib/github-package.js index bd7c38c003..17e2d3c653 100644 --- a/lib/github-package.js +++ b/lib/github-package.js @@ -266,7 +266,7 @@ export default class GithubPackage { serialize() { return { - activeRepositoryPath: this.getActiveRepositoryPath(), + activeRepositoryPath: this.getActiveWorkdir(), contextLocked: Boolean(this.lockedContext), newProject: false, }; @@ -506,14 +506,6 @@ export default class GithubPackage { return this.activeContext.getRepository(); } - getActiveRepositoryPath() { - const activeRepository = this.activeContext.getRepository(); - if (!activeRepository) { - return null; - } - return activeRepository.getWorkingDirectoryPath(); - } - getActiveResolutionProgress() { return this.activeContext.getResolutionProgress(); } From 54d8a6be0368c32f4ad868fbf76fd009ef6a40a8 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 3 Feb 2020 14:51:06 -0500 Subject: [PATCH 30/36] Prefer locked context over active item --- lib/github-package.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/github-package.js b/lib/github-package.js index 17e2d3c653..c5a62d89e0 100644 --- a/lib/github-package.js +++ b/lib/github-package.js @@ -598,12 +598,17 @@ export default class GithubPackage { } } - // 2 - Follow the active workspace pane item. + // 2 - Use the currently locked context, if one is present. + if (this.lockedContext) { + return this.lockedContext; + } + + // 3 - Follow the active workspace pane item. if (activeItemWorkdir) { return this.contextPool.getContext(activeItemWorkdir); } - // 3 - The first open project. + // 4 - The first open project. if (firstProjectWorkdir) { return this.contextPool.getContext(firstProjectWorkdir); } From 9dd2f2555802db605ea240d1766d1fe06222126c Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 3 Feb 2020 14:51:14 -0500 Subject: [PATCH 31/36] Coverage tweaks --- lib/github-package.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/github-package.js b/lib/github-package.js index c5a62d89e0..f0c88c6c99 100644 --- a/lib/github-package.js +++ b/lib/github-package.js @@ -555,6 +555,7 @@ export default class GithubPackage { const candidatePaths = new Set(this.project.getPaths()); if (this.lockedContext) { const lockedRepo = this.lockedContext.getRepository(); + /* istanbul ignore else */ if (lockedRepo) { candidatePaths.add(lockedRepo.getWorkingDirectoryPath()); } @@ -673,7 +674,7 @@ export default class GithubPackage { * usePath - Path of the context to use as the next context, if it is present in the pool. * lock - True or false to lock the ultimately chosen context. Omit to preserve the current lock state. */ - async updateActiveContext(options = {}) { + async updateActiveContext(options) { if (this.workspace.isDestroyed()) { return; } From 09c4a6db74f8a879680022610773e4296c2b25fa Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 3 Feb 2020 14:52:25 -0500 Subject: [PATCH 32/36] Tests for locked contexts --- test/github-package.test.js | 111 +++++++++++++++++++++++++++++++++++- 1 file changed, 108 insertions(+), 3 deletions(-) diff --git a/test/github-package.test.js b/test/github-package.test.js index 038cfb6556..67d50bcddd 100644 --- a/test/github-package.test.js +++ b/test/github-package.test.js @@ -412,12 +412,23 @@ describe('GithubPackage', function() { }); describe('when removing a project', function() { - beforeEach(async function() { + it('removes the project\'s context', async function() { await contextUpdateAfter(githubPackage, () => project.setPaths([workdirPath1])); - }); - it('removes the project\'s context', function() { assert.isFalse(contextPool.getContext(workdirPath2).isPresent()); + assert.strictEqual(githubPackage.getActiveWorkdir(), workdirPath1); + }); + + it('does nothing if the context is locked', async function() { + await githubPackage.scheduleActiveContextUpdate({ + usePath: workdirPath2, + lock: true, + }); + + await contextUpdateAfter(githubPackage, () => project.setPaths([workdirPath1])); + + assert.isTrue(contextPool.getContext(workdirPath2).isPresent()); + assert.strictEqual(githubPackage.getActiveWorkdir(), workdirPath2); }); }); @@ -434,6 +445,100 @@ describe('GithubPackage', function() { assert.isTrue(githubPackage.getActiveRepository().isAbsent()); }); }); + + describe('when changing the active pane item', function() { + it('follows the active pane item', async function() { + const itemPath2 = path.join(workdirPath2, 'b.txt'); + + assert.strictEqual(githubPackage.getActiveWorkdir(), workdirPath1); + await contextUpdateAfter(githubPackage, () => atomEnv.workspace.open(itemPath2)); + assert.strictEqual(githubPackage.getActiveWorkdir(), workdirPath2); + }); + + it('does nothing if the context is locked', async function() { + const itemPath2 = path.join(workdirPath2, 'c.txt'); + + await githubPackage.scheduleActiveContextUpdate({ + usePath: workdirPath1, + lock: true, + }); + + assert.strictEqual(githubPackage.getActiveWorkdir(), workdirPath1); + await contextUpdateAfter(githubPackage, () => atomEnv.workspace.open(itemPath2)); + assert.strictEqual(githubPackage.getActiveWorkdir(), workdirPath1); + }); + }); + + describe('with a locked context', function() { + it('preserves the locked context in the pool', async function() { + await githubPackage.scheduleActiveContextUpdate({ + usePath: workdirPath1, + lock: true, + }); + + await contextUpdateAfter(githubPackage, () => project.setPaths([workdirPath2])); + + assert.isTrue(contextPool.getContext(workdirPath1).isPresent()); + assert.isTrue(contextPool.getContext(workdirPath2).isPresent()); + + assert.strictEqual(githubPackage.getActiveWorkdir(), workdirPath1); + }); + + it('may be unlocked', async function() { + const itemPath1a = path.join(workdirPath1, 'a.txt'); + const itemPath1b = path.join(workdirPath2, 'b.txt'); + + await githubPackage.scheduleActiveContextUpdate({ + usePath: workdirPath2, + lock: true, + }); + + await contextUpdateAfter(githubPackage, () => atomEnv.workspace.open(itemPath1a)); + assert.strictEqual(githubPackage.getActiveWorkdir(), workdirPath2); + + await githubPackage.scheduleActiveContextUpdate({ + usePath: workdirPath1, + lock: false, + }); + + await contextUpdateAfter(githubPackage, () => atomEnv.workspace.open(itemPath1b)); + assert.strictEqual(githubPackage.getActiveWorkdir(), workdirPath1); + }); + + it('triggers a re-render when the context is unchanged', async function() { + sinon.stub(githubPackage, 'rerender'); + + assert.strictEqual(githubPackage.getActiveWorkdir(), workdirPath1); + await githubPackage.scheduleActiveContextUpdate({ + usePath: workdirPath1, + lock: true, + }); + + assert.strictEqual(githubPackage.getActiveWorkdir(), workdirPath1); + assert.isTrue(githubPackage.rerender.called); + githubPackage.rerender.resetHistory(); + + await githubPackage.scheduleActiveContextUpdate({ + usePath: workdirPath1, + lock: false, + }); + + assert.strictEqual(githubPackage.getActiveWorkdir(), workdirPath1); + assert.isTrue(githubPackage.rerender.called); + }); + }); + + it('does nothing when the workspace is destroyed', async function() { + sinon.stub(githubPackage, 'rerender'); + atomEnv.destroy(); + + await githubPackage.scheduleActiveContextUpdate({ + usePath: workdirPath2, + }); + + assert.isFalse(githubPackage.rerender.called); + assert.strictEqual(githubPackage.getActiveWorkdir(), workdirPath1); + }); }); describe('with non-repository, no-conflict, and in-progress merge-conflict projects', function() { From 9ae31ddfbe6f4ee1698d19ac220d0b8dd1a0d397 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 3 Feb 2020 15:18:23 -0500 Subject: [PATCH 33/36] Inexpert but hopefully passable unlock icon --- img/unlock.svg | 61 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 img/unlock.svg diff --git a/img/unlock.svg b/img/unlock.svg new file mode 100644 index 0000000000..9e7927c140 --- /dev/null +++ b/img/unlock.svg @@ -0,0 +1,61 @@ + + + + + + image/svg+xml + + + + + + + + + From 2ea13775adfa248655683c2f69d879a46a4ee98e Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 5 Feb 2020 14:51:47 -0500 Subject: [PATCH 34/36] Render manual SVG "octicons" --- lib/atom/octicon.js | 24 ++++++++++++++++++++++++ test/atom/octicon.test.js | 17 +++++++++++++++++ 2 files changed, 41 insertions(+) create mode 100644 test/atom/octicon.test.js diff --git a/lib/atom/octicon.js b/lib/atom/octicon.js index 1f306dda38..c4a3d14676 100644 --- a/lib/atom/octicon.js +++ b/lib/atom/octicon.js @@ -2,8 +2,32 @@ import React from 'react'; import PropTypes from 'prop-types'; import cx from 'classnames'; +/* eslint-disable max-len */ +const SVG = { + unlock: { + viewBox: '0 0 24 16', + element: ( + + ), + }, +}; +/* eslint-enable max-len */ + export default function Octicon({icon, ...others}) { const classes = cx('icon', `icon-${icon}`, others.className); + + const svgContent = SVG[icon]; + if (svgContent) { + return ( + + {svgContent.element} + + ); + } + return ; } diff --git a/test/atom/octicon.test.js b/test/atom/octicon.test.js new file mode 100644 index 0000000000..f7f373cc8a --- /dev/null +++ b/test/atom/octicon.test.js @@ -0,0 +1,17 @@ +import React from 'react'; +import {shallow} from 'enzyme'; + +import Octicon from '../../lib/atom/octicon'; + +describe('Octicon', function() { + it('defaults to rendering an octicon span', function() { + const wrapper = shallow(); + assert.isTrue(wrapper.exists('span.icon.icon-octoface')); + }); + + it('renders SVG overrides', function() { + const wrapper = shallow(); + + assert.strictEqual(wrapper.find('svg').prop('viewBox'), '0 0 24 16'); + }); +}); From 4955c9ba0ec1ffbcc83fb6ea77f4c14feb8ab58a Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 5 Feb 2020 14:57:21 -0500 Subject: [PATCH 35/36] Use the new unlock icon in the button --- lib/views/git-tab-header-view.js | 10 ++++++-- lib/views/github-tab-header-view.js | 13 +++++++--- styles/project.less | 30 +++++++++++++++++++++++ test/views/git-tab-header-view.test.js | 2 +- test/views/github-tab-header-view.test.js | 2 +- 5 files changed, 49 insertions(+), 8 deletions(-) diff --git a/lib/views/git-tab-header-view.js b/lib/views/git-tab-header-view.js index 1ecb0f1d61..0a24c41ecd 100644 --- a/lib/views/git-tab-header-view.js +++ b/lib/views/git-tab-header-view.js @@ -22,6 +22,11 @@ export default class GitTabHeaderView extends React.Component { } render() { + const lockIcon = this.props.contextLocked ? 'lock' : 'unlock'; + const lockToggleTitle = this.props.contextLocked ? + 'Change repository with the dropdown' : + 'Follow the active pane item'; + return (
{this.renderCommitter()} @@ -33,8 +38,9 @@ export default class GitTabHeaderView extends React.Component {
); diff --git a/lib/views/github-tab-header-view.js b/lib/views/github-tab-header-view.js index aa5708b63e..19b21c3537 100644 --- a/lib/views/github-tab-header-view.js +++ b/lib/views/github-tab-header-view.js @@ -20,6 +20,11 @@ export default class GithubTabHeaderView extends React.Component { } render() { + const lockIcon = this.props.contextLocked ? 'lock' : 'unlock'; + const lockToggleTitle = this.props.contextLocked ? + 'Change repository with the dropdown' : + 'Follow the active pane item'; + return (
{this.renderUser()} @@ -29,11 +34,11 @@ export default class GithubTabHeaderView extends React.Component { onChange={this.props.handleWorkDirChange}> {this.renderWorkDirs()} -
); diff --git a/styles/project.less b/styles/project.less index d88b86d2d9..143cd6a5a8 100644 --- a/styles/project.less +++ b/styles/project.less @@ -18,8 +18,38 @@ } &-lock.btn { + width: 40px; + height: 20px; + padding: 0; + display: flex; + flex-direction: row; + justify-content: center; + align-items: baseline; + border: none; background-color: transparent; background-image: none; + + &:active, &:hover { + background-color: transparent; + background-image: none; + } + } + + .icon { + padding: 0; + margin: 0; + line-height: 1em; + fill: @text-color; + + &:hover { + fill: @text-color-highlight; + } + + &.icon-unlock { + width: 21px; + height: 17px; + padding-left: 1px; + } } } diff --git a/test/views/git-tab-header-view.test.js b/test/views/git-tab-header-view.test.js index cfd4ca3de7..c1074551dd 100644 --- a/test/views/git-tab-header-view.test.js +++ b/test/views/git-tab-header-view.test.js @@ -69,7 +69,7 @@ describe('GitTabHeaderView', function() { it('renders unlocked when the lock is disengaged', function() { const wrapper = build({contextLocked: false}); - assert.isTrue(wrapper.exists('Octicon[icon="globe"]')); + assert.isTrue(wrapper.exists('Octicon[icon="unlock"]')); }); it('calls handleLockToggle when the lock is clicked', function() { diff --git a/test/views/github-tab-header-view.test.js b/test/views/github-tab-header-view.test.js index c1c65ab630..882d92f43e 100644 --- a/test/views/github-tab-header-view.test.js +++ b/test/views/github-tab-header-view.test.js @@ -65,7 +65,7 @@ describe('GithubTabHeaderView', function() { it('renders unlocked when the lock is disengaged', function() { const wrapper = build({contextLocked: false}); - assert.isTrue(wrapper.exists('Octicon[icon="globe"]')); + assert.isTrue(wrapper.exists('Octicon[icon="unlock"]')); }); it('calls handleLockToggle when the lock is clicked', function() { From 36d001e7b18464f2c05cf60a5e488ec2bc1d1cda Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 5 Feb 2020 15:01:03 -0500 Subject: [PATCH 36/36] Fall back to '' instead of null for the selects' value --- lib/views/git-tab-header-view.js | 2 +- lib/views/github-tab-header-view.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/views/git-tab-header-view.js b/lib/views/git-tab-header-view.js index 0a24c41ecd..7189782236 100644 --- a/lib/views/git-tab-header-view.js +++ b/lib/views/git-tab-header-view.js @@ -31,7 +31,7 @@ export default class GitTabHeaderView extends React.Component {
{this.renderCommitter()} {this.renderWorkDirs()}