diff --git a/package.json b/package.json index a0f15cacc..1be0bff2b 100644 --- a/package.json +++ b/package.json @@ -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": { @@ -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", diff --git a/src/components/FileList.tsx b/src/components/FileList.tsx index c65d7313a..bc6c4b011 100644 --- a/src/components/FileList.tsx +++ b/src/components/FileList.tsx @@ -457,6 +457,50 @@ export class FileList extends React.Component { private _renderStaged(files: Git.IStatusFile[]) { const doubleClickDiff = this.props.settings.get('doubleClickDiff') .composite as boolean; + + const renderStagedRow = (file: Git.IStatusFile) => { + const openFile = () => { + openListedFile(file, this.props.model); + }; + const diffButton = this._createDiffButton(file, 'INDEX'); + return ( + + + {diffButton} + { + this.resetStagedFile(file.to); + }} + /> + + } + file={file} + contextMenu={this.contextMenuStaged} + model={this.props.model} + selected={this._isSelectedFile(file)} + selectFile={this.updateSelectedFile} + onDoubleClick={ + doubleClickDiff + ? diffButton + ? () => this._openDiffView(file, 'INDEX') + : () => undefined + : openFile + } + /> + ); + }; + return ( { } collapsible heading={'Staged'} - nFiles={files.length} - > - {files.map((file: Git.IStatusFile) => { - const openFile = () => { - openListedFile(file, this.props.model); - }; - const diffButton = this._createDiffButton(file, 'INDEX'); - return ( - - - {diffButton} - { - this.resetStagedFile(file.to); - }} - /> - - } - file={file} - contextMenu={this.contextMenuStaged} - model={this.props.model} - selected={this._isSelectedFile(file)} - selectFile={this.updateSelectedFile} - onDoubleClick={ - doubleClickDiff - ? diffButton - ? () => this._openDiffView(file, 'INDEX') - : () => undefined - : openFile - } - /> - ); - })} - + files={files} + rowRenderer={renderStagedRow} + /> ); } @@ -522,6 +524,57 @@ export class FileList extends React.Component { const doubleClickDiff = this.props.settings.get('doubleClickDiff') .composite as boolean; const disabled = files.length === 0; + + const renderChangedRow = (file: Git.IStatusFile) => { + const openFile = () => { + openListedFile(file, this.props.model); + }; + const diffButton = this._createDiffButton(file, 'WORKING'); + return ( + + + {diffButton} + { + this.discardChanges(file); + }} + /> + { + this.addFile(file.to); + }} + /> + + } + file={file} + contextMenu={this.contextMenuUnstaged} + model={this.props.model} + selected={this._isSelectedFile(file)} + selectFile={this.updateSelectedFile} + onDoubleClick={ + doubleClickDiff + ? diffButton + ? () => this._openDiffView(file, 'WORKING') + : () => undefined + : openFile + } + /> + ); + }; return ( { } collapsible heading={'Changed'} - nFiles={files.length} - > - {files.map((file: Git.IStatusFile) => { - const openFile = () => { - openListedFile(file, this.props.model); - }; - const diffButton = this._createDiffButton(file, 'WORKING'); - return ( - - - {diffButton} - { - this.discardChanges(file); - }} - /> - { - this.addFile(file.to); - }} - /> - - } - file={file} - contextMenu={this.contextMenuUnstaged} - model={this.props.model} - selected={this._isSelectedFile(file)} - selectFile={this.updateSelectedFile} - onDoubleClick={ - doubleClickDiff - ? diffButton - ? () => this._openDiffView(file, 'WORKING') - : () => undefined - : openFile - } - /> - ); - })} - + files={files} + rowRenderer={renderChangedRow} + /> ); } private _renderUntracked(files: Git.IStatusFile[]) { const doubleClickDiff = this.props.settings.get('doubleClickDiff') .composite as boolean; + + const renderUntrackedRow = (file: Git.IStatusFile) => { + return ( + + { + openListedFile(file, this.props.model); + }} + /> + { + this.addFile(file.to); + }} + /> + + } + file={file} + contextMenu={this.contextMenuUntracked} + model={this.props.model} + onDoubleClick={() => { + if (!doubleClickDiff) { + openListedFile(file, this.props.model); + } + }} + selected={this._isSelectedFile(file)} + selectFile={this.updateSelectedFile} + /> + ); + }; + return ( { } collapsible heading={'Untracked'} - nFiles={files.length} - > - {files.map((file: Git.IStatusFile) => { - return ( - - { - openListedFile(file, this.props.model); - }} - /> - { - this.addFile(file.to); - }} - /> - - } - file={file} - contextMenu={this.contextMenuUntracked} - model={this.props.model} - onDoubleClick={() => { - if (!doubleClickDiff) { - openListedFile(file, this.props.model); - } - }} - selected={this._isSelectedFile(file)} - selectFile={this.updateSelectedFile} - /> - ); - })} - + files={files} + rowRenderer={renderUntrackedRow} + /> ); } private _renderSimpleStage(files: Git.IStatusFile[]) { const doubleClickDiff = this.props.settings.get('doubleClickDiff') .composite as boolean; + + const renderSimpleStageRow = (file: Git.IStatusFile) => { + const openFile = () => { + openListedFile(file, this.props.model); + }; + + // Default value for actions and double click + let actions: JSX.Element = ( + + ); + let onDoubleClick = doubleClickDiff ? (): void => undefined : openFile; + + let contextMenu = this.contextMenuSimpleUntracked; + + if (file.status === 'unstaged' || file.status === 'partially-staged') { + const diffButton = this._createDiffButton(file, 'WORKING'); + actions = ( + + + {diffButton} + { + this.discardChanges(file); + }} + /> + + ); + onDoubleClick = doubleClickDiff + ? diffButton + ? () => this._openDiffView(file, 'WORKING') + : () => undefined + : openFile; + contextMenu = this.contextMenuSimpleTracked; + } else if (file.status === 'staged') { + const diffButton = this._createDiffButton(file, 'INDEX'); + actions = ( + + + {diffButton} + { + this.discardChanges(file); + }} + /> + + ); + onDoubleClick = doubleClickDiff + ? diffButton + ? () => this._openDiffView(file, 'INDEX') + : () => undefined + : openFile; + contextMenu = this.contextMenuSimpleTracked; + } + + return ( + + ); + }; + return ( { /> } heading={'Changed'} - nFiles={files.length} - > - {files.map((file: Git.IStatusFile) => { - const openFile = () => { - openListedFile(file, this.props.model); - }; - - // Default value for actions and double click - let actions: JSX.Element = ( - - ); - let onDoubleClick = doubleClickDiff - ? (): void => undefined - : openFile; - - let contextMenu = this.contextMenuSimpleUntracked; - - if ( - file.status === 'unstaged' || - file.status === 'partially-staged' - ) { - const diffButton = this._createDiffButton(file, 'WORKING'); - actions = ( - - - {diffButton} - { - this.discardChanges(file); - }} - /> - - ); - onDoubleClick = doubleClickDiff - ? diffButton - ? () => this._openDiffView(file, 'WORKING') - : () => undefined - : openFile; - contextMenu = this.contextMenuSimpleTracked; - } else if (file.status === 'staged') { - const diffButton = this._createDiffButton(file, 'INDEX'); - actions = ( - - - {diffButton} - { - this.discardChanges(file); - }} - /> - - ); - onDoubleClick = doubleClickDiff - ? diffButton - ? () => this._openDiffView(file, 'INDEX') - : () => undefined - : openFile; - contextMenu = this.contextMenuSimpleTracked; - } - - return ( - - ); - })} - + files={files} + rowRenderer={renderSimpleStageRow} + /> ); } diff --git a/src/components/GitStage.tsx b/src/components/GitStage.tsx index 2daaff627..31d9cde94 100644 --- a/src/components/GitStage.tsx +++ b/src/components/GitStage.tsx @@ -1,5 +1,7 @@ import { caretDownIcon, caretRightIcon } from '@jupyterlab/ui-components'; import * as React from 'react'; +import AutoSizer from 'react-virtualized-auto-sizer'; +import { FixedSizeList } from 'react-window'; import { changeStageButtonStyle, sectionAreaStyle, @@ -7,6 +9,7 @@ import { sectionHeaderLabelStyle, sectionHeaderSizeStyle } from '../style/GitStageStyle'; +import { Git } from '../tokens'; /** * Git stage component properties @@ -25,9 +28,13 @@ export interface IGitStageProps { */ heading: string; /** - * Number of files in the group + * Files in the group */ - nFiles: number; + files: Git.IStatusFile[]; + /** + * Row renderer + */ + rowRenderer: (file: Git.IStatusFile) => JSX.Element; } /** @@ -38,9 +45,10 @@ export interface IGitStageState { } export const GitStage: React.FunctionComponent = ( - props: React.PropsWithChildren + props: IGitStageProps ) => { const [showFiles, setShowFiles] = React.useState(true); + const nFiles = props.files.length; return (
@@ -49,12 +57,12 @@ export const GitStage: React.FunctionComponent = (
{showFiles && ( -
    {props.children}
+ + {({ height, width }) => ( + + {({ index, style }) => { + const file = props.files[index]; + return ( +
+ {props.rowRenderer(file)} +
+ ); + }} +
+ //
    + )} +
    )} ); diff --git a/yarn.lock b/yarn.lock index 8a1c722b1..baa93f9c8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1822,6 +1822,20 @@ dependencies: "@types/react" "*" +"@types/react-virtualized-auto-sizer@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@types/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.0.tgz#fc32f30a8dab527b5816f3a757e1e1d040c8f272" + integrity sha512-NMErdIdSnm2j/7IqMteRiRvRulpjoELnXWUwdbucYCz84xG9PHcoOrr7QfXwB/ku7wd6egiKFrzt/+QK4Imeeg== + dependencies: + "@types/react" "*" + +"@types/react-window@^1.8.2": + version "1.8.2" + resolved "https://registry.yarnpkg.com/@types/react-window/-/react-window-1.8.2.tgz#a5a6b2762ce73ffaab7911ee1397cf645f2459fe" + integrity sha512-gP1xam68Wc4ZTAee++zx6pTdDAH08rAkQrWm4B4F/y6hhmlT9Mgx2q8lTCXnrPHXsr15XjRN9+K2DLKcz44qEQ== + dependencies: + "@types/react" "*" + "@types/react@*", "@types/react@~16.8.13", "@types/react@~16.8.4", "@types/react@~16.9.16": version "16.8.25" resolved "https://registry.yarnpkg.com/@types/react/-/react-16.8.25.tgz#0247613ab58b1b11ba10fed662e1947c5f2bb89c" @@ -5150,6 +5164,11 @@ matcher@^1.0.0: dependencies: escape-string-regexp "^1.0.4" +"memoize-one@>=3.1.1 <6": + version "5.1.1" + resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.1.1.tgz#047b6e3199b508eaec03504de71229b8eb1d75c0" + integrity sha512-HKeeBpWvqiVJD57ZUAsJNm71eHTykffzcLZVYWiVfQeI1rJtuEaS7hQiEpWfVVk18donPwJEcFKIkCmPJNOhHA== + merge-stream@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" @@ -6040,6 +6059,19 @@ react-transition-group@^4.4.0: loose-envify "^1.4.0" prop-types "^15.6.2" +react-virtualized-auto-sizer@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.2.tgz#a61dd4f756458bbf63bd895a92379f9b70f803bd" + integrity sha512-MYXhTY1BZpdJFjUovvYHVBmkq79szK/k7V3MO+36gJkWGkrXKtyr4vCPtpphaTLRAdDNoYEYFZWE8LjN+PIHNg== + +react-window@^1.8.5: + version "1.8.5" + resolved "https://registry.yarnpkg.com/react-window/-/react-window-1.8.5.tgz#a56b39307e79979721021f5d06a67742ecca52d1" + integrity sha512-HeTwlNa37AFa8MDZFZOKcNEkuF2YflA0hpGPiTT9vR7OawEt+GZbfM6wqkBahD3D3pUjIabQYzsnY/BSJbgq6Q== + dependencies: + "@babel/runtime" "^7.0.0" + memoize-one ">=3.1.1 <6" + react@~16.9.0: version "16.9.0" resolved "https://registry.yarnpkg.com/react/-/react-16.9.0.tgz#40ba2f9af13bc1a38d75dbf2f4359a5185c4f7aa"