From 21b2daff3958105f37a86ee6a05eb23bab729493 Mon Sep 17 00:00:00 2001 From: david califf Date: Sun, 3 Mar 2019 23:57:08 -0600 Subject: [PATCH] Multiselect, allow users to select multiple commands with shift key --- .../src/neo/components/TestRow/index.jsx | 61 ++++++++++++++++--- .../src/neo/components/TestTable/index.jsx | 36 ++++++++++- .../src/neo/containers/Editor/index.jsx | 27 ++++---- .../selenium-ide/src/neo/models/TestCase.js | 2 + .../src/neo/stores/view/UiState.js | 58 ++++++++++++++++-- 5 files changed, 156 insertions(+), 28 deletions(-) diff --git a/packages/selenium-ide/src/neo/components/TestRow/index.jsx b/packages/selenium-ide/src/neo/components/TestRow/index.jsx index af2813e84..c67119099 100644 --- a/packages/selenium-ide/src/neo/components/TestRow/index.jsx +++ b/packages/selenium-ide/src/neo/components/TestRow/index.jsx @@ -108,6 +108,7 @@ class TestRow extends React.Component { static propTypes = { index: PropTypes.number, selected: PropTypes.bool, + lastSelected: PropTypes.bool, className: PropTypes.string, status: PropTypes.string, readOnly: PropTypes.bool, @@ -133,9 +134,11 @@ class TestRow extends React.Component { setContextMenu: PropTypes.func, level: PropTypes.number, scrollToLastPos: PropTypes.func, + selectCommandByRange: PropTypes.func, } componentDidMount() { if (this.props.selected) { + this.node.focus() this.props.scrollToLastPos() this.props.setSectionFocus('editor', () => { this.node.focus() @@ -143,7 +146,11 @@ class TestRow extends React.Component { } } componentDidUpdate(prevProps) { - if (this.props.selected && !prevProps.selected) { + if ( + this.props.selected && + !prevProps.lastSelected && + this.props.lastSelected + ) { this.scrollToRowIfNeeded(this.node) this.node.focus() this.props.setSectionFocus('editor', () => { @@ -170,7 +177,7 @@ class TestRow extends React.Component { noModifiers && (e.key === 'Delete' || e.key == 'Backspace') ) { - this.remove() + this.remove(this.props.index) } else if (!this.props.isPristine && noModifiers && key === 'B') { this.props.command.toggleBreakpoint() } else if (!this.props.isPristine && noModifiers && key === 'S') { @@ -185,6 +192,9 @@ class TestRow extends React.Component { this.props.executeCommand(this.props.command) } else if (this.props.moveSelection && noModifiers && e.key === 'ArrowUp') { e.preventDefault() + if (!e.shiftKey) { + this.props.clearAllSelectedCommands() + } this.props.moveSelection(this.props.index - 1) } else if ( this.props.moveSelection && @@ -192,6 +202,9 @@ class TestRow extends React.Component { e.key === 'ArrowDown' ) { e.preventDefault() + if (!e.shiftKey) { + this.props.clearAllSelectedCommands() + } this.props.moveSelection(this.props.index + 1) } else if (!this.props.isPristine && onlyPrimary && key === 'X') { this.cut() @@ -219,7 +232,7 @@ class TestRow extends React.Component { cut() { if (!this.props.readOnly) { this.props.copyToClipboard(this.props.command) - this.props.remove(this.props.index, this.props.command) + this.props.remove() } } paste() { @@ -227,12 +240,38 @@ class TestRow extends React.Component { this.props.pasteFromClipboard(this.props.index) } } - select() { + select(e) { + if ( + (e.type == 'contextmenu' || e.type == 'mousedown') && + this.props.selected + ) { + return + } + + if (e.type == 'click' && (e.shiftKey || this.props.lastSelected)) { + if (!e.shiftKey) { + this.props.clearAllSelectedCommands() + this.props.select(this.props.command) + } + return + } + + if (e && e.shiftKey === false) { + this.props.clearAllSelectedCommands() + } + if (this.props.isPristine) { + this.props.clearAllSelectedCommands() + } + if (e && e.shiftKey && !this.props.isPristine) { + this.props.selectCommandByRange(this.props.index) + } + this.props.select(this.props.command) } - remove() { + + remove(index) { if (!this.props.readOnly) { - this.props.remove(this.props.index, this.props.command) + this.props.remove(index) } } async clearAll() { @@ -278,7 +317,12 @@ class TestRow extends React.Component { > Paste - + { + this.remove(this.props.index) + }} + > Delete @@ -363,13 +407,14 @@ class TestRow extends React.Component { : null } onClick={this.select} + onFocus={this.select} + onMouseDown={this.select} onDoubleClick={() => { this.props.executeCommand && this.props.singleCommandExecutionEnabled ? this.props.executeCommand(this.props.command) : undefined }} onKeyDown={this.handleKeyDown.bind(this)} - onFocus={this.select} style={{ opacity: this.props.isDragging ? '0' : '1', }} diff --git a/packages/selenium-ide/src/neo/components/TestTable/index.jsx b/packages/selenium-ide/src/neo/components/TestTable/index.jsx index 6ce2f5d01..92268a92f 100644 --- a/packages/selenium-ide/src/neo/components/TestTable/index.jsx +++ b/packages/selenium-ide/src/neo/components/TestTable/index.jsx @@ -42,16 +42,19 @@ export default class TestTable extends React.Component { ) this.commandLevels = [] this.node = null + this.selectCommandByRange = this.selectCommandByRange.bind(this) } static propTypes = { commands: MobxPropTypes.arrayOrObservableArray, callstackIndex: PropTypes.number, selectedCommand: PropTypes.string, + selectedCommands: MobxPropTypes.arrayOrObservableArray, selectCommand: PropTypes.func, addCommand: PropTypes.func, - removeCommand: PropTypes.func, + removeSelectedCommands: PropTypes.func, swapCommands: PropTypes.func, clearAllCommands: PropTypes.func, + clearAllSelectedCommand: PropTypes.func, } detectNewCommand(change) { this.newCommand = change.added[0] @@ -77,6 +80,20 @@ export default class TestTable extends React.Component { } } } + selectCommandByRange(index) { + if (this.props.selectedCommands.length > 0) { + let fromIndex = this.props.commands.indexOf( + this.props.selectedCommands[0] + ) + fromIndex = fromIndex == -1 ? index : fromIndex + const toIndex = index + const from = fromIndex > toIndex ? toIndex : fromIndex + const to = fromIndex > toIndex ? fromIndex : toIndex + for (let i = from; i <= to; i++) { + UiState.addToSelectedCommands(this.props.commands[i]) + } + } + } handleScroll() { UiState.selectedTest.test.scrollY = this.node.scrollTop } @@ -130,7 +147,13 @@ export default class TestTable extends React.Component { ).state : '' )} - selected={this.props.selectedCommand === command.id} + selected={ + this.props.selectedCommand === command.id || + !!this.props.selectedCommands.find( + cmd => cmd.id === command.id + ) + } + lastSelected={this.props.selectedCommand === command.id} readOnly={PlaybackState.isPlaying} singleCommandExecutionEnabled={ PlaybackState.isSingleCommandExecutionEnabled @@ -145,15 +168,19 @@ export default class TestTable extends React.Component { scrollToLastPos={this.scrollToLastPos} isPristine={false} select={this.props.selectCommand} + selectCommandByRange={this.selectCommandByRange} startPlayingHere={PlaybackState.startPlaying} executeCommand={PlaybackState.playCommand} moveSelection={UiState.selectCommandByIndex} addCommand={this.props.addCommand} - remove={this.props.removeCommand} + remove={this.props.removeSelectedCommands} swapCommands={this.props.swapCommands} copyToClipboard={UiState.copyToClipboard} pasteFromClipboard={UiState.pasteFromClipboard} clearAllCommands={this.props.clearAllCommands} + clearAllSelectedCommands={ + this.props.clearAllSelectedCommands + } setSectionFocus={UiState.setSectionFocus} level={this.commandLevels[index]} /> @@ -174,6 +201,9 @@ export default class TestTable extends React.Component { moveSelection={UiState.selectCommandByIndex} pasteFromClipboard={UiState.pasteFromClipboard} setSectionFocus={UiState.setSectionFocus} + clearAllSelectedCommands={ + this.props.clearAllSelectedCommands + } /> ) : null} diff --git a/packages/selenium-ide/src/neo/containers/Editor/index.jsx b/packages/selenium-ide/src/neo/containers/Editor/index.jsx index 8012acad9..0262635a2 100644 --- a/packages/selenium-ide/src/neo/containers/Editor/index.jsx +++ b/packages/selenium-ide/src/neo/containers/Editor/index.jsx @@ -38,7 +38,7 @@ export default class Editor extends React.Component { constructor(props) { super(props) this.addCommand = this.addCommand.bind(this) - this.removeCommand = this.removeCommand.bind(this) + this.removeSelectedCommands = this.removeSelectedCommands.bind(this) } addCommand(index, command) { if (command) { @@ -50,17 +50,18 @@ export default class Editor extends React.Component { return newCommand } } - removeCommand(index, command) { + removeSelectedCommands(index) { const { test } = this.props - test.removeCommand(command) - if (UiState.selectedCommand === command) { - if (test.commands.length > index) { - UiState.selectCommand(test.commands[index]) - } else if (test.commands.length) { - UiState.selectCommand(test.commands[test.commands.length - 1]) - } else { - UiState.selectCommand(UiState.pristineCommand) - } + index = index - (UiState.selectedCommands.length - 1) + index = Math.max(index, 0) + UiState.selectedCommands.forEach(command => test.removeCommand(command)) + UiState.clearAllSelectedCommands() + if (test.commands.length > index) { + UiState.selectCommand(test.commands[index]) + } else if (test.commands.length) { + UiState.selectCommand(test.commands[test.commands.length - 1]) + } else { + UiState.selectCommand(UiState.pristineCommand) } } handleKeyDown(event) { @@ -94,12 +95,14 @@ export default class Editor extends React.Component { selectedCommand={ UiState.selectedCommand ? UiState.selectedCommand.id : null } + selectedCommands={UiState.selectedCommands} selectCommand={UiState.selectCommand} addCommand={this.addCommand} - removeCommand={this.removeCommand} + removeSelectedCommands={this.removeSelectedCommands} clearAllCommands={ this.props.test ? this.props.test.clearAllCommands : null } + clearAllSelectedCommands={UiState.clearAllSelectedCommands} swapCommands={this.props.test ? this.props.test.swapCommands : null} /> { + let c1Index = this.displayedTest.commands.indexOf(c1) + let c2Index = this.displayedTest.commands.indexOf(c2) + return c1Index - c2Index + }) } @action.bound pasteFromClipboard(index) { - if (this.clipboard && this.displayedTest) { - const newCommand = this.clipboard.clone() - this.displayedTest.insertCommandAt(newCommand, index) + if (this.clipboard.length && this.displayedTest) { + this.clipboard.forEach((command, idx) => { + const newCommand = command.clone() + this.displayedTest.insertCommandAt(newCommand, index + idx + 1) + }) } } @@ -200,11 +209,18 @@ class UiState { _test && (_test !== this.displayedTest || suite !== this.selectedTest.suite) ) { + if (this.selectedTest.test) { + this.selectedTest.test.selectedCommands = this.selectedCommands.slice( + 0 + ) + } + this.selectedCommands.clear() this.selectedTest = { test, suite, stack: stack >= 0 ? stack : undefined, } + this.selectedCommands = this.selectedTest.test.selectedCommands.slice(0) if (PlaybackState.isPlaying && !PlaybackState.paused) { this.selectCommand(undefined) } else if (_test && _test.commands.length) { @@ -295,6 +311,7 @@ class UiState { if (this.selectedTest.test) { this.selectedTest.test.selectedCommand = command this.selectedCommand = command + this.addToSelectedCommands(command) } else { this.selectedCommand = undefined } @@ -307,10 +324,41 @@ class UiState { if (index >= 0 && index < test.commands.length) { this.selectCommand(test.commands[index], opts) } else if (index === test.commands.length) { + this.clearAllSelectedCommands() this.selectCommand(this.pristineCommand, opts) } } + @action.bound + clearAllSelectedCommands() { + if (this.selectedTest.test) { + this.selectedTest.test.selectedCommand = undefined + this.selectedTest.test.selectedCommands.clear() + this.selectedCommands.clear() + this.selectedCommand = undefined + } else { + this.selectedCommand = undefined + this.selectedCommands.clear() + } + } + + @action.bound + addToSelectedCommands(command) { + if (!PlaybackState.isPlaying || PlaybackState.paused) { + if (command) { + if ( + this.selectTest && + this.selectedTest.test.commands.find(c => c === command) + ) { + if ( + this.selectedCommands.findIndex(c => c.id === command.id) === -1 + ) { + this.selectedCommands.push(command) + } + } + } + } + } @action.bound selectNextCommand(opts = { from: undefined, isCommandTarget: false }) { this.selectCommandByIndex(this.nextCommandIndex(opts.from), opts)