Skip to content

Commit 2091f48

Browse files
BoscoCHWiflindafcollonval
authored
Support multi-selection in file lists (jupyterlab#1136)
* enable multiple selection with control-click without functionality * Implemented multiple file selection with control-click * Remove handling this.state.selectedFiles is null in FileList selectFile method * Enable toggle select/deselect files with control-click * Implement Shift-Click file selection Shift-click behaviour is not ideal. Failed to simulate shift-click behaviour on Google Drive. Issues: 1. Can't deselect when shift click direction changes Other issues: 1. IStatusFile object equality checking leads to a lot of duplicate codes 2. File status changes is not immediately reflected in FileList state, leading to invalid commands in context menu * extract file comparison logic and remove duplicate selected files due to shift click * limit multiple file selection by control-click to within the same category (git status) * rename replaceSelectedFile to selectOnlyOneFile for better comprehension * shift-click on a file with a different category (git status) becomes a simple click, handle partially staged files for shift clicks * Make FileItem action commands (open, diff and stage file) apply to selected files * Make FileItem action commands (open, diff and unstage file) apply to selected files * Make FileItem action commands (discarding changes and staging file) apply to selected files * fix bug - open correct files on clicking action button * fix bug - diff correct files on clicking action button * fix bug - unstage correct files on clicking action button * fix bug - stage correct files on clicking action button * fix bug - discard changes on correct files on clicking action button * fix FileItem test * Rename function filesAreEqual to areFilesEqual Use _isSelectedFile where appropriate * Remove selectedFileStatus from state * change _openDiffViews function to async and await the execute command * Remove unnecessary code in openContextMenu * Ensure selectFiles arrives at correct state * implement single selection callback 'setSelection' * rework _selectUntilFile to index in sub array * fix FileItem test * implement advanced shift-click behaviours * Added label to wrap around div object to allow click on whole div element to control checkbox * Fixed label placement issue * Styled filename div * Fixed styling issue by importing and applying fileLabelStyle to the appropriate label container * tried implementing shiftclick but unsuccessful: * Fixed issue with shift click when clicking in reverse order * Implement markedFile getter in gitModel * implement shift click problem: the clicked file is not selected * Fix bug for shift-click for simple staging * Clean up * remove console logs * insert in FileItemStyle.ts * added simple ui tests for ctlr and shift click on file selection for normal staging * Add skeleton code for implementing de/select all button for simple staging * Fix original ui tests * Changed file name in ui-tests README and have the select all in simple staging selecting all files * Changed name of variables and function from markAllFile to toggleAllFiles for clarity * Added the ability to deselect items using select all * Simplified code in toggleAllFiles to use state and removed redundant code. * Improve mark-all button behaviour - sync checked state of the box to reflect if all files are marked * Fix ui tests * Add ui tests for simple staging * Clear all signal connections when unmounting FileList Co-authored-by: Frédéric Collonval <[email protected]> * Delete commented code in GitPanel Co-authored-by: Frédéric Collonval <[email protected]> * Delete unused prop from interface (GitStage) Co-authored-by: Frédéric Collonval <[email protected]> * Improve consistency and fix bug * fix ui test readme * Change control-click ui test to expect exactly 2 selected items * Stop propagation when users click on action buttons on FileItem * Add doc strings for functions * Document helper functions Co-authored-by: iflinda <[email protected]> Co-authored-by: Frédéric Collonval <[email protected]>
1 parent 2eabf94 commit 2091f48

File tree

10 files changed

+746
-168
lines changed

10 files changed

+746
-168
lines changed

src/components/FileItem.tsx

Lines changed: 75 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ import * as React from 'react';
33
import { classes } from 'typestyle';
44
import { GitExtension } from '../model';
55
import {
6+
checkboxLabelContainerStyle,
7+
checkboxLabelLastContainerStyle,
8+
checkboxLabelStyle,
69
fileChangedLabelBrandStyle,
710
fileChangedLabelInfoStyle,
811
fileChangedLabelStyle,
@@ -12,6 +15,7 @@ import {
1215
selectedFileChangedLabelStyle,
1316
selectedFileStyle
1417
} from '../style/FileItemStyle';
18+
import { fileLabelStyle } from '../style/FilePathStyle';
1519
import { Git } from '../tokens';
1620
import { FilePath } from './FilePath';
1721

@@ -44,20 +48,16 @@ interface IGitMarkBoxProps {
4448
* File status
4549
*/
4650
stage: Git.Status;
51+
/**
52+
* Whether the checkbox is checked
53+
*/
54+
checked: boolean;
4755
}
4856

4957
/**
5058
* Render the selection box in simple mode
5159
*/
5260
class GitMarkBox extends React.PureComponent<IGitMarkBoxProps> {
53-
protected _onClick = (): void => {
54-
// toggle will emit a markChanged signal
55-
this.props.model.toggleMark(this.props.fname);
56-
57-
// needed if markChanged doesn't force an update of a parent
58-
this.forceUpdate();
59-
};
60-
6161
protected _onDoubleClick = (
6262
event: React.MouseEvent<HTMLInputElement>
6363
): void => {
@@ -76,8 +76,7 @@ class GitMarkBox extends React.PureComponent<IGitMarkBoxProps> {
7676
name="gitMark"
7777
className={gitMarkBoxStyle}
7878
type="checkbox"
79-
checked={this.props.model.getMark(this.props.fname)}
80-
onChange={this._onClick}
79+
checked={this.props.checked}
8180
onDoubleClick={this._onDoubleClick}
8281
/>
8382
);
@@ -117,9 +116,12 @@ export interface IFileItemProps {
117116
*/
118117
selected?: boolean;
119118
/**
120-
* Callback to select the file
119+
* Callback to select file(s)
121120
*/
122-
selectFile?: (file: Git.IStatusFile | null) => void;
121+
setSelection?: (
122+
file: Git.IStatusFile,
123+
options?: { singleton?: boolean; group?: boolean }
124+
) => void;
123125
/**
124126
* Optional style class
125127
*/
@@ -132,12 +134,40 @@ export interface IFileItemProps {
132134
* The application language translator.
133135
*/
134136
trans: TranslationBundle;
137+
/**
138+
* Callback to implement shift-click for simple staging
139+
*/
140+
markUntilFile?: (file: Git.IStatusFile) => void;
141+
/**
142+
* whether the GitMarkBox is checked
143+
*/
144+
checked?: boolean;
135145
}
136146

137147
export class FileItem extends React.PureComponent<IFileItemProps> {
138148
constructor(props: IFileItemProps) {
139149
super(props);
140150
}
151+
152+
protected _onClick = (event: React.MouseEvent<HTMLInputElement>): void => {
153+
if (this.props.markBox) {
154+
if (event.shiftKey) {
155+
this.props.markUntilFile(this.props.file);
156+
} else {
157+
this.props.model.toggleMark(this.props.file.to);
158+
this.props.setSelection(this.props.file, { singleton: true });
159+
}
160+
} else {
161+
if (event.ctrlKey || event.metaKey) {
162+
this.props.setSelection(this.props.file);
163+
} else if (event.shiftKey) {
164+
this.props.setSelection(this.props.file, { group: true });
165+
} else {
166+
this.props.setSelection(this.props.file, { singleton: true });
167+
}
168+
}
169+
};
170+
141171
protected _getFileChangedLabel(change: keyof typeof STATUS_CODES): string {
142172
return STATUS_CODES[change] || 'Unmodified';
143173
}
@@ -190,11 +220,10 @@ export class FileItem extends React.PureComponent<IFileItemProps> {
190220

191221
return (
192222
<div
223+
data-test-selected={this.props.selected}
224+
data-test-checked={this.props.checked}
193225
className={this._getFileClass()}
194-
onClick={
195-
this.props.selectFile &&
196-
(() => this.props.selectFile(this.props.file))
197-
}
226+
onClick={this._onClick}
198227
onContextMenu={
199228
this.props.contextMenu &&
200229
(event => {
@@ -205,29 +234,36 @@ export class FileItem extends React.PureComponent<IFileItemProps> {
205234
style={this.props.style}
206235
title={this.props.trans.__(`%1 • ${status}`, this.props.file.to)}
207236
>
208-
{this.props.markBox && (
209-
<GitMarkBox
210-
fname={this.props.file.to}
211-
stage={this.props.file.status}
212-
model={this.props.model}
213-
/>
214-
)}
215-
<FilePath
216-
filepath={this.props.file.to}
217-
filetype={this.props.file.type}
218-
/>
219-
{this.props.actions}
220-
<span
221-
className={this._getFileChangedLabelClass(
222-
this.props.file.status === 'unmerged' ? '!' : this.props.file.y
223-
)}
224-
>
225-
{this.props.file.status === 'unmerged'
226-
? '!'
227-
: this.props.file.y === '?'
228-
? 'U'
229-
: status_code}
230-
</span>
237+
<div className={checkboxLabelContainerStyle}>
238+
<div className={checkboxLabelStyle + ' ' + fileLabelStyle}>
239+
{this.props.markBox && (
240+
<GitMarkBox
241+
fname={this.props.file.to}
242+
stage={this.props.file.status}
243+
model={this.props.model}
244+
checked={this.props.checked}
245+
/>
246+
)}
247+
<FilePath
248+
filepath={this.props.file.to}
249+
filetype={this.props.file.type}
250+
/>
251+
</div>
252+
<div className={checkboxLabelLastContainerStyle}>
253+
{this.props.actions}
254+
<span
255+
className={this._getFileChangedLabelClass(
256+
this.props.file.status === 'unmerged' ? '!' : this.props.file.y
257+
)}
258+
>
259+
{this.props.file.status === 'unmerged'
260+
? '!'
261+
: this.props.file.y === '?'
262+
? 'U'
263+
: status_code}
264+
</span>
265+
</div>
266+
</div>
231267
</div>
232268
);
233269
}

0 commit comments

Comments
 (0)