Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pin repository path #515

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 6 additions & 21 deletions src/components/GitPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,19 @@
import * as React from 'react';
import { showErrorMessage, showDialog } from '@jupyterlab/apputils';
import { ISettingRegistry } from '@jupyterlab/coreutils';
import { FileBrowserModel } from '@jupyterlab/filebrowser';
import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
import { JSONObject } from '@phosphor/coreutils';
import { GitExtension } from '../model';
import {
findRepoButtonStyle,
panelContainerStyle,
panelWarningStyle
} from '../style/GitPanelStyle';
import { panelContainerStyle } from '../style/GitPanelStyle';
import { Git } from '../tokens';
import { decodeStage } from '../utils';
import { GitAuthorForm } from '../widgets/AuthorBox';
import { BranchHeader } from './BranchHeader';
import { CommitBox } from './CommitBox';
import { FileList } from './FileList';
import { HistorySideBar } from './HistorySideBar';
import { PathHeader } from './PathHeader';
import { CommitBox } from './CommitBox';

/** Interface for GitPanel component state */
export interface IGitSessionNodeState {
Expand All @@ -40,6 +37,7 @@ export interface IGitSessionNodeProps {
model: GitExtension;
renderMime: IRenderMimeRegistry;
settings: ISettingRegistry.ISettings;
fileBrowserModel: FileBrowserModel;
}

/** A React component for the git extension's main display */
Expand Down Expand Up @@ -199,7 +197,7 @@ export class GitPanel extends React.Component<

render() {
let filelist: React.ReactElement;
let main: React.ReactElement;
let main: React.ReactElement = null;
let sub: React.ReactElement;
let msg: React.ReactElement;

Expand Down Expand Up @@ -270,26 +268,13 @@ export class GitPanel extends React.Component<
{sub}
</React.Fragment>
);
} else {
main = (
<div className={panelWarningStyle}>
<div>You aren’t in a git repository.</div>
<button
className={findRepoButtonStyle}
onClick={() =>
this.props.model.commands.execute('filebrowser:toggle-main')
}
>
Go find a repo
</button>
</div>
);
}

return (
<div className={panelContainerStyle}>
<PathHeader
model={this.props.model}
fileBrowserModel={this.props.fileBrowserModel}
refresh={async () => {
await this.refreshBranch();
if (this.state.isHistoryVisible) {
Expand Down
255 changes: 176 additions & 79 deletions src/components/PathHeader.tsx
Original file line number Diff line number Diff line change
@@ -1,117 +1,214 @@
import * as React from 'react';
import { Dialog, showDialog, UseSignal } from '@jupyterlab/apputils';
import { PathExt } from '@jupyterlab/coreutils';
import * as React from 'react';
import { FileBrowserModel, FileDialog } from '@jupyterlab/filebrowser';
import { DefaultIconReact } from '@jupyterlab/ui-components';
import { classes } from 'typestyle';
import {
gitPullStyle,
gitPushStyle,
noRepoPathStyle,
pinIconStyle,
repoPathStyle,
repoPinStyle,
repoRefreshStyle,
repoStyle
repoStyle,
separatorStyle,
toolBarStyle
} from '../style/PathHeaderStyle';
import { IGitExtension } from '../tokens';
import { GitCredentialsForm } from '../widgets/CredentialsBox';
import { GitPullPushDialog, Operation } from '../widgets/gitPushPull';
import { IGitExtension } from '../tokens';

/**
* Properties of the PathHeader React component
*/
export interface IPathHeaderProps {
/**
* Git extension model
*/
model: IGitExtension;
/**
* File browser model
*/
fileBrowserModel: FileBrowserModel;
/**
* Refresh UI callback
*/
refresh: () => Promise<void>;
}

export class PathHeader extends React.Component<IPathHeaderProps> {
constructor(props: IPathHeaderProps) {
super(props);
/**
* Displays the error dialog when the Git Push/Pull operation fails.
*
* @param title the title of the error dialog
* @param body the message to be shown in the body of the modal.
*/
async function showGitPushPullDialog(
model: IGitExtension,
operation: Operation
): Promise<void> {
let result = await showDialog({
title: `Git ${operation}`,
body: new GitPullPushDialog(model, operation),
buttons: [Dialog.okButton({ label: 'DISMISS' })]
});
let retry = false;
while (!result.button.accept) {
retry = true;

let response = await showDialog({
title: 'Git credentials required',
body: new GitCredentialsForm(
'Enter credentials for remote repository',
retry ? 'Incorrect username or password.' : ''
),
buttons: [Dialog.cancelButton(), Dialog.okButton({ label: 'OK' })]
});

if (response.button.accept) {
// user accepted attempt to login
result = await showDialog({
title: `Git ${operation}`,
body: new GitPullPushDialog(model, operation, response.value),
buttons: [Dialog.okButton({ label: 'DISMISS' })]
});
} else {
break;
}
}
}

render() {
return (
<div className={repoStyle}>
<UseSignal
signal={this.props.model.repositoryChanged}
initialArgs={{
name: 'pathRepository',
oldValue: null,
newValue: this.props.model.pathRepository
}}
>
{(_, change) => (
<span className={repoPathStyle} title={change.newValue}>
{PathExt.basename(change.newValue || '')}
</span>
)}
</UseSignal>
/**
* Select a Git repository folder
*
* @param model Git extension model
* @param fileModel file browser model
*/
async function selectGitRepository(
model: IGitExtension,
fileModel: FileBrowserModel
) {
const result = await FileDialog.getExistingDirectory({
iconRegistry: fileModel.iconRegistry,
manager: fileModel.manager,
title: 'Select a Git repository folder'
});

if (result.button.accept) {
const folder = result.value[0];
if (model.repositoryPinned) {
model.pathRepository = folder.path;
} else if (fileModel.path !== folder.path) {
// Change current filebrowser path
// => will be propagated to path repository
fileModel.cd(`/${folder.path}`);
}
}
}

/**
* React function component to render the toolbar and path header component
*
* @param props Properties for the path header component
*/
export const PathHeader: React.FunctionComponent<IPathHeaderProps> = (
props: IPathHeaderProps
) => {
const [pin, setPin] = React.useState(props.model.repositoryPinned);

React.useEffect(() => {
props.model.restored.then(() => {
setPin(props.model.repositoryPinned);
});
});

return (
<React.Fragment>
<div className={toolBarStyle}>
<button
className={classes(gitPullStyle, 'jp-Icon-16')}
title={'Pull latest changes'}
onClick={() =>
this.showGitPushPullDialog(this.props.model, Operation.Pull).catch(
reason => {
console.error(
`An error occurs when pulling the changes.\n${reason}`
);
}
)
showGitPushPullDialog(props.model, Operation.Pull).catch(reason => {
console.error(
`An error occurs when pulling the changes.\n${reason}`
);
})
}
/>
<button
className={classes(gitPushStyle, 'jp-Icon-16')}
title={'Push committed changes'}
onClick={() =>
this.showGitPushPullDialog(this.props.model, Operation.Push).catch(
reason => {
console.error(
`An error occurs when pulling the changes.\n${reason}`
);
}
)
showGitPushPullDialog(props.model, Operation.Push).catch(reason => {
console.error(
`An error occurs when pulling the changes.\n${reason}`
);
})
}
/>
<button
className={classes(repoRefreshStyle, 'jp-Icon-16')}
title={'Refresh the repository to detect local and remote changes'}
onClick={() => this.props.refresh()}
onClick={() => props.refresh()}
/>
</div>
);
}

/**
* Displays the error dialog when the Git Push/Pull operation fails.
* @param title the title of the error dialog
* @param body the message to be shown in the body of the modal.
*/
private async showGitPushPullDialog(
model: IGitExtension,
operation: Operation
): Promise<void> {
let result = await showDialog({
title: `Git ${operation}`,
body: new GitPullPushDialog(model, operation),
buttons: [Dialog.okButton({ label: 'DISMISS' })]
});
let retry = false;
while (!result.button.accept) {
retry = true;
<UseSignal
signal={props.model.repositoryChanged}
initialArgs={{
name: 'pathRepository',
oldValue: null,
newValue: props.model.pathRepository
}}
>
{(_, change) => {
const pathStyles = [repoPathStyle];
if (!change.newValue) {
pathStyles.push(noRepoPathStyle);
}

let response = await showDialog({
title: 'Git credentials required',
body: new GitCredentialsForm(
'Enter credentials for remote repository',
retry ? 'Incorrect username or password.' : ''
),
buttons: [Dialog.cancelButton(), Dialog.okButton({ label: 'OK' })]
});
let pinTitle = 'the repository path';
if (pin) {
pinTitle = 'Unpin ' + pinTitle;
} else {
pinTitle = 'Pin ' + pinTitle;
}

if (response.button.accept) {
// user accepted attempt to login
result = await showDialog({
title: `Git ${operation}`,
body: new GitPullPushDialog(model, operation, response.value),
buttons: [Dialog.okButton({ label: 'DISMISS' })]
});
} else {
break;
}
}
}
}
return (
<div className={repoStyle}>
<label className={repoPinStyle} title={pinTitle}>
<input
type="checkbox"
checked={pin}
onChange={() => {
props.model.repositoryPinned = !props.model
.repositoryPinned;
setPin(props.model.repositoryPinned);
}}
/>
<DefaultIconReact
className={pinIconStyle}
tag="span"
name="git-pin"
/>
</label>
<span
className={classes(...pathStyles)}
title={change.newValue}
onClick={() => {
selectGitRepository(props.model, props.fileBrowserModel);
}}
>
{change.newValue
? PathExt.basename(change.newValue)
: 'Click to select a Git repository.'}
</span>
</div>
);
}}
</UseSignal>
<div className={separatorStyle} />
</React.Fragment>
);
};
Loading