From 261c15e3100f161c96ac36bfb05b2ed22a84547c Mon Sep 17 00:00:00 2001 From: boscochw_ubuntU_ideapad Date: Thu, 7 Jul 2022 16:11:23 -0700 Subject: [PATCH 01/29] implement name-url pair for adding remote repo --- src/commandsAndMenu.tsx | 18 ++++++--- src/tokens.ts | 8 ++++ src/widgets/GitAddRemoteForm.tsx | 69 ++++++++++++++++++++++++++++++++ 3 files changed, 89 insertions(+), 6 deletions(-) create mode 100644 src/widgets/GitAddRemoteForm.tsx diff --git a/src/commandsAndMenu.tsx b/src/commandsAndMenu.tsx index bddbc9986..40c66db7e 100644 --- a/src/commandsAndMenu.tsx +++ b/src/commandsAndMenu.tsx @@ -1,7 +1,6 @@ import { JupyterFrontEnd } from '@jupyterlab/application'; import { Dialog, - InputDialog, MainAreaWidget, ReactWidget, showDialog, @@ -47,6 +46,7 @@ import { } from './tokens'; import { GitCredentialsForm } from './widgets/CredentialsBox'; import { discardAllChanges } from './widgets/discardAllChanges'; +import { GitAddRemoteForm } from './widgets/GitAddRemoteForm'; import { CheckboxForm } from './widgets/GitResetToRemoteForm'; export interface IGitCloneArgs { @@ -265,16 +265,22 @@ export function addCommands( return; } let url = args['url'] as string; - const name = args['name'] as string; + let name = args['name'] as string; if (!url) { - const result = await InputDialog.getText({ + const remoteRepo = await showDialog({ title: trans.__('Add a remote repository'), - placeholder: trans.__('Remote Git repository URL') + body: new GitAddRemoteForm( + trans, + 'Enter remote repository name and url', + '', + gitModel + ) }); - if (result.button.accept) { - url = result.value; + if (remoteRepo.button.accept) { + name = remoteRepo.value.name; + url = remoteRepo.value.url; } } diff --git a/src/tokens.ts b/src/tokens.ts index 62128cba6..8509aee62 100644 --- a/src/tokens.ts +++ b/src/tokens.ts @@ -987,6 +987,14 @@ export namespace Git { cache_credentials?: boolean; } + /** + * Structure for the request to the Git Remote Add API. + */ + export interface IGitRemote { + url: string; + name: string; + } + /** * Structure for the request to the Git Clone API. */ diff --git a/src/widgets/GitAddRemoteForm.tsx b/src/widgets/GitAddRemoteForm.tsx new file mode 100644 index 000000000..fca19d3e4 --- /dev/null +++ b/src/widgets/GitAddRemoteForm.tsx @@ -0,0 +1,69 @@ +import { Dialog } from '@jupyterlab/apputils'; +import { TranslationBundle } from '@jupyterlab/translation'; +import { Widget } from '@lumino/widgets'; +import { Git } from '../tokens'; +import { GitExtension } from '../model'; + +/** + * The UI for the add remote repository form + */ +export class GitAddRemoteForm + extends Widget + implements Dialog.IBodyWidget +{ + constructor( + trans: TranslationBundle, + textContent = trans.__('Enter remote repository name and url'), + warningContent = '', + model: GitExtension + ) { + super(); + this._trans = trans; + this._model = model; + this.node.appendChild(this.createBody(textContent, warningContent)); + } + + private createBody(textContent: string, warningContent: string): HTMLElement { + console.log(this._model); + const node = document.createElement('div'); + node.className = 'jp-AddRemoteBox'; + + const label = document.createElement('label'); + + const text = document.createElement('span'); + text.textContent = textContent; + this._name = document.createElement('input'); + this._name.type = 'text'; + this._name.placeholder = this._trans.__('namename'); + this._url = document.createElement('input'); + this._url.type = 'text'; + this._url.placeholder = this._trans.__('Remote GIt repository URL'); + + label.appendChild(text); + label.appendChild(this._name); + label.appendChild(this._url); + + const warning = document.createElement('div'); + warning.className = 'jp-AddRemoteBox-warning'; + warning.textContent = warningContent; + + node.appendChild(label); + node.appendChild(warning); + + return node; + } + + /** + * Returns the input value. + */ + getValue(): Git.IGitRemote { + return { + url: this._url.value, + name: this._name.value + }; + } + protected _trans: TranslationBundle; + private _model: GitExtension; + private _url: HTMLInputElement; + private _name: HTMLInputElement; +} From c1e6e90b032fec445baea7658a1d8041db5f11d9 Mon Sep 17 00:00:00 2001 From: boscochw_ubuntU_ideapad Date: Thu, 7 Jul 2022 16:11:23 -0700 Subject: [PATCH 02/29] implement name-url pair for adding remote repo --- src/commandsAndMenu.tsx | 18 ++++++--- src/tokens.ts | 8 ++++ src/widgets/GitAddRemoteForm.tsx | 69 ++++++++++++++++++++++++++++++++ 3 files changed, 89 insertions(+), 6 deletions(-) create mode 100644 src/widgets/GitAddRemoteForm.tsx diff --git a/src/commandsAndMenu.tsx b/src/commandsAndMenu.tsx index 2bb5f8887..9b22a7659 100644 --- a/src/commandsAndMenu.tsx +++ b/src/commandsAndMenu.tsx @@ -1,7 +1,6 @@ import { JupyterFrontEnd } from '@jupyterlab/application'; import { Dialog, - InputDialog, MainAreaWidget, ReactWidget, showDialog, @@ -47,6 +46,7 @@ import { } from './tokens'; import { GitCredentialsForm } from './widgets/CredentialsBox'; import { discardAllChanges } from './widgets/discardAllChanges'; +import { GitAddRemoteForm } from './widgets/GitAddRemoteForm'; import { CheckboxForm } from './widgets/GitResetToRemoteForm'; export interface IGitCloneArgs { @@ -265,16 +265,22 @@ export function addCommands( return; } let url = args['url'] as string; - const name = args['name'] as string; + let name = args['name'] as string; if (!url) { - const result = await InputDialog.getText({ + const remoteRepo = await showDialog({ title: trans.__('Add a remote repository'), - placeholder: trans.__('Remote Git repository URL') + body: new GitAddRemoteForm( + trans, + 'Enter remote repository name and url', + '', + gitModel + ) }); - if (result.button.accept) { - url = result.value; + if (remoteRepo.button.accept) { + name = remoteRepo.value.name; + url = remoteRepo.value.url; } } diff --git a/src/tokens.ts b/src/tokens.ts index df1d92545..c654bb3e6 100644 --- a/src/tokens.ts +++ b/src/tokens.ts @@ -991,6 +991,14 @@ export namespace Git { cache_credentials?: boolean; } + /** + * Structure for the request to the Git Remote Add API. + */ + export interface IGitRemote { + url: string; + name: string; + } + /** * Structure for the request to the Git Clone API. */ diff --git a/src/widgets/GitAddRemoteForm.tsx b/src/widgets/GitAddRemoteForm.tsx new file mode 100644 index 000000000..fca19d3e4 --- /dev/null +++ b/src/widgets/GitAddRemoteForm.tsx @@ -0,0 +1,69 @@ +import { Dialog } from '@jupyterlab/apputils'; +import { TranslationBundle } from '@jupyterlab/translation'; +import { Widget } from '@lumino/widgets'; +import { Git } from '../tokens'; +import { GitExtension } from '../model'; + +/** + * The UI for the add remote repository form + */ +export class GitAddRemoteForm + extends Widget + implements Dialog.IBodyWidget +{ + constructor( + trans: TranslationBundle, + textContent = trans.__('Enter remote repository name and url'), + warningContent = '', + model: GitExtension + ) { + super(); + this._trans = trans; + this._model = model; + this.node.appendChild(this.createBody(textContent, warningContent)); + } + + private createBody(textContent: string, warningContent: string): HTMLElement { + console.log(this._model); + const node = document.createElement('div'); + node.className = 'jp-AddRemoteBox'; + + const label = document.createElement('label'); + + const text = document.createElement('span'); + text.textContent = textContent; + this._name = document.createElement('input'); + this._name.type = 'text'; + this._name.placeholder = this._trans.__('namename'); + this._url = document.createElement('input'); + this._url.type = 'text'; + this._url.placeholder = this._trans.__('Remote GIt repository URL'); + + label.appendChild(text); + label.appendChild(this._name); + label.appendChild(this._url); + + const warning = document.createElement('div'); + warning.className = 'jp-AddRemoteBox-warning'; + warning.textContent = warningContent; + + node.appendChild(label); + node.appendChild(warning); + + return node; + } + + /** + * Returns the input value. + */ + getValue(): Git.IGitRemote { + return { + url: this._url.value, + name: this._name.value + }; + } + protected _trans: TranslationBundle; + private _model: GitExtension; + private _url: HTMLInputElement; + private _name: HTMLInputElement; +} From 2b0987b0255e50c514a360b9bbcd92135107c431 Mon Sep 17 00:00:00 2001 From: boscochw_ubuntU_ideapad Date: Mon, 11 Jul 2022 16:42:34 -0700 Subject: [PATCH 03/29] Style Add Remote form --- src/widgets/GitAddRemoteForm.tsx | 2 +- style/add-remote-box.css | 10 ++++++++++ style/base.css | 1 + 3 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 style/add-remote-box.css diff --git a/src/widgets/GitAddRemoteForm.tsx b/src/widgets/GitAddRemoteForm.tsx index fca19d3e4..c98072180 100644 --- a/src/widgets/GitAddRemoteForm.tsx +++ b/src/widgets/GitAddRemoteForm.tsx @@ -34,7 +34,7 @@ export class GitAddRemoteForm text.textContent = textContent; this._name = document.createElement('input'); this._name.type = 'text'; - this._name.placeholder = this._trans.__('namename'); + this._name.placeholder = this._trans.__('name'); this._url = document.createElement('input'); this._url.type = 'text'; this._url.placeholder = this._trans.__('Remote GIt repository URL'); diff --git a/style/add-remote-box.css b/style/add-remote-box.css new file mode 100644 index 000000000..b52968689 --- /dev/null +++ b/style/add-remote-box.css @@ -0,0 +1,10 @@ +.jp-AddRemoteBox input { + display: block; + width: 100%; + margin-top: 10px; + margin-bottom: 10px; +} + +jp-AddRemoteBox-warning { + color: var(--jp-warn-color0); +} \ No newline at end of file diff --git a/style/base.css b/style/base.css index 0a1857c04..8be69086e 100644 --- a/style/base.css +++ b/style/base.css @@ -9,3 +9,4 @@ @import url('diff-text.css'); @import url('variables.css'); @import url('status-widget.css'); +@import url('add-remote-box.css'); From b40ba7753373679d244594c057beccc64f142805 Mon Sep 17 00:00:00 2001 From: boscochw_ubuntU_ideapad Date: Mon, 11 Jul 2022 17:57:11 -0700 Subject: [PATCH 04/29] show existing remote on add remote form --- src/commandsAndMenu.tsx | 4 +-- src/model.ts | 15 +++++++++ ...GitAddRemoteForm.tsx => AddRemoteForm.tsx} | 33 +++++++++++++++++-- style/add-remote-box.css | 18 ++++++++++ 4 files changed, 65 insertions(+), 5 deletions(-) rename src/widgets/{GitAddRemoteForm.tsx => AddRemoteForm.tsx} (60%) diff --git a/src/commandsAndMenu.tsx b/src/commandsAndMenu.tsx index 9b22a7659..5fcd54c4d 100644 --- a/src/commandsAndMenu.tsx +++ b/src/commandsAndMenu.tsx @@ -46,7 +46,7 @@ import { } from './tokens'; import { GitCredentialsForm } from './widgets/CredentialsBox'; import { discardAllChanges } from './widgets/discardAllChanges'; -import { GitAddRemoteForm } from './widgets/GitAddRemoteForm'; +import { AddRemoteForm } from './widgets/AddRemoteForm'; import { CheckboxForm } from './widgets/GitResetToRemoteForm'; export interface IGitCloneArgs { @@ -270,7 +270,7 @@ export function addCommands( if (!url) { const remoteRepo = await showDialog({ title: trans.__('Add a remote repository'), - body: new GitAddRemoteForm( + body: new AddRemoteForm( trans, 'Enter remote repository name and url', '', diff --git a/src/model.ts b/src/model.ts index 067f78618..b273e64a5 100644 --- a/src/model.ts +++ b/src/model.ts @@ -453,6 +453,21 @@ export class GitExtension implements IGitExtension { }); } + async getRemotes(): Promise { + //const path = await this._getPathRepository(); + const remotes: Git.IGitRemote[] = [ + { + name: 'origin', + url: 'https://github.com/BoscoCHW/test_private_repo_2.git' + }, + { + name: 'git', + url: 'git@github.com:jupyterlab/jupyterlab-git.git' + } + ]; + return remotes; + } + /** * Retrieve the repository commit log. * diff --git a/src/widgets/GitAddRemoteForm.tsx b/src/widgets/AddRemoteForm.tsx similarity index 60% rename from src/widgets/GitAddRemoteForm.tsx rename to src/widgets/AddRemoteForm.tsx index c98072180..f9e6bb5f7 100644 --- a/src/widgets/GitAddRemoteForm.tsx +++ b/src/widgets/AddRemoteForm.tsx @@ -7,7 +7,7 @@ import { GitExtension } from '../model'; /** * The UI for the add remote repository form */ -export class GitAddRemoteForm +export class AddRemoteForm extends Widget implements Dialog.IBodyWidget { @@ -20,11 +20,12 @@ export class GitAddRemoteForm super(); this._trans = trans; this._model = model; - this.node.appendChild(this.createBody(textContent, warningContent)); + this._addRemoteFormContainer = this.createBody(textContent, warningContent); + this.node.appendChild(this._addRemoteFormContainer); + this._showRemotes(); } private createBody(textContent: string, warningContent: string): HTMLElement { - console.log(this._model); const node = document.createElement('div'); node.className = 'jp-AddRemoteBox'; @@ -53,6 +54,31 @@ export class GitAddRemoteForm return node; } + private async _showRemotes(): Promise { + const remotes: Git.IGitRemote[] = await this._model.getRemotes(); + + const existingRemotesWrapper = document.createElement('div'); + existingRemotesWrapper.className = 'jp-existing-remotes-wrapper'; + const existingRemotesHeader = document.createElement('div'); + existingRemotesHeader.textContent = 'Existing remotes:'; + existingRemotesWrapper.appendChild(existingRemotesHeader); + + const remoteList = document.createElement('ul'); + remoteList.className = 'jp-remote-list'; + remotes.forEach(remote => { + const { name, url } = remote; + const container = document.createElement('li'); + container.innerHTML = ` +
${name}
+
${url}
+ `; + remoteList.appendChild(container); + }); + + existingRemotesWrapper.appendChild(remoteList); + this._addRemoteFormContainer.appendChild(existingRemotesWrapper); + } + /** * Returns the input value. */ @@ -63,6 +89,7 @@ export class GitAddRemoteForm }; } protected _trans: TranslationBundle; + private _addRemoteFormContainer: HTMLElement; private _model: GitExtension; private _url: HTMLInputElement; private _name: HTMLInputElement; diff --git a/style/add-remote-box.css b/style/add-remote-box.css index b52968689..6eb33be4a 100644 --- a/style/add-remote-box.css +++ b/style/add-remote-box.css @@ -5,6 +5,24 @@ margin-bottom: 10px; } +.jp-existing-remotes-wrapper { + margin-top: 20px; + margin-bottom: 10px; +} + +.jp-remote-list { + list-style: none; + margin-top: 5px; + padding: 0px; + display: flex; + flex-direction: column; + row-gap: 3px; +} + +.jp-remote-item { + +} + jp-AddRemoteBox-warning { color: var(--jp-warn-color0); } \ No newline at end of file From ae6785faf06940430701e87692f95fdb4bf93624 Mon Sep 17 00:00:00 2001 From: boscochw_ubuntU_ideapad Date: Tue, 12 Jul 2022 19:17:02 -0700 Subject: [PATCH 05/29] Implement backend api to show remote url info --- jupyterlab_git/git.py | 21 +++++++++++++++++++++ jupyterlab_git/handlers.py | 16 ++++++++++++++++ src/model.ts | 21 ++++++++++----------- src/tokens.ts | 10 ++++++++++ style/add-remote-box.css | 3 --- 5 files changed, 57 insertions(+), 14 deletions(-) diff --git a/jupyterlab_git/git.py b/jupyterlab_git/git.py index 66b1f5bae..36ca03a92 100644 --- a/jupyterlab_git/git.py +++ b/jupyterlab_git/git.py @@ -1512,6 +1512,27 @@ async def remote_show(self, path): return response + async def remote_show_details(self, path): + """Handle call to `git remote -v show` command. + Args: + path (str): Git repository path + + Returns: + List[Tuple(str, str)]: Known remotes (name and url) + """ + command = ["git", "remote", "-v", "show"] + code, output, error = await execute(command, cwd=path) + response = {"code": code, "command": " ".join(command)} + if code == 0: + response["remotes"] = [ + {"name": r.split("\t")[0], "url": r.split("\t")[1]} + for r in output.splitlines() + ] + else: + response["message"] = error + + return response + async def ensure_gitignore(self, path): """Handle call to ensure .gitignore file exists and the next append will be on a new line (this means an empty file diff --git a/jupyterlab_git/handlers.py b/jupyterlab_git/handlers.py index 769f93516..55bb7179d 100644 --- a/jupyterlab_git/handlers.py +++ b/jupyterlab_git/handlers.py @@ -391,6 +391,21 @@ async def post(self, path: str = ""): self.finish(json.dumps(output)) +class GitRemoteShowDetailsHandler(GitHandler): + """Handler for 'git remote -v'.""" + + @tornado.web.authenticated + async def get(self, path: str = ""): + """GET request handler to retrieve existing remotes.""" + local_path = self.url2localpath(path) + output = await self.git.remote_show_details(local_path) + if output["code"] == 0: + self.set_status(201) + else: + self.set_status(500) + self.finish(json.dumps(output)) + + class GitResetHandler(GitHandler): """ Handler for 'git reset '. @@ -871,6 +886,7 @@ def setup_handlers(web_app): ("/push", GitPushHandler), ("/remote/add", GitRemoteAddHandler), ("/remote/fetch", GitFetchHandler), + ("/remote/show", GitRemoteShowDetailsHandler), ("/reset", GitResetHandler), ("/reset_to_commit", GitResetToCommitHandler), ("/show_prefix", GitShowPrefixHandler), diff --git a/src/model.ts b/src/model.ts index b273e64a5..aa359ec74 100644 --- a/src/model.ts +++ b/src/model.ts @@ -454,18 +454,17 @@ export class GitExtension implements IGitExtension { } async getRemotes(): Promise { - //const path = await this._getPathRepository(); - const remotes: Git.IGitRemote[] = [ - { - name: 'origin', - url: 'https://github.com/BoscoCHW/test_private_repo_2.git' - }, - { - name: 'git', - url: 'git@github.com:jupyterlab/jupyterlab-git.git' + const path = await this._getPathRepository(); + const result = await this._taskHandler.execute( + 'git:show:remote', + async () => { + return await requestAPI( + URLExt.join(path, 'remote', 'show'), + 'GET' + ); } - ]; - return remotes; + ); + return result.remotes; } /** diff --git a/src/tokens.ts b/src/tokens.ts index c654bb3e6..2f410ff1c 100644 --- a/src/tokens.ts +++ b/src/tokens.ts @@ -999,6 +999,16 @@ export namespace Git { name: string; } + /** + * Interface for GitRemoteShowDetails request result, + * has the name and urls of all remotes + */ + export interface IGitRemoteResult { + code: number; + command: string; + remotes: Git.IGitRemote[]; + } + /** * Structure for the request to the Git Clone API. */ diff --git a/style/add-remote-box.css b/style/add-remote-box.css index 6eb33be4a..456f1273d 100644 --- a/style/add-remote-box.css +++ b/style/add-remote-box.css @@ -19,9 +19,6 @@ row-gap: 3px; } -.jp-remote-item { - -} jp-AddRemoteBox-warning { color: var(--jp-warn-color0); From f2287fd2144bfb0f767276a84044f7d0645f22d6 Mon Sep 17 00:00:00 2001 From: boscochw_ubuntU_ideapad Date: Wed, 13 Jul 2022 11:43:32 -0700 Subject: [PATCH 06/29] Refactor remote_show function to handle verbose case --- jupyterlab_git/git.py | 37 ++++++++++++++----------------------- jupyterlab_git/handlers.py | 6 +++--- 2 files changed, 17 insertions(+), 26 deletions(-) diff --git a/jupyterlab_git/git.py b/jupyterlab_git/git.py index 36ca03a92..9da35df15 100644 --- a/jupyterlab_git/git.py +++ b/jupyterlab_git/git.py @@ -1494,40 +1494,31 @@ async def remote_add(self, path, url, name=DEFAULT_REMOTE_NAME): return response - async def remote_show(self, path): + async def remote_show(self, path, verbose=False): """Handle call to `git remote show` command. Args: path (str): Git repository path - + verbose (bool): true if details are needed, otherwise, false Returns: List[str]: Known remotes """ - command = ["git", "remote", "show"] - code, output, error = await execute(command, cwd=path) - response = {"code": code, "command": " ".join(command)} - if code == 0: - response["remotes"] = [r.strip() for r in output.splitlines()] + command = ["git", "remote"] + if verbose: + command.extend(["-v", "show"]) else: - response["message"] = error - - return response + command.append("show") - async def remote_show_details(self, path): - """Handle call to `git remote -v show` command. - Args: - path (str): Git repository path - - Returns: - List[Tuple(str, str)]: Known remotes (name and url) - """ - command = ["git", "remote", "-v", "show"] code, output, error = await execute(command, cwd=path) response = {"code": code, "command": " ".join(command)} + if code == 0: - response["remotes"] = [ - {"name": r.split("\t")[0], "url": r.split("\t")[1]} - for r in output.splitlines() - ] + if verbose: + response["remotes"] = [ + {"name": r.split("\t")[0], "url": r.split("\t")[1]} + for r in output.splitlines() + ] + else: + response["remotes"] = [r.strip() for r in output.splitlines()] else: response["message"] = error diff --git a/jupyterlab_git/handlers.py b/jupyterlab_git/handlers.py index 55bb7179d..207f0d786 100644 --- a/jupyterlab_git/handlers.py +++ b/jupyterlab_git/handlers.py @@ -391,14 +391,14 @@ async def post(self, path: str = ""): self.finish(json.dumps(output)) -class GitRemoteShowDetailsHandler(GitHandler): +class GitRemoteDetailsShowHandler(GitHandler): """Handler for 'git remote -v'.""" @tornado.web.authenticated async def get(self, path: str = ""): """GET request handler to retrieve existing remotes.""" local_path = self.url2localpath(path) - output = await self.git.remote_show_details(local_path) + output = await self.git.remote_show(local_path, verbose=True) if output["code"] == 0: self.set_status(201) else: @@ -886,7 +886,7 @@ def setup_handlers(web_app): ("/push", GitPushHandler), ("/remote/add", GitRemoteAddHandler), ("/remote/fetch", GitFetchHandler), - ("/remote/show", GitRemoteShowDetailsHandler), + ("/remote/show", GitRemoteDetailsShowHandler), ("/reset", GitResetHandler), ("/reset_to_commit", GitResetToCommitHandler), ("/show_prefix", GitShowPrefixHandler), From 5af1c3bc60345ab139ae974622762d0bbabb9d71 Mon Sep 17 00:00:00 2001 From: boscochw_ubuntU_ideapad Date: Thu, 14 Jul 2022 14:51:15 -0700 Subject: [PATCH 07/29] Implement remote dialog box using React --- src/commandsAndMenu.tsx | 40 ++++--- src/components/AddRemoteDialogue.tsx | 155 +++++++++++++++++++++++++++ src/style/AddRemoteDialog.ts | 11 ++ src/widgets/AddRemoteForm.tsx | 96 ----------------- style/add-remote-box.css | 25 ----- style/base.css | 1 - 6 files changed, 193 insertions(+), 135 deletions(-) create mode 100644 src/components/AddRemoteDialogue.tsx create mode 100644 src/style/AddRemoteDialog.ts delete mode 100644 src/widgets/AddRemoteForm.tsx delete mode 100644 style/add-remote-box.css diff --git a/src/commandsAndMenu.tsx b/src/commandsAndMenu.tsx index 5fcd54c4d..7ce257ba6 100644 --- a/src/commandsAndMenu.tsx +++ b/src/commandsAndMenu.tsx @@ -46,7 +46,7 @@ import { } from './tokens'; import { GitCredentialsForm } from './widgets/CredentialsBox'; import { discardAllChanges } from './widgets/discardAllChanges'; -import { AddRemoteForm } from './widgets/AddRemoteForm'; +import { AddRemoteDialogue } from './components/AddRemoteDialogue'; import { CheckboxForm } from './widgets/GitResetToRemoteForm'; export interface IGitCloneArgs { @@ -268,19 +268,33 @@ export function addCommands( let name = args['name'] as string; if (!url) { - const remoteRepo = await showDialog({ - title: trans.__('Add a remote repository'), - body: new AddRemoteForm( - trans, - 'Enter remote repository name and url', - '', - gitModel - ) - }); + const widgetId = 'git-dialog-AddRemote'; + let anchor = document.querySelector(`#${widgetId}`); + if (!anchor) { + anchor = document.createElement('div'); + anchor.id = widgetId; + document.body.appendChild(anchor); + } + + const waitForDialog = new PromiseDelegate(); + const dialog = ReactWidget.create( + { + dialog.dispose(); + waitForDialog.resolve(remote ?? null); + }} + /> + ); + + Widget.attach(dialog, anchor); + + const remote = await waitForDialog.promise; - if (remoteRepo.button.accept) { - name = remoteRepo.value.name; - url = remoteRepo.value.url; + if (remote) { + name = remote.name; + url = remote.url; } } diff --git a/src/components/AddRemoteDialogue.tsx b/src/components/AddRemoteDialogue.tsx new file mode 100644 index 000000000..a7f85d086 --- /dev/null +++ b/src/components/AddRemoteDialogue.tsx @@ -0,0 +1,155 @@ +import * as React from 'react'; +import ClearIcon from '@material-ui/icons/Clear'; +import Dialog from '@material-ui/core/Dialog'; +import DialogActions from '@material-ui/core/DialogActions'; +import { Git } from '../tokens'; +import { GitExtension } from '../model'; +import { remoteDialogClass } from '../style/AddRemoteDialog'; +import { TranslationBundle } from '@jupyterlab/translation'; + +import { classes } from 'typestyle'; +import { + actionsWrapperClass, + buttonClass, + cancelButtonClass, + closeButtonClass, + contentWrapperClass, + createButtonClass, + titleClass, + titleWrapperClass +} from '../style/NewBranchDialog'; + +export interface IAddRemoteDialogueProps { + /** + * The application language translator. + */ + trans: TranslationBundle; + warningContent?: string; + model: GitExtension; + onClose: (remote?: Git.IGitRemote) => void; +} + +export interface IAddRemoteDialogueState { + newRemote: Git.IGitRemote; + existingRemotes: Git.IGitRemote[]; +} + +export class AddRemoteDialogue extends React.Component< + IAddRemoteDialogueProps, + IAddRemoteDialogueState +> { + constructor(props: IAddRemoteDialogueProps) { + super(props); + this.state = { + newRemote: { + name: '', + url: '' + }, + existingRemotes: [] + }; + } + + async componentDidMount(): Promise { + const remotes = await this.props.model.getRemotes(); + this.setState({ existingRemotes: remotes }); + } + + render(): JSX.Element { + return ( + +
+

{this.props.trans.__('Manage Remotes')}

+ +
+
+ + + {this.props.warningContent && ( +
+ {this.props.warningContent} +
+ )} + + {this.state.existingRemotes.length > 0 && ( +
    +

    Existing Remotes:

    + {this.state.existingRemotes.map((remote, index) => ( +
  • + {remote.name} + {remote.url} +
  • + ))} +
+ )} +
+ + { + this.props.onClose(); + }} + /> + { + this.props.onClose(this.state.newRemote); + }} + disabled={!this.state.newRemote.name || !this.state.newRemote.url} + /> + +
+ ); + } +} diff --git a/src/style/AddRemoteDialog.ts b/src/style/AddRemoteDialog.ts new file mode 100644 index 000000000..0238a5ae5 --- /dev/null +++ b/src/style/AddRemoteDialog.ts @@ -0,0 +1,11 @@ +import { style } from 'typestyle'; + +export const remoteDialogClass = style({ + width: '400px', + + color: 'var(--jp-ui-font-color1)!important', + + borderRadius: '3px!important', + + backgroundColor: 'var(--jp-layout-color1)!important' +}); diff --git a/src/widgets/AddRemoteForm.tsx b/src/widgets/AddRemoteForm.tsx deleted file mode 100644 index f9e6bb5f7..000000000 --- a/src/widgets/AddRemoteForm.tsx +++ /dev/null @@ -1,96 +0,0 @@ -import { Dialog } from '@jupyterlab/apputils'; -import { TranslationBundle } from '@jupyterlab/translation'; -import { Widget } from '@lumino/widgets'; -import { Git } from '../tokens'; -import { GitExtension } from '../model'; - -/** - * The UI for the add remote repository form - */ -export class AddRemoteForm - extends Widget - implements Dialog.IBodyWidget -{ - constructor( - trans: TranslationBundle, - textContent = trans.__('Enter remote repository name and url'), - warningContent = '', - model: GitExtension - ) { - super(); - this._trans = trans; - this._model = model; - this._addRemoteFormContainer = this.createBody(textContent, warningContent); - this.node.appendChild(this._addRemoteFormContainer); - this._showRemotes(); - } - - private createBody(textContent: string, warningContent: string): HTMLElement { - const node = document.createElement('div'); - node.className = 'jp-AddRemoteBox'; - - const label = document.createElement('label'); - - const text = document.createElement('span'); - text.textContent = textContent; - this._name = document.createElement('input'); - this._name.type = 'text'; - this._name.placeholder = this._trans.__('name'); - this._url = document.createElement('input'); - this._url.type = 'text'; - this._url.placeholder = this._trans.__('Remote GIt repository URL'); - - label.appendChild(text); - label.appendChild(this._name); - label.appendChild(this._url); - - const warning = document.createElement('div'); - warning.className = 'jp-AddRemoteBox-warning'; - warning.textContent = warningContent; - - node.appendChild(label); - node.appendChild(warning); - - return node; - } - - private async _showRemotes(): Promise { - const remotes: Git.IGitRemote[] = await this._model.getRemotes(); - - const existingRemotesWrapper = document.createElement('div'); - existingRemotesWrapper.className = 'jp-existing-remotes-wrapper'; - const existingRemotesHeader = document.createElement('div'); - existingRemotesHeader.textContent = 'Existing remotes:'; - existingRemotesWrapper.appendChild(existingRemotesHeader); - - const remoteList = document.createElement('ul'); - remoteList.className = 'jp-remote-list'; - remotes.forEach(remote => { - const { name, url } = remote; - const container = document.createElement('li'); - container.innerHTML = ` -
${name}
-
${url}
- `; - remoteList.appendChild(container); - }); - - existingRemotesWrapper.appendChild(remoteList); - this._addRemoteFormContainer.appendChild(existingRemotesWrapper); - } - - /** - * Returns the input value. - */ - getValue(): Git.IGitRemote { - return { - url: this._url.value, - name: this._name.value - }; - } - protected _trans: TranslationBundle; - private _addRemoteFormContainer: HTMLElement; - private _model: GitExtension; - private _url: HTMLInputElement; - private _name: HTMLInputElement; -} diff --git a/style/add-remote-box.css b/style/add-remote-box.css deleted file mode 100644 index 456f1273d..000000000 --- a/style/add-remote-box.css +++ /dev/null @@ -1,25 +0,0 @@ -.jp-AddRemoteBox input { - display: block; - width: 100%; - margin-top: 10px; - margin-bottom: 10px; -} - -.jp-existing-remotes-wrapper { - margin-top: 20px; - margin-bottom: 10px; -} - -.jp-remote-list { - list-style: none; - margin-top: 5px; - padding: 0px; - display: flex; - flex-direction: column; - row-gap: 3px; -} - - -jp-AddRemoteBox-warning { - color: var(--jp-warn-color0); -} \ No newline at end of file diff --git a/style/base.css b/style/base.css index 8be69086e..0a1857c04 100644 --- a/style/base.css +++ b/style/base.css @@ -9,4 +9,3 @@ @import url('diff-text.css'); @import url('variables.css'); @import url('status-widget.css'); -@import url('add-remote-box.css'); From eda196af7cc90867ee1a298e1ecb1731725ede4d Mon Sep 17 00:00:00 2001 From: boscochw_ubuntU_ideapad Date: Thu, 14 Jul 2022 15:42:45 -0700 Subject: [PATCH 08/29] Style remote dialog --- src/components/AddRemoteDialogue.tsx | 17 +++++++++++------ src/style/AddRemoteDialog.ts | 28 ++++++++++++++++++++++++++-- 2 files changed, 37 insertions(+), 8 deletions(-) diff --git a/src/components/AddRemoteDialogue.tsx b/src/components/AddRemoteDialogue.tsx index a7f85d086..d38da004a 100644 --- a/src/components/AddRemoteDialogue.tsx +++ b/src/components/AddRemoteDialogue.tsx @@ -4,7 +4,12 @@ import Dialog from '@material-ui/core/Dialog'; import DialogActions from '@material-ui/core/DialogActions'; import { Git } from '../tokens'; import { GitExtension } from '../model'; -import { remoteDialogClass } from '../style/AddRemoteDialog'; +import { + remoteDialogClass, + remoteDialogInputClass, + existingRemoteListClass, + existingRemoteItemClass +} from '../style/AddRemoteDialog'; import { TranslationBundle } from '@jupyterlab/translation'; import { classes } from 'typestyle'; @@ -76,10 +81,10 @@ export class AddRemoteDialogue extends React.Component<
-
diff --git a/src/style/AddRemoteDialog.ts b/src/style/AddRemoteDialog.ts index 215384ec7..51041339f 100644 --- a/src/style/AddRemoteDialog.ts +++ b/src/style/AddRemoteDialog.ts @@ -19,14 +19,15 @@ export const remoteDialogInputClass = style({ } }); -export const existingRemoteListClass = style({ +export const existingRemoteWrapperClass = style({ marginTop: '1.5rem', - listStyle: 'none', padding: '0px' }); -export const existingRemoteItemClass = style({ +export const existingRemoteGridClass = style({ marginTop: '2px', - display: 'flex', - columnGap: '5px' + display: 'grid', + rowGap: '5px', + columnGap: '10px', + gridTemplateColumns: 'auto auto auto' }); From 99df0f31bd27175bfbe3eea7b9a8bc25c6d6ef69 Mon Sep 17 00:00:00 2001 From: boscochw_ubuntU_ideapad Date: Fri, 22 Jul 2022 11:00:31 -0700 Subject: [PATCH 17/29] Fix git remote remove route bug --- jupyterlab_git/handlers.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/jupyterlab_git/handlers.py b/jupyterlab_git/handlers.py index 8a0d2c28f..30edbdc5a 100644 --- a/jupyterlab_git/handlers.py +++ b/jupyterlab_git/handlers.py @@ -410,17 +410,16 @@ class GitRemoteRemoveHandler(GitHandler): """Handler for 'git remote remove '.""" @tornado.web.authenticated - async def delete(self, path: str = ""): + async def delete(self, path: str = "", name: str = ""): """DELETE request handler to remove a remote.""" local_path = self.url2localpath(path) - name = self.path_kwargs.get("name", "") output = await self.git.remote_remove(local_path, name) if output["code"] == 0: self.set_status(204) else: self.set_status(500) - self.finish(json.dumps(output)) + self.finish(json.dumps(output)) class GitResetHandler(GitHandler): From 5e505c00d915d9185f2bf3d0975325a373bf5d6f Mon Sep 17 00:00:00 2001 From: boscochw_ubuntU_ideapad Date: Fri, 22 Jul 2022 12:49:24 -0700 Subject: [PATCH 18/29] Implement advanced push dialog box --- schema/plugin.json | 2 +- src/commandsAndMenu.tsx | 26 ++--- src/widgets/AdvancedPushForm.tsx | 103 ++++++++++++++++++ src/widgets/SelectRemoteForm.tsx | 70 ------------ ...ection-form.css => advanced-push-form.css} | 11 +- style/base.css | 2 +- 6 files changed, 126 insertions(+), 88 deletions(-) create mode 100644 src/widgets/AdvancedPushForm.tsx delete mode 100644 src/widgets/SelectRemoteForm.tsx rename style/{single-selection-form.css => advanced-push-form.css} (62%) diff --git a/schema/plugin.json b/schema/plugin.json index ad8528588..6339889a7 100644 --- a/schema/plugin.json +++ b/schema/plugin.json @@ -100,7 +100,7 @@ }, { "command": "git:push", - "args": { "force": true } + "args": { "advanced": true } }, { "command": "git:pull" diff --git a/src/commandsAndMenu.tsx b/src/commandsAndMenu.tsx index 936e4e799..7d0560490 100644 --- a/src/commandsAndMenu.tsx +++ b/src/commandsAndMenu.tsx @@ -48,7 +48,7 @@ import { GitCredentialsForm } from './widgets/CredentialsBox'; import { discardAllChanges } from './widgets/discardAllChanges'; import { AddRemoteDialogue } from './components/AddRemoteDialogue'; import { CheckboxForm } from './widgets/GitResetToRemoteForm'; -import { SingleSelectionForm } from './widgets/SelectRemoteForm'; +import { AdvancedPushForm } from './widgets/AdvancedPushForm'; export interface IGitCloneArgs { /** @@ -326,30 +326,28 @@ export function addCommands( /** Add git push command */ commands.addCommand(CommandIDs.gitPush, { label: args => - args.force - ? trans.__('Push to Remote (Force)') + (args['advanced'] as boolean) + ? trans.__('Push to Remote (Advanced)') : trans.__('Push to Remote'), caption: trans.__('Push code to remote repository'), isEnabled: () => gitModel.pathRepository !== null, execute: async args => { try { - const remotes = await gitModel.getRemotes(); - let remote; - if (remotes.length > 1) { + let force; + + if (args['advanced'] as boolean) { const result = await showDialog({ - title: trans.__('Pick a remote repository to push.'), - body: new SingleSelectionForm( - trans.__(''), - remotes.map(remote => remote.name) - ), + title: trans.__('Please select push options.'), + body: new AdvancedPushForm(trans, gitModel), buttons: [ Dialog.cancelButton({ label: trans.__('Cancel') }), Dialog.okButton({ label: trans.__('Proceed') }) ] }); if (result.button.accept) { - remote = result.value.selection; + remote = result.value.remoteName; + force = result.value.force; } else { return; } @@ -361,7 +359,7 @@ export function addCommands( }); const details = await showGitOperationDialog( gitModel, - args.force ? Operation.ForcePush : Operation.Push, + force ? Operation.ForcePush : Operation.Push, trans, (args = { remote }) ); @@ -1320,7 +1318,7 @@ export function createGitMenu( ].forEach(command => { menu.addItem({ command }); if (command === CommandIDs.gitPush) { - menu.addItem({ command, args: { force: true } }); + menu.addItem({ command, args: { advanced: true } }); } if (command === CommandIDs.gitPull) { menu.addItem({ command, args: { force: true } }); diff --git a/src/widgets/AdvancedPushForm.tsx b/src/widgets/AdvancedPushForm.tsx new file mode 100644 index 000000000..86446248c --- /dev/null +++ b/src/widgets/AdvancedPushForm.tsx @@ -0,0 +1,103 @@ +import { Dialog } from '@jupyterlab/apputils'; +import { Widget } from '@lumino/widgets'; +import { GitExtension } from '../model'; +import { TranslationBundle } from '@jupyterlab/translation'; + +/** + * Interface for returned value from dialog box + */ +export interface IAdvancedPushFormValue { + remoteName: string; + force: boolean; +} + +/** + * A widget form with advanced push options, + * can be used as a Dialog body. + */ +export class AdvancedPushForm + extends Widget + implements Dialog.IBodyWidget +{ + constructor(trans: TranslationBundle, model: GitExtension) { + super(); + this._trans = trans; + this._model = model; + this._radioButtons = []; + this.node.appendChild(this.createBody()); + this.addRemoteOptions(); + } + + private createBody(): HTMLElement { + const mainNode = document.createElement('div'); + + // Instructional text + const text = document.createElement('div'); + text.textContent = this._trans.__('Choose a remote to push to.'); + + // List of remotes + const remoteOptionsContainer = document.createElement('div'); + remoteOptionsContainer.className = 'jp-remote-options-wrapper'; + this._remoteOptionsContainer = remoteOptionsContainer; + + // Force option + const forceCheckboxContainer = document.createElement('label'); + forceCheckboxContainer.className = 'jp-force-box-container'; + + this._forceCheckbox = document.createElement('input'); + this._forceCheckbox.type = 'checkbox'; + this._forceCheckbox.checked = false; + + const label = document.createElement('span'); + label.textContent = this._trans.__('Force Push'); + + forceCheckboxContainer.appendChild(this._forceCheckbox); + forceCheckboxContainer.appendChild(label); + + mainNode.appendChild(text); + mainNode.appendChild(remoteOptionsContainer); + mainNode.appendChild(forceCheckboxContainer); + + return mainNode; + } + + private async addRemoteOptions(): Promise { + const remotes = await this._model.getRemotes(); + + remotes.forEach(remote => { + const buttonWrapper = document.createElement('div'); + buttonWrapper.className = 'jp-button-wrapper'; + const radioButton = document.createElement('input'); + radioButton.type = 'radio'; + radioButton.id = remote.name; + radioButton.value = remote.name; + radioButton.name = 'option'; + radioButton.className = 'jp-option'; + if (remote.name === 'origin') { + radioButton.checked = true; + } + this._radioButtons.push(radioButton); + + const label = document.createElement('label'); + label.htmlFor = remote.name; + label.textContent = remote.name; + + buttonWrapper.appendChild(radioButton); + buttonWrapper.appendChild(label); + this._remoteOptionsContainer.appendChild(buttonWrapper); + }); + } + + getValue(): IAdvancedPushFormValue { + return { + remoteName: this._radioButtons.find(rb => rb.checked).value, + force: this._forceCheckbox.checked + }; + } + + private _trans: TranslationBundle; + private _model: GitExtension; + private _remoteOptionsContainer: HTMLElement; + private _radioButtons: HTMLInputElement[]; + private _forceCheckbox: HTMLInputElement; +} diff --git a/src/widgets/SelectRemoteForm.tsx b/src/widgets/SelectRemoteForm.tsx deleted file mode 100644 index 5f4891dd6..000000000 --- a/src/widgets/SelectRemoteForm.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import { Dialog } from '@jupyterlab/apputils'; -import { Widget } from '@lumino/widgets'; - -/** - * Interface for returned value from dialog box - */ -export interface ISingleSelectionFormValue { - selection: string; -} - -/** - * A widget form containing a text block and a list of options, - * can be used as a Dialog body. - */ -export class SingleSelectionForm - extends Widget - implements Dialog.IBodyWidget -{ - constructor(textBody: string, options: string[]) { - super(); - this._radioButtons = []; - this.node.appendChild(this.createBody(textBody, options)); - } - - private createBody(textBody: string, options: string[]): HTMLElement { - const mainNode = document.createElement('div'); - - const text = document.createElement('div'); - text.textContent = textBody; - - const optionsContainer = document.createElement('div'); - optionsContainer.className = 'jp-options-wrapper'; - - options.forEach(option => { - const buttonWrapper = document.createElement('div'); - buttonWrapper.className = 'jp-button-wrapper'; - const radioButton = document.createElement('input'); - radioButton.type = 'radio'; - radioButton.id = option; - radioButton.value = option; - radioButton.name = 'option'; - radioButton.className = 'jp-option'; - if (option === 'origin') { - radioButton.checked = true; - } - this._radioButtons.push(radioButton); - - const label = document.createElement('label'); - label.htmlFor = option; - label.textContent = option; - - buttonWrapper.appendChild(radioButton); - buttonWrapper.appendChild(label); - optionsContainer.appendChild(buttonWrapper); - }); - - mainNode.appendChild(text); - mainNode.appendChild(optionsContainer); - - return mainNode; - } - - getValue(): ISingleSelectionFormValue { - return { - selection: this._radioButtons.find(rb => rb.checked).value - }; - } - - private _radioButtons: HTMLInputElement[]; -} diff --git a/style/single-selection-form.css b/style/advanced-push-form.css similarity index 62% rename from style/single-selection-form.css rename to style/advanced-push-form.css index c245ff793..b308fae83 100644 --- a/style/single-selection-form.css +++ b/style/advanced-push-form.css @@ -1,4 +1,5 @@ -.jp-options-wrapper { +.jp-remote-options-wrapper { + margin: 4px; display: flex; flex-direction: column; align-items: stretch; @@ -15,5 +16,11 @@ height: fit-content !important; appearance: auto !important; margin: 0px; - padding: 0px; +} + +.jp-force-box-container { + margin-top: 1rem; + display: flex; + align-items: flex-end; + column-gap: 5px; } \ No newline at end of file diff --git a/style/base.css b/style/base.css index f3b19d248..a7596bc69 100644 --- a/style/base.css +++ b/style/base.css @@ -9,4 +9,4 @@ @import url('diff-text.css'); @import url('variables.css'); @import url('status-widget.css'); -@import url('single-selection-form.css'); +@import url('advanced-push-form.css'); From f077fc045b2a4996be4c35760cbf580f51d5c2df Mon Sep 17 00:00:00 2001 From: boscochw_ubuntU_ideapad Date: Fri, 22 Jul 2022 12:57:24 -0700 Subject: [PATCH 19/29] Show remote url in advanced push dialog and increase text font size --- src/widgets/AdvancedPushForm.tsx | 3 ++- style/advanced-push-form.css | 4 ++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/widgets/AdvancedPushForm.tsx b/src/widgets/AdvancedPushForm.tsx index 86446248c..015371ed9 100644 --- a/src/widgets/AdvancedPushForm.tsx +++ b/src/widgets/AdvancedPushForm.tsx @@ -33,6 +33,7 @@ export class AdvancedPushForm // Instructional text const text = document.createElement('div'); + text.className = 'jp-remote-text'; text.textContent = this._trans.__('Choose a remote to push to.'); // List of remotes @@ -80,7 +81,7 @@ export class AdvancedPushForm const label = document.createElement('label'); label.htmlFor = remote.name; - label.textContent = remote.name; + label.textContent = `${remote.name}: ${remote.url}`; buttonWrapper.appendChild(radioButton); buttonWrapper.appendChild(label); diff --git a/style/advanced-push-form.css b/style/advanced-push-form.css index b308fae83..0cc8de83b 100644 --- a/style/advanced-push-form.css +++ b/style/advanced-push-form.css @@ -1,3 +1,7 @@ +.jp-remote-text { + font-size: 1rem; +} + .jp-remote-options-wrapper { margin: 4px; display: flex; From dd214d7491666216df91b4f8bab50c9269019e57 Mon Sep 17 00:00:00 2001 From: boscochw_ubuntU_ideapad Date: Fri, 29 Jul 2022 11:48:32 -0700 Subject: [PATCH 20/29] Move dialog action buttons to just below input fields and display message when loading remotes --- src/commandsAndMenu.tsx | 4 +- src/components/AddRemoteDialogue.tsx | 70 +++++++++++++++------------- src/style/AddRemoteDialog.ts | 7 ++- 3 files changed, 46 insertions(+), 35 deletions(-) diff --git a/src/commandsAndMenu.tsx b/src/commandsAndMenu.tsx index 7d0560490..a392fa8cd 100644 --- a/src/commandsAndMenu.tsx +++ b/src/commandsAndMenu.tsx @@ -255,8 +255,8 @@ export function addCommands( /** Command to add a remote Git repository */ commands.addCommand(CommandIDs.gitAddRemote, { - label: trans.__('Add Remote Repository'), - caption: trans.__('Add a Git remote repository'), + label: trans.__('Manage Remote Repositories'), + caption: trans.__('Manage Remote Repositories'), isEnabled: () => gitModel.pathRepository !== null, execute: async args => { if (gitModel.pathRepository === null) { diff --git a/src/components/AddRemoteDialogue.tsx b/src/components/AddRemoteDialogue.tsx index 21e9937f1..d09a56ef6 100644 --- a/src/components/AddRemoteDialogue.tsx +++ b/src/components/AddRemoteDialogue.tsx @@ -9,13 +9,13 @@ import { remoteDialogClass, remoteDialogInputClass, existingRemoteWrapperClass, - existingRemoteGridClass + existingRemoteGridClass, + actionsWrapperClass } from '../style/AddRemoteDialog'; import { TranslationBundle } from '@jupyterlab/translation'; import { classes } from 'typestyle'; import { - actionsWrapperClass, buttonClass, cancelButtonClass, closeButtonClass, @@ -54,7 +54,7 @@ export interface IAddRemoteDialogueState { /** * List of known remotes */ - existingRemotes: Git.IGitRemote[]; + existingRemotes: Git.IGitRemote[] | null; } export class AddRemoteDialogue extends React.Component< @@ -68,7 +68,7 @@ export class AddRemoteDialogue extends React.Component< name: '', url: '' }, - existingRemotes: [] + existingRemotes: null }; } @@ -137,9 +137,36 @@ export class AddRemoteDialogue extends React.Component< )} - {this.state.existingRemotes.length > 0 && ( -
-

{this.props.trans.__('Existing Remotes:')}

+ + { + this.props.onClose(); + }} + /> + { + this.props.onClose(this.state.newRemote); + }} + disabled={!this.state.newRemote.name || !this.state.newRemote.url} + /> + + +
+

{this.props.trans.__('Existing Remotes:')}

+ + {this.state.existingRemotes === null ? ( +

Loading remote repositories...

+ ) : this.state.existingRemotes.length > 0 ? (
{this.state.existingRemotes.map((remote, index) => ( <> @@ -160,32 +187,11 @@ export class AddRemoteDialogue extends React.Component< ))}
-
- )} -
- - This repository does not have any remote.

)} - value={this.props.trans.__('Cancel')} - onClick={() => { - this.props.onClose(); - }} - /> - { - this.props.onClose(this.state.newRemote); - }} - disabled={!this.state.newRemote.name || !this.state.newRemote.url} - /> -
+ + ); } diff --git a/src/style/AddRemoteDialog.ts b/src/style/AddRemoteDialog.ts index 51041339f..a4ee2bd53 100644 --- a/src/style/AddRemoteDialog.ts +++ b/src/style/AddRemoteDialog.ts @@ -19,8 +19,13 @@ export const remoteDialogInputClass = style({ } }); +export const actionsWrapperClass = style({ + padding: '15px 0px !important', + justifyContent: 'space-around !important' +}); + export const existingRemoteWrapperClass = style({ - marginTop: '1.5rem', + margin: '1.5rem 0rem 1rem', padding: '0px' }); From 30fdc4231ff8e3bf2f0c316116c84998c3ba54d0 Mon Sep 17 00:00:00 2001 From: boscochw_ubuntU_ideapad Date: Fri, 29 Jul 2022 12:34:38 -0700 Subject: [PATCH 21/29] Display loading message when getting remote information and handle no remote case --- src/widgets/AdvancedPushForm.tsx | 67 ++++++++++++++++++++------------ style/advanced-push-form.css | 2 +- 2 files changed, 44 insertions(+), 25 deletions(-) diff --git a/src/widgets/AdvancedPushForm.tsx b/src/widgets/AdvancedPushForm.tsx index 015371ed9..aa5827f79 100644 --- a/src/widgets/AdvancedPushForm.tsx +++ b/src/widgets/AdvancedPushForm.tsx @@ -7,7 +7,13 @@ import { TranslationBundle } from '@jupyterlab/translation'; * Interface for returned value from dialog box */ export interface IAdvancedPushFormValue { + /** + * The name of the remote repository to push to. + */ remoteName: string; + /** + * Whether to use force push. + */ force: boolean; } @@ -39,6 +45,11 @@ export class AdvancedPushForm // List of remotes const remoteOptionsContainer = document.createElement('div'); remoteOptionsContainer.className = 'jp-remote-options-wrapper'; + const loadingMessage = document.createElement('div'); + loadingMessage.textContent = this._trans.__( + 'Loading remote repositories...' + ); + remoteOptionsContainer.appendChild(loadingMessage); this._remoteOptionsContainer = remoteOptionsContainer; // Force option @@ -64,34 +75,42 @@ export class AdvancedPushForm private async addRemoteOptions(): Promise { const remotes = await this._model.getRemotes(); - - remotes.forEach(remote => { - const buttonWrapper = document.createElement('div'); - buttonWrapper.className = 'jp-button-wrapper'; - const radioButton = document.createElement('input'); - radioButton.type = 'radio'; - radioButton.id = remote.name; - radioButton.value = remote.name; - radioButton.name = 'option'; - radioButton.className = 'jp-option'; - if (remote.name === 'origin') { - radioButton.checked = true; - } - this._radioButtons.push(radioButton); - - const label = document.createElement('label'); - label.htmlFor = remote.name; - label.textContent = `${remote.name}: ${remote.url}`; - - buttonWrapper.appendChild(radioButton); - buttonWrapper.appendChild(label); - this._remoteOptionsContainer.appendChild(buttonWrapper); - }); + this._remoteOptionsContainer.innerHTML = ''; + if (remotes.length > 0) { + remotes.forEach(remote => { + const buttonWrapper = document.createElement('div'); + buttonWrapper.className = 'jp-button-wrapper'; + const radioButton = document.createElement('input'); + radioButton.type = 'radio'; + radioButton.id = remote.name; + radioButton.value = remote.name; + radioButton.name = 'option'; + radioButton.className = 'jp-option'; + if (remote.name === 'origin') { + radioButton.checked = true; + } + this._radioButtons.push(radioButton); + + const label = document.createElement('label'); + label.htmlFor = remote.name; + label.textContent = `${remote.name}: ${remote.url}`; + + buttonWrapper.appendChild(radioButton); + buttonWrapper.appendChild(label); + this._remoteOptionsContainer.appendChild(buttonWrapper); + }); + } else { + const noRemoteMsg = document.createElement('div'); + noRemoteMsg.textContent = this._trans.__( + 'This repository has no known remotes.' + ); + this._remoteOptionsContainer.appendChild(noRemoteMsg); + } } getValue(): IAdvancedPushFormValue { return { - remoteName: this._radioButtons.find(rb => rb.checked).value, + remoteName: this._radioButtons.find(rb => rb.checked)?.value, force: this._forceCheckbox.checked }; } diff --git a/style/advanced-push-form.css b/style/advanced-push-form.css index 0cc8de83b..314f3f7d8 100644 --- a/style/advanced-push-form.css +++ b/style/advanced-push-form.css @@ -12,7 +12,7 @@ .jp-button-wrapper { display: flex; - gap: 1rem; + gap: 0.5rem; align-items: center; } From 386bf3b7ebe7edb7412d7ad36db161314928edca Mon Sep 17 00:00:00 2001 From: boscochw_ubuntU_ideapad Date: Tue, 2 Aug 2022 13:44:56 -0700 Subject: [PATCH 22/29] Add tests for remote_show and remote_remove --- jupyterlab_git/tests/test_remote.py | 82 ++++++++++++++++++++++++++++- 1 file changed, 80 insertions(+), 2 deletions(-) diff --git a/jupyterlab_git/tests/test_remote.py b/jupyterlab_git/tests/test_remote.py index 1ed6b1c07..8e5ccc335 100644 --- a/jupyterlab_git/tests/test_remote.py +++ b/jupyterlab_git/tests/test_remote.py @@ -1,11 +1,11 @@ import json from unittest.mock import patch - +import os import pytest import tornado +from jupyterlab_git.git import Git from jupyterlab_git.handlers import NAMESPACE - from .testutils import assert_http_error, maybe_future @@ -101,3 +101,81 @@ async def test_git_add_remote_failure(mock_execute, jp_fetch, jp_root_dir): mock_execute.assert_called_once_with( ["git", "remote", "add", "origin", url], cwd=str(local_path) ) + + +@patch("jupyterlab_git.git.execute") +async def test_git_remote_show(mock_execute, jp_root_dir): + # Given + local_path = jp_root_dir / "test_path" + mock_execute.return_value = maybe_future( + (0, os.linesep.join(["origin", "test"]), "") + ) + + # When + output = await Git().remote_show(str(local_path), False) + + # Then + command = ["git", "remote", "show"] + mock_execute.assert_called_once_with(command, cwd=str(local_path)) + assert output == { + "code": 0, + "command": " ".join(command), + "remotes": ["origin", "test"], + } + + +@patch("jupyterlab_git.git.execute") +async def test_git_remote_show_verbose(mock_execute, jp_fetch, jp_root_dir): + # Given + local_path = jp_root_dir / "test_path" + url = "http://github.com/myid/myrepository.git" + process_output = os.linesep.join( + [f"origin\t{url} (fetch)", f"origin\t{url} (push)"] + ) + mock_execute.return_value = maybe_future((0, process_output, "")) + + # When + response = await jp_fetch( + NAMESPACE, + local_path.name, + "remote", + "show", + method="GET", + ) + + # Then + command = ["git", "remote", "-v", "show"] + mock_execute.assert_called_once_with(command, cwd=str(local_path)) + + assert response.code == 200 + payload = json.loads(response.body) + assert payload == { + "code": 0, + "command": " ".join(command), + "remotes": [ + {"name": "origin", "url": "http://github.com/myid/myrepository.git"} + ], + } + + +@patch("jupyterlab_git.git.execute") +async def test_git_remote_remove(mock_execute, jp_fetch, jp_root_dir): + # Given + local_path = jp_root_dir / "test_path" + mock_execute.return_value = maybe_future((0, "", "")) + + # When + name = "origin" + response = await jp_fetch( + NAMESPACE, + local_path.name, + "remote", + name, + method="DELETE", + ) + + # Then + command = ["git", "remote", "remove", name] + mock_execute.assert_called_once_with(command, cwd=str(local_path)) + + assert response.code == 204 From 94f3f10ea15afe728690f6ee4b4445d5e465f924 Mon Sep 17 00:00:00 2001 From: boscochw_ubuntU_ideapad Date: Wed, 3 Aug 2022 10:35:54 -0700 Subject: [PATCH 23/29] Remove cancel button from add remote dialog --- src/components/AddRemoteDialogue.tsx | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/components/AddRemoteDialogue.tsx b/src/components/AddRemoteDialogue.tsx index d09a56ef6..1096b78e1 100644 --- a/src/components/AddRemoteDialogue.tsx +++ b/src/components/AddRemoteDialogue.tsx @@ -17,7 +17,6 @@ import { TranslationBundle } from '@jupyterlab/translation'; import { classes } from 'typestyle'; import { buttonClass, - cancelButtonClass, closeButtonClass, contentWrapperClass, createButtonClass, @@ -138,17 +137,6 @@ export class AddRemoteDialogue extends React.Component< )} - { - this.props.onClose(); - }} - /> Date: Wed, 3 Aug 2022 12:16:42 -0700 Subject: [PATCH 24/29] Change command gitAddRemote to gitManageRemote Disable arguments for the command --- schema/plugin.json | 2 +- src/commandsAndMenu.tsx | 55 +++++++++++++++++++---------------------- src/tokens.ts | 2 +- 3 files changed, 28 insertions(+), 31 deletions(-) diff --git a/schema/plugin.json b/schema/plugin.json index 6339889a7..d88271822 100644 --- a/schema/plugin.json +++ b/schema/plugin.json @@ -113,7 +113,7 @@ "command": "git:reset-to-remote" }, { - "command": "git:add-remote" + "command": "git:manage-remote" }, { "command": "git:terminal-command" diff --git a/src/commandsAndMenu.tsx b/src/commandsAndMenu.tsx index a392fa8cd..ef8f24a1d 100644 --- a/src/commandsAndMenu.tsx +++ b/src/commandsAndMenu.tsx @@ -254,7 +254,7 @@ export function addCommands( }); /** Command to add a remote Git repository */ - commands.addCommand(CommandIDs.gitAddRemote, { + commands.addCommand(CommandIDs.gitManageRemote, { label: trans.__('Manage Remote Repositories'), caption: trans.__('Manage Remote Repositories'), isEnabled: () => gitModel.pathRepository !== null, @@ -265,38 +265,35 @@ export function addCommands( ); return; } - let url = args['url'] as string; - let name = args['name'] as string; - if (!url) { - const widgetId = 'git-dialog-AddRemote'; - let anchor = document.querySelector(`#${widgetId}`); - if (!anchor) { - anchor = document.createElement('div'); - anchor.id = widgetId; - document.body.appendChild(anchor); - } + const widgetId = 'git-dialog-AddRemote'; + let anchor = document.querySelector(`#${widgetId}`); + if (!anchor) { + anchor = document.createElement('div'); + anchor.id = widgetId; + document.body.appendChild(anchor); + } - const waitForDialog = new PromiseDelegate(); - const dialog = ReactWidget.create( - { - dialog.dispose(); - waitForDialog.resolve(remote ?? null); - }} - /> - ); + const waitForDialog = new PromiseDelegate(); + const dialog = ReactWidget.create( + { + dialog.dispose(); + waitForDialog.resolve(remote ?? null); + }} + /> + ); - Widget.attach(dialog, anchor); + Widget.attach(dialog, anchor); - const remote = await waitForDialog.promise; + const remote = await waitForDialog.promise; - if (remote) { - name = remote.name; - url = remote.url; - } + let name, url; + if (remote) { + name = remote.name; + url = remote.url; } if (url) { @@ -1313,7 +1310,7 @@ export function createGitMenu( CommandIDs.gitPush, CommandIDs.gitPull, CommandIDs.gitResetToRemote, - CommandIDs.gitAddRemote, + CommandIDs.gitManageRemote, CommandIDs.gitTerminalCommand ].forEach(command => { menu.addItem({ command }); diff --git a/src/tokens.ts b/src/tokens.ts index 0ca1db5b9..b57a2f775 100644 --- a/src/tokens.ts +++ b/src/tokens.ts @@ -1189,7 +1189,7 @@ export enum CommandIDs { gitOpenUrl = 'git:open-url', gitToggleSimpleStaging = 'git:toggle-simple-staging', gitToggleDoubleClickDiff = 'git:toggle-double-click-diff', - gitAddRemote = 'git:add-remote', + gitManageRemote = 'git:manage-remote', gitClone = 'git:clone', gitMerge = 'git:merge', gitOpenGitignore = 'git:open-gitignore', From 9f3ee728f067d1328e855a22c36fd2ca608de955 Mon Sep 17 00:00:00 2001 From: boscochw_ubuntU_ideapad Date: Wed, 3 Aug 2022 12:28:48 -0700 Subject: [PATCH 25/29] Rename files to reflect command name 'ManageRemote' --- src/commandsAndMenu.tsx | 6 +++--- ...RemoteDialogue.tsx => ManageRemoteDialogue.tsx} | 14 +++++++------- .../{AddRemoteDialog.ts => ManageRemoteDialog.ts} | 0 3 files changed, 10 insertions(+), 10 deletions(-) rename src/components/{AddRemoteDialogue.tsx => ManageRemoteDialogue.tsx} (94%) rename src/style/{AddRemoteDialog.ts => ManageRemoteDialog.ts} (100%) diff --git a/src/commandsAndMenu.tsx b/src/commandsAndMenu.tsx index ef8f24a1d..e06af3577 100644 --- a/src/commandsAndMenu.tsx +++ b/src/commandsAndMenu.tsx @@ -46,7 +46,7 @@ import { } from './tokens'; import { GitCredentialsForm } from './widgets/CredentialsBox'; import { discardAllChanges } from './widgets/discardAllChanges'; -import { AddRemoteDialogue } from './components/AddRemoteDialogue'; +import { ManageRemoteDialogue } from './components/ManageRemoteDialogue'; import { CheckboxForm } from './widgets/GitResetToRemoteForm'; import { AdvancedPushForm } from './widgets/AdvancedPushForm'; @@ -266,7 +266,7 @@ export function addCommands( return; } - const widgetId = 'git-dialog-AddRemote'; + const widgetId = 'git-dialog-ManageRemote'; let anchor = document.querySelector(`#${widgetId}`); if (!anchor) { anchor = document.createElement('div'); @@ -276,7 +276,7 @@ export function addCommands( const waitForDialog = new PromiseDelegate(); const dialog = ReactWidget.create( - { diff --git a/src/components/AddRemoteDialogue.tsx b/src/components/ManageRemoteDialogue.tsx similarity index 94% rename from src/components/AddRemoteDialogue.tsx rename to src/components/ManageRemoteDialogue.tsx index 1096b78e1..a0fe16e17 100644 --- a/src/components/AddRemoteDialogue.tsx +++ b/src/components/ManageRemoteDialogue.tsx @@ -11,7 +11,7 @@ import { existingRemoteWrapperClass, existingRemoteGridClass, actionsWrapperClass -} from '../style/AddRemoteDialog'; +} from '../style/ManageRemoteDialog'; import { TranslationBundle } from '@jupyterlab/translation'; import { classes } from 'typestyle'; @@ -26,7 +26,7 @@ import { import { trashIcon } from '../style/icons'; -export interface IAddRemoteDialogueProps { +export interface IManageRemoteDialogueProps { /** * The application language translator. */ @@ -45,7 +45,7 @@ export interface IAddRemoteDialogueProps { onClose: (remote?: Git.IGitRemote) => void; } -export interface IAddRemoteDialogueState { +export interface IManageRemoteDialogueState { /** * New remote name and url pair */ @@ -56,11 +56,11 @@ export interface IAddRemoteDialogueState { existingRemotes: Git.IGitRemote[] | null; } -export class AddRemoteDialogue extends React.Component< - IAddRemoteDialogueProps, - IAddRemoteDialogueState +export class ManageRemoteDialogue extends React.Component< + IManageRemoteDialogueProps, + IManageRemoteDialogueState > { - constructor(props: IAddRemoteDialogueProps) { + constructor(props: IManageRemoteDialogueProps) { super(props); this.state = { newRemote: { diff --git a/src/style/AddRemoteDialog.ts b/src/style/ManageRemoteDialog.ts similarity index 100% rename from src/style/AddRemoteDialog.ts rename to src/style/ManageRemoteDialog.ts From ecff43a141798ea5de04358bb8ceaf0b940a8565 Mon Sep 17 00:00:00 2001 From: boscochw_ubuntU_ideapad Date: Wed, 3 Aug 2022 14:26:01 -0700 Subject: [PATCH 26/29] Refactor manageRemote command to let the dialog handle the adding and removing of remtoes --- src/commandsAndMenu.tsx | 28 +------------ src/components/ManageRemoteDialogue.tsx | 55 ++++++++++++++++++++----- 2 files changed, 46 insertions(+), 37 deletions(-) diff --git a/src/commandsAndMenu.tsx b/src/commandsAndMenu.tsx index e06af3577..a9ff31925 100644 --- a/src/commandsAndMenu.tsx +++ b/src/commandsAndMenu.tsx @@ -258,7 +258,7 @@ export function addCommands( label: trans.__('Manage Remote Repositories'), caption: trans.__('Manage Remote Repositories'), isEnabled: () => gitModel.pathRepository !== null, - execute: async args => { + execute: () => { if (gitModel.pathRepository === null) { console.warn( trans.__('Not in a Git repository. Unable to add a remote.') @@ -274,39 +274,15 @@ export function addCommands( document.body.appendChild(anchor); } - const waitForDialog = new PromiseDelegate(); const dialog = ReactWidget.create( { - dialog.dispose(); - waitForDialog.resolve(remote ?? null); - }} + onClose={() => dialog.dispose()} /> ); Widget.attach(dialog, anchor); - - const remote = await waitForDialog.promise; - - let name, url; - if (remote) { - name = remote.name; - url = remote.url; - } - - if (url) { - try { - await gitModel.addRemote(url, name); - } catch (error) { - console.error(error); - showErrorMessage( - trans.__('Error when adding remote repository'), - error - ); - } - } } }); diff --git a/src/components/ManageRemoteDialogue.tsx b/src/components/ManageRemoteDialogue.tsx index a0fe16e17..6caa7fcb7 100644 --- a/src/components/ManageRemoteDialogue.tsx +++ b/src/components/ManageRemoteDialogue.tsx @@ -1,10 +1,13 @@ import * as React from 'react'; -import { ActionButton } from './ActionButton'; import ClearIcon from '@material-ui/icons/Clear'; import Dialog from '@material-ui/core/Dialog'; import DialogActions from '@material-ui/core/DialogActions'; +import { TranslationBundle } from '@jupyterlab/translation'; +import { showErrorMessage } from '@jupyterlab/apputils'; +import { ActionButton } from './ActionButton'; import { Git } from '../tokens'; import { GitExtension } from '../model'; +import { classes } from 'typestyle'; import { remoteDialogClass, remoteDialogInputClass, @@ -12,9 +15,6 @@ import { existingRemoteGridClass, actionsWrapperClass } from '../style/ManageRemoteDialog'; -import { TranslationBundle } from '@jupyterlab/translation'; - -import { classes } from 'typestyle'; import { buttonClass, closeButtonClass, @@ -23,7 +23,6 @@ import { titleClass, titleWrapperClass } from '../style/NewBranchDialog'; - import { trashIcon } from '../style/icons'; export interface IManageRemoteDialogueProps { @@ -42,7 +41,7 @@ export interface IManageRemoteDialogueProps { /** * Callback to handle the closing of dialogue */ - onClose: (remote?: Git.IGitRemote) => void; + onClose: () => void; } export interface IManageRemoteDialogueState { @@ -91,9 +90,7 @@ export class ManageRemoteDialogue extends React.Component< { - this.props.onClose(); - }} + onClick={() => this.props.onClose()} /> @@ -105,6 +102,9 @@ export class ManageRemoteDialogue extends React.Component< )} { + this._nameInput = node; + }} type="text" placeholder={this.props.trans.__('name')} onChange={event => @@ -117,6 +117,9 @@ export class ManageRemoteDialogue extends React.Component< } /> { + this._urlInput = node; + }} type="text" placeholder={this.props.trans.__('Remote Git repository URL')} onChange={event => @@ -127,6 +130,11 @@ export class ManageRemoteDialogue extends React.Component< } }) } + onKeyPress={e => { + if (e.key === 'Enter') { + this._addRemoteButton.click(); + } + }} /> @@ -138,12 +146,33 @@ export class ManageRemoteDialogue extends React.Component< { + this._addRemoteButton = btn; + }} className={classes(buttonClass, createButtonClass)} type="button" title={this.props.trans.__('Add Remote')} value={this.props.trans.__('Add')} - onClick={() => { - this.props.onClose(this.state.newRemote); + onClick={async () => { + const { name, url } = this.state.newRemote; + try { + await this.props.model.addRemote(url, name); + this._nameInput.value = ''; + this._urlInput.value = ''; + this.setState(prevState => ({ + existingRemotes: [ + ...prevState.existingRemotes, + prevState.newRemote + ], + newRemote: { name: '', url: '' } + })); + } catch (error) { + console.error(error); + showErrorMessage( + this.props.trans.__('Error when adding remote repository'), + error + ); + } }} disabled={!this.state.newRemote.name || !this.state.newRemote.url} /> @@ -183,4 +212,8 @@ export class ManageRemoteDialogue extends React.Component< ); } + + private _nameInput: HTMLInputElement; + private _urlInput: HTMLInputElement; + private _addRemoteButton: HTMLInputElement; } From 1618609b79cd28710a7480d480948d603ef65769 Mon Sep 17 00:00:00 2001 From: boscochw_ubuntU_ideapad Date: Wed, 3 Aug 2022 15:59:24 -0700 Subject: [PATCH 27/29] Comment out tests for addRemote command --- tests/commands.spec.tsx | 108 ++++++++++++++++++++-------------------- 1 file changed, 54 insertions(+), 54 deletions(-) diff --git a/tests/commands.spec.tsx b/tests/commands.spec.tsx index 983d0b3a1..ef94ee13f 100644 --- a/tests/commands.spec.tsx +++ b/tests/commands.spec.tsx @@ -58,60 +58,60 @@ describe('git-commands', () => { ); }); - describe('git:add-remote', () => { - it('should admit user and name arguments', async () => { - const name = 'ref'; - const url = 'https://www.mygitserver.com/me/myrepo.git'; - const path = DEFAULT_REPOSITORY_PATH; - - mockGit.requestAPI.mockImplementation( - mockedRequestAPI({ - ...mockResponses, - 'remote/add': { - body: () => { - return { code: 0, command: `git remote add ${name} ${url}` }; - } - } - }) - ); - - model.pathRepository = path; - await model.ready; - - await commands.execute(CommandIDs.gitAddRemote, { url, name }); - - expect(mockGit.requestAPI).toBeCalledWith(`${path}/remote/add`, 'POST', { - url, - name - }); - }); - - it('has optional argument name', async () => { - const name = 'origin'; - const url = 'https://www.mygitserver.com/me/myrepo.git'; - const path = DEFAULT_REPOSITORY_PATH; - - mockGit.requestAPI.mockImplementation( - mockedRequestAPI({ - ...mockResponses, - 'remote/add': { - body: () => { - return { code: 0, command: `git remote add ${name} ${url}` }; - } - } - }) - ); - - model.pathRepository = path; - await model.ready; - - await commands.execute(CommandIDs.gitAddRemote, { url }); - - expect(mockGit.requestAPI).toBeCalledWith(`${path}/remote/add`, 'POST', { - url - }); - }); - }); + // describe('git:manage-remote', () => { + // it('should admit user and name arguments', async () => { + // const name = 'ref'; + // const url = 'https://www.mygitserver.com/me/myrepo.git'; + // const path = DEFAULT_REPOSITORY_PATH; + + // mockGit.requestAPI.mockImplementation( + // mockedRequestAPI({ + // ...mockResponses, + // 'remote/add': { + // body: () => { + // return { code: 0, command: `git remote add ${name} ${url}` }; + // } + // } + // }) + // ); + + // model.pathRepository = path; + // await model.ready; + + // await commands.execute(CommandIDs.gitManageRemote, { url, name }); + + // expect(mockGit.requestAPI).toBeCalledWith(`${path}/remote/add`, 'POST', { + // url, + // name + // }); + // }); + + // it('has optional argument name', async () => { + // const name = 'origin'; + // const url = 'https://www.mygitserver.com/me/myrepo.git'; + // const path = DEFAULT_REPOSITORY_PATH; + + // mockGit.requestAPI.mockImplementation( + // mockedRequestAPI({ + // ...mockResponses, + // 'remote/add': { + // body: () => { + // return { code: 0, command: `git remote add ${name} ${url}` }; + // } + // } + // }) + // ); + + // model.pathRepository = path; + // await model.ready; + + // await commands.execute(CommandIDs.gitManageRemote); + + // expect(mockGit.requestAPI).toBeCalledWith(`${path}/remote/add`, 'POST', { + // url + // }); + // }); + // }); describe('git:context-discard', () => { ['staged', 'partially-staged', 'unstaged', 'untracked'].forEach(status => { From 9c686a42352ec012b6e3209404d6fce674ecd0ab Mon Sep 17 00:00:00 2001 From: boscochw_ubuntU_ideapad Date: Fri, 5 Aug 2022 13:18:36 -0700 Subject: [PATCH 28/29] Remove test for git:add-remote command --- tests/commands.spec.tsx | 55 ----------------------------------------- 1 file changed, 55 deletions(-) diff --git a/tests/commands.spec.tsx b/tests/commands.spec.tsx index ef94ee13f..50df186ba 100644 --- a/tests/commands.spec.tsx +++ b/tests/commands.spec.tsx @@ -58,61 +58,6 @@ describe('git-commands', () => { ); }); - // describe('git:manage-remote', () => { - // it('should admit user and name arguments', async () => { - // const name = 'ref'; - // const url = 'https://www.mygitserver.com/me/myrepo.git'; - // const path = DEFAULT_REPOSITORY_PATH; - - // mockGit.requestAPI.mockImplementation( - // mockedRequestAPI({ - // ...mockResponses, - // 'remote/add': { - // body: () => { - // return { code: 0, command: `git remote add ${name} ${url}` }; - // } - // } - // }) - // ); - - // model.pathRepository = path; - // await model.ready; - - // await commands.execute(CommandIDs.gitManageRemote, { url, name }); - - // expect(mockGit.requestAPI).toBeCalledWith(`${path}/remote/add`, 'POST', { - // url, - // name - // }); - // }); - - // it('has optional argument name', async () => { - // const name = 'origin'; - // const url = 'https://www.mygitserver.com/me/myrepo.git'; - // const path = DEFAULT_REPOSITORY_PATH; - - // mockGit.requestAPI.mockImplementation( - // mockedRequestAPI({ - // ...mockResponses, - // 'remote/add': { - // body: () => { - // return { code: 0, command: `git remote add ${name} ${url}` }; - // } - // } - // }) - // ); - - // model.pathRepository = path; - // await model.ready; - - // await commands.execute(CommandIDs.gitManageRemote); - - // expect(mockGit.requestAPI).toBeCalledWith(`${path}/remote/add`, 'POST', { - // url - // }); - // }); - // }); - describe('git:context-discard', () => { ['staged', 'partially-staged', 'unstaged', 'untracked'].forEach(status => { [' ', 'M', 'A'].forEach(x => { From c799be3b7ded4d46235a448e15dc3c6dbc65ae61 Mon Sep 17 00:00:00 2001 From: boscochw_ubuntU_ideapad Date: Mon, 8 Aug 2022 18:02:45 -0700 Subject: [PATCH 29/29] Add tests for component 'ManageRemoteDialogue' --- src/components/ManageRemoteDialogue.tsx | 12 +- .../ManageRemoteDialogue.spec.tsx | 193 ++++++++++++++++++ 2 files changed, 201 insertions(+), 4 deletions(-) create mode 100644 tests/test-components/ManageRemoteDialogue.spec.tsx diff --git a/src/components/ManageRemoteDialogue.tsx b/src/components/ManageRemoteDialogue.tsx index 6caa7fcb7..c3eef4348 100644 --- a/src/components/ManageRemoteDialogue.tsx +++ b/src/components/ManageRemoteDialogue.tsx @@ -71,8 +71,12 @@ export class ManageRemoteDialogue extends React.Component< } async componentDidMount(): Promise { - const remotes = await this.props.model.getRemotes(); - this.setState({ existingRemotes: remotes }); + try { + const remotes = await this.props.model.getRemotes(); + this.setState({ existingRemotes: remotes }); + } catch (err) { + console.error(err); + } } render(): JSX.Element { @@ -186,7 +190,7 @@ export class ManageRemoteDialogue extends React.Component< ) : this.state.existingRemotes.length > 0 ? (
{this.state.existingRemotes.map((remote, index) => ( - <> + {remote.name} {remote.url} - + ))}
) : ( diff --git a/tests/test-components/ManageRemoteDialogue.spec.tsx b/tests/test-components/ManageRemoteDialogue.spec.tsx new file mode 100644 index 000000000..8f15923a4 --- /dev/null +++ b/tests/test-components/ManageRemoteDialogue.spec.tsx @@ -0,0 +1,193 @@ +// @ts-nocheck +import { shallow, mount } from 'enzyme'; +import 'jest'; +import * as React from 'react'; +import { ActionButton } from '../../src/components/ActionButton'; +import { + ManageRemoteDialogue, + IManageRemoteDialogueProps +} from '../../src/components/ManageRemoteDialogue'; +import * as git from '../../src/git'; +import { GitExtension } from '../../src/model'; +import { createButtonClass } from '../../src/style/NewBranchDialog'; +import { + mockedRequestAPI, + defaultMockedResponses, + DEFAULT_REPOSITORY_PATH +} from '../utils'; +import ClearIcon from '@material-ui/icons/Clear'; +import { nullTranslator } from '@jupyterlab/translation'; + +jest.mock('../../src/git'); +jest.mock('@jupyterlab/apputils'); + +const REMOTES = [ + { + name: 'test', + url: 'https://test.com' + }, + { + name: 'origin', + url: 'https://origin.com' + } +]; + +async function createModel() { + const model = new GitExtension(); + model.pathRepository = DEFAULT_REPOSITORY_PATH; + + await model.ready; + return model; +} + +describe('ManageRemoteDialogue', () => { + let model: GitExtension; + const trans = nullTranslator.load('jupyterlab_git'); + + beforeEach(async () => { + jest.restoreAllMocks(); + + const mock = git as jest.Mocked; + mock.requestAPI.mockImplementation( + mockedRequestAPI({ + responses: { + ...defaultMockedResponses, + 'remote/add': { + body: () => { + return { code: 0 }; + } + }, + 'remote/show': { + body: () => { + return { code: 0, remotes: REMOTES }; + } + } + } + }) + ); + + model = await createModel(); + }); + + function createProps( + props?: Partial + ): IManageRemoteDialogueProps { + return { + model: model, + trans: trans, + onClose: () => null, + ...props + }; + } + + describe('constructor', () => { + it('should return a new instance with initial state', () => { + const remoteDialogue = shallow( + + ); + expect(remoteDialogue.instance()).toBeInstanceOf(ManageRemoteDialogue); + const initialState = { + newRemote: { + name: '', + url: '' + }, + existingRemotes: null + }; + expect(remoteDialogue.state()).toEqual(initialState); + }); + + it('should set the correct state after mounting', async () => { + const spyGitGetRemotes = jest.spyOn(GitExtension.prototype, 'getRemotes'); + const spyComponentDidMount = jest.spyOn( + ManageRemoteDialogue.prototype, + 'componentDidMount' + ); + const remoteDialogue = shallow( + + ); + await remoteDialogue.instance().componentDidMount(); + expect(remoteDialogue.state()).toEqual({ + newRemote: { + name: '', + url: '' + }, + existingRemotes: REMOTES + }); + expect(spyGitGetRemotes).toHaveBeenCalledTimes(2); + expect(spyComponentDidMount).toHaveBeenCalledTimes(2); + }); + }); + + describe('render', () => { + it('should display a title for the dialogue "Manage Remotes"', () => { + const remoteDialogue = shallow( + + ); + const node = remoteDialogue.find('p').first(); + expect(node.text()).toEqual('Manage Remotes'); + }); + it('should display a button to close the dialogue', () => { + const remoteDialogue = shallow( + + ); + const nodes = remoteDialogue.find(ClearIcon); + expect(nodes.length).toEqual(1); + }); + + it('should display two input boxes for entering new remote name and url', () => { + const remoteDialogue = shallow( + + ); + const nameInput = remoteDialogue.find('input[placeholder="name"]'); + const urlInput = remoteDialogue.find( + 'input[placeholder="Remote Git repository URL"]' + ); + expect(nameInput.length).toEqual(1); + expect(urlInput.length).toEqual(1); + }); + + it('should display a button to add a new remote', () => { + const remoteDialogue = shallow( + + ); + const node = remoteDialogue.find(`.${createButtonClass}`).first(); + expect(node.prop('value')).toEqual('Add'); + }); + + it('should display buttons to remove existing remotes', async () => { + const remoteDialogue = shallow( + + ); + await remoteDialogue.instance().componentDidMount(); + const nodes = remoteDialogue.find(ActionButton); + expect(nodes.length).toEqual(REMOTES.length); + }); + }); + + describe('functionality', () => { + it('should add a new remote', async () => { + const remoteDialogue = shallow( + + ); + const newRemote = { + name: 'newRemote', + url: 'newremote.com' + }; + await remoteDialogue.setState({ + newRemote + }); + + const spyGitAddRemote = jest.spyOn(GitExtension.prototype, 'addRemote'); + const addRemoteButton = remoteDialogue + .find(`.${createButtonClass}`) + .first(); + addRemoteButton.simulate('click'); + + expect(spyGitAddRemote).toHaveBeenCalledTimes(1); + expect(spyGitAddRemote).toHaveBeenCalledWith( + newRemote.url, + newRemote.name + ); + }); + }); +});