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

Virtualized files list #767

Merged
merged 7 commits into from
Sep 26, 2020
Merged
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
3 changes: 3 additions & 0 deletions jupyterlab_git/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,9 @@ async def post(self):
selected_hash = data["selected_hash"]
current_path = data["current_path"]
result = await self.git.detailed_log(selected_hash, current_path)

if result["code"] != 0:
self.set_status(500)
self.finish(json.dumps(result))


Expand Down
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@
"react": "~16.9.0",
"react-dom": "~16.9.0",
"react-textarea-autosize": "^7.1.2",
"react-virtualized-auto-sizer": "^1.0.2",
"react-window": "^1.8.5",
"typestyle": "^2.0.1"
},
"devDependencies": {
Expand All @@ -86,6 +88,8 @@
"@types/react": "~16.8.13",
"@types/react-dom": "~16.0.5",
"@types/react-textarea-autosize": "^4.3.5",
"@types/react-virtualized-auto-sizer": "^1.0.0",
"@types/react-window": "^1.8.2",
"@typescript-eslint/eslint-plugin": "^2.25.0",
"@typescript-eslint/parser": "^2.25.0",
"all-contributors-cli": "^6.14.0",
Expand Down
54 changes: 29 additions & 25 deletions src/components/BranchMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import List from '@material-ui/core/List';
import ListItem from '@material-ui/core/ListItem';
import ClearIcon from '@material-ui/icons/Clear';
import * as React from 'react';
import { FixedSizeList, ListChildComponentProps } from 'react-window';
import { classes } from 'typestyle';
import {
activeListItemClass,
Expand All @@ -12,7 +13,6 @@ import {
filterWrapperClass,
listItemClass,
listItemIconClass,
listWrapperClass,
newBranchButtonClass,
wrapperClass
} from '../style/BranchMenu';
Expand All @@ -24,6 +24,9 @@ import { SuspendModal } from './SuspendModal';

const CHANGES_ERR_MSG =
'The current branch contains files with uncommitted changes. Please commit or discard these changes before switching to or creating another branch.';
const ITEM_HEIGHT = 24.8; // HTML element height for a single branch
const MIN_HEIGHT = 150; // Minimal HTML element height for the branches list
const MAX_HEIGHT = 400; // Maximal HTML element height for the branches list

/**
* Callback invoked upon encountering an error when switching branches.
Expand Down Expand Up @@ -237,37 +240,38 @@ export class BranchMenu extends React.Component<
* @returns React element
*/
private _renderBranchList(): React.ReactElement {
// Perform a "simple" filter... (TODO: consider implementing fuzzy filtering)
const filter = this.state.filter;
const branches = this.state.branches.filter(
branch => !filter || branch.name.includes(filter)
);
return (
<div className={listWrapperClass}>
<List disablePadding>{this._renderItems()}</List>
</div>
<FixedSizeList
height={Math.min(
Math.max(MIN_HEIGHT, branches.length * ITEM_HEIGHT),
MAX_HEIGHT
)}
itemCount={branches.length}
itemData={branches}
itemKey={(index, data) => data[index].name}
itemSize={ITEM_HEIGHT}
style={{ overflowX: 'hidden', paddingTop: 0, paddingBottom: 0 }}
width={'auto'}
>
{this._renderItem}
</FixedSizeList>
);
}

/**
* Renders menu items.
*
* @returns array of React elements
*/
private _renderItems(): React.ReactElement[] {
return this.state.branches.map(this._renderItem, this);
}

/**
* Renders a menu item.
*
* @param branch - branch
* @param idx - item index
* @param props Row properties
* @returns React element
*/
private _renderItem(
branch: Git.IBranch,
idx: number
): React.ReactElement | null {
// Perform a "simple" filter... (TODO: consider implementing fuzzy filtering)
if (this.state.filter && !branch.name.includes(this.state.filter)) {
return null;
}
private _renderItem = (props: ListChildComponentProps): JSX.Element => {
const { data, index, style } = props;
const branch = data[index] as Git.IBranch;
const isActive = branch.name === this.state.current;
return (
<ListItem
Expand All @@ -277,8 +281,8 @@ export class BranchMenu extends React.Component<
listItemClass,
isActive ? activeListItemClass : null
)}
key={branch.name}
onClick={this._onBranchClickFactory(branch.name)}
style={style}
>
<span
className={classes(
Expand All @@ -290,7 +294,7 @@ export class BranchMenu extends React.Component<
{branch.name}
</ListItem>
);
}
};

/**
* Renders a dialog for creating a new branch.
Expand Down
147 changes: 96 additions & 51 deletions src/components/FileItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,29 +25,110 @@ export const STATUS_CODES = {
'!': 'Ignored'
};

/**
* File marker properties
*/
interface IGitMarkBoxProps {
/**
* Filename
*/
fname: string;
/**
* Git repository model
*/
model: GitExtension;
/**
* File status
*/
stage: Git.Status;
}

/**
* Render the selection box in simple mode
*/
class GitMarkBox extends React.PureComponent<IGitMarkBoxProps> {
protected _onClick = (): void => {
// toggle will emit a markChanged signal
this.props.model.toggleMark(this.props.fname);

// needed if markChanged doesn't force an update of a parent
this.forceUpdate();
};

protected _onDoubleClick = (
event: React.MouseEvent<HTMLInputElement>
): void => {
event.stopPropagation();
};

render(): JSX.Element {
// idempotent, will only run once per file
this.props.model.addMark(
this.props.fname,
this.props.stage !== 'untracked'
);

return (
<input
name="gitMark"
className={gitMarkBoxStyle}
type="checkbox"
checked={this.props.model.getMark(this.props.fname)}
onChange={this._onClick}
onDoubleClick={this._onDoubleClick}
/>
);
}
}

/**
* File item properties
*/
export interface IFileItemProps {
/**
* Action buttons on the file
*/
actions?: React.ReactElement;
/**
* Callback to open a context menu on the file
*/
contextMenu?: (file: Git.IStatusFile, event: React.MouseEvent) => void;
/**
* File model
*/
file: Git.IStatusFile;
/**
* Is the file marked?
*/
markBox?: boolean;
/**
* Git repository model
*/
model: GitExtension;
/**
* Callback on double click
*/
onDoubleClick: () => void;
/**
* Is the file selected?
*/
selected?: boolean;
/**
* Callback to select the file
*/
selectFile?: (file: Git.IStatusFile | null) => void;
/**
* Inline styling for the windowing
*/
style: React.CSSProperties;
}

export interface IGitMarkBoxProps {
fname: string;
model: GitExtension;
stage: string;
}

export class FileItem extends React.Component<IFileItemProps> {
getFileChangedLabel(change: keyof typeof STATUS_CODES): string {
export class FileItem extends React.PureComponent<IFileItemProps> {
protected _getFileChangedLabel(change: keyof typeof STATUS_CODES): string {
return STATUS_CODES[change];
}

getFileChangedLabelClass(change: string) {
protected _getFileChangedLabelClass(change: string): string {
if (change === 'M') {
return this.props.selected
? classes(
Expand All @@ -67,20 +148,20 @@ export class FileItem extends React.Component<IFileItemProps> {
}
}

getFileClass() {
protected _getFileClass(): string {
return this.props.selected
? classes(fileStyle, selectedFileStyle)
: fileStyle;
}

render() {
render(): JSX.Element {
const { file } = this.props;
const status_code = file.status === 'staged' ? file.x : file.y;
const status = this.getFileChangedLabel(status_code as any);
const status = this._getFileChangedLabel(status_code as any);

return (
<li
className={this.getFileClass()}
className={this._getFileClass()}
onClick={
this.props.selectFile &&
(() => this.props.selectFile(this.props.file))
Expand All @@ -92,6 +173,7 @@ export class FileItem extends React.Component<IFileItemProps> {
})
}
onDoubleClick={this.props.onDoubleClick}
style={this.props.style}
title={`${this.props.file.to} ● ${status}`}
>
{this.props.markBox && (
Expand All @@ -106,47 +188,10 @@ export class FileItem extends React.Component<IFileItemProps> {
selected={this.props.selected}
/>
{this.props.actions}
<span className={this.getFileChangedLabelClass(this.props.file.y)}>
<span className={this._getFileChangedLabelClass(this.props.file.y)}>
{this.props.file.y === '?' ? 'U' : status_code}
</span>
</li>
);
}
}

export class GitMarkBox extends React.Component<IGitMarkBoxProps> {
constructor(props: IGitMarkBoxProps) {
super(props);
}

protected _onClick = (event: React.ChangeEvent<HTMLInputElement>) => {
// toggle will emit a markChanged signal
this.props.model.toggleMark(this.props.fname);

// needed if markChanged doesn't force an update of a parent
this.forceUpdate();
};

protected _onDoubleClick = (event: React.MouseEvent<HTMLInputElement>) => {
event.stopPropagation();
};

render() {
// idempotent, will only run once per file
this.props.model.addMark(
this.props.fname,
this.props.stage !== 'untracked'
);

return (
<input
name="gitMark"
className={gitMarkBoxStyle}
type="checkbox"
checked={this.props.model.getMark(this.props.fname)}
onChange={this._onClick}
onDoubleClick={this._onDoubleClick}
/>
);
}
}
Loading