Skip to content
This repository was archived by the owner on Jul 30, 2025. It is now read-only.

Commit e935092

Browse files
myan9starpit
authored andcommitted
feat(plugins/plugin-client-common): allow users to insert command before a repl block
Fixes #5576
1 parent 40a4bd1 commit e935092

File tree

5 files changed

+116
-8
lines changed

5 files changed

+116
-8
lines changed

packages/test/src/api/selectors.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ export const PROMPT_BLOCK_FINAL = `${PROMPT_BLOCK}:nth-last-child(1)`
137137
export const OVERFLOW_MENU = '.kui--repl-block-right-element.kui--toolbar-button-with-icon'
138138
export const PROMPT_BLOCK_MENU = (N: number) => `${PROMPT_BLOCK_N(N)} ${OVERFLOW_MENU}`
139139
export const BLOCK_REMOVE_BUTTON = `${OVERFLOW_MENU} button[data-mode="Remove"]` // in carbon, this is a global
140+
export const BLOCK_INSERT_BUTTON = `${OVERFLOW_MENU} button[data-mode="Insert Command"]` // in carbon, this is a global
140141
export const COMMAND_COPY_BUTTON = `${OVERFLOW_MENU} button[data-mode="Copy"]` // in carbon, this is a global
141142
export const COMMAND_RERUN_BUTTON = `${OVERFLOW_MENU} button[data-mode="Rerun"]` // in carbon, this is a global
142143
export const PROMPT_LAST = `${PROMPT_BLOCK_LAST} .repl-input-element`

plugins/plugin-client-common/i18n/resources_en_US.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
"concurrencyColdStartInDurationSplit": "{0}-way concurrency in cold starts with execution time {1}",
2424
"Copy": "Copy",
2525
"Rerun": "Rerun",
26+
"Insert Command": "Insert Command",
2627
"Split the Terminal": "Split the Terminal",
2728
"Output has been pinned to a watch pane": "Output has been **pinned** to a watch pane",
2829
"No more splits allowed": "No more splits allowed",

plugins/plugin-client-common/src/components/Views/Terminal/Block/Input.tsx

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,9 @@ export interface InputOptions {
9393
/** Remove the enclosing block */
9494
willRemove?: () => void
9595

96+
/** insert an active block before this block */
97+
willInsertBlock?: () => void
98+
9699
/** Capture a screenshot of the enclosing block */
97100
willScreenshot?: () => void
98101

@@ -549,6 +552,17 @@ export default class Input extends InputProvider {
549552
]
550553
}
551554

555+
private insertAction(): DropDownAction[] {
556+
return !this.props.willInsertBlock
557+
? []
558+
: [
559+
{
560+
label: strings('Insert Command'),
561+
handler: () => this.props.willInsertBlock()
562+
}
563+
]
564+
}
565+
552566
private screenshotAction(): DropDownAction[] {
553567
return !this.props.willScreenshot || inBrowser()
554568
? []
@@ -566,7 +580,8 @@ export default class Input extends InputProvider {
566580
const actions = this.screenshotAction().concat(
567581
this.copyAction(command),
568582
this.rerunAction(command),
569-
this.removeAction()
583+
this.removeAction(),
584+
this.insertAction()
570585
)
571586
return (
572587
<DropDown

plugins/plugin-client-common/src/components/Views/Terminal/ScrollableTerminal.tsx

Lines changed: 45 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,16 @@ export default class ScrollableTerminal extends React.PureComponent<Props, State
354354
.concat([Processing(curState.blocks[rerunIdx], event, event.evaluatorOptions.isExperimental, true)])
355355
.concat(curState.blocks.slice(rerunIdx + 1)) // everything after
356356
}
357+
} else if (this.hasActiveBlock(curState)) {
358+
// Transform the active block to Processing
359+
const activeBlockIdx = this.findActiveBlock(curState)
360+
361+
return {
362+
blocks: curState.blocks
363+
.slice(0, activeBlockIdx)
364+
.concat([Processing(curState.blocks[activeBlockIdx], event, event.evaluatorOptions.isExperimental)])
365+
.concat(curState.blocks.slice(activeBlockIdx + 1))
366+
}
357367
} else {
358368
// Transform the last block to Processing
359369
return {
@@ -406,7 +416,8 @@ export default class ScrollableTerminal extends React.PureComponent<Props, State
406416
.slice(0, inProcessIdx) // everything before
407417
.concat([Finished(inProcess, event, prefersTerminalPresentation, outputOnly)]) // mark as finished
408418
.concat(curState.blocks.slice(inProcessIdx + 1)) // everything after
409-
.concat(!inProcess.isRerun ? [Active()] : []) // plus a new block!
419+
.concat(!inProcess.isRerun && !this.hasActiveBlock(curState) ? [Active()] : []) // plus a new block!
420+
410421
return {
411422
blocks
412423
}
@@ -418,13 +429,14 @@ export default class ScrollableTerminal extends React.PureComponent<Props, State
418429
console.error('invalid state: got a command completion event for a block that is not processing', event)
419430
}
420431
} else if (event.cancelled) {
421-
// we get here if the user just types ctrl+c without having executed any command. add a new block!
422-
const inProcessIdx = curState.blocks.length - 1
432+
// we get here if the user just types ctrl+c without having executed any command. add a new block if needed!
433+
const inProcessIdx = this.hasActiveBlock(curState) ? this.findActiveBlock(curState) : curState.blocks.length - 1
423434
const inProcess = curState.blocks[inProcessIdx]
424435
const blocks = curState.blocks
425436
.slice(0, inProcessIdx)
426437
.concat([Cancelled(inProcess)]) // mark as cancelled
427-
.concat([Active()]) // plus a new block!
438+
.concat(curState.blocks.slice(inProcessIdx + 1))
439+
.concat(inProcessIdx === curState.blocks.length - 1 ? [Active()] : []) // plus a new block if needed
428440
return {
429441
blocks
430442
}
@@ -640,6 +652,18 @@ export default class ScrollableTerminal extends React.PureComponent<Props, State
640652
})
641653
}
642654

655+
/** insert an active block before the given idx */
656+
private willInsertBlock(uuid: string, idx: number) {
657+
this.splice(uuid, curState => {
658+
return {
659+
blocks: curState.blocks
660+
.slice(0, idx)
661+
.concat([Active()])
662+
.concat(curState.blocks.slice(idx))
663+
}
664+
})
665+
}
666+
643667
/** remove the block at the given index */
644668
private willRemoveBlock(uuid: string, idx: number) {
645669
this.splice(uuid, curState => {
@@ -649,11 +673,21 @@ export default class ScrollableTerminal extends React.PureComponent<Props, State
649673
blocks: curState.blocks
650674
.slice(0, idx)
651675
.concat(curState.blocks.slice(idx + 1))
652-
.concat(curState.blocks.find(_ => isActive(_)) ? [] : [Active()]) // plus a new block, if needed
676+
.concat(this.hasActiveBlock(curState) ? [] : [Active()]) // plus a new block, if needed
653677
}
654678
})
655679
}
656680

681+
/** whether the given scrollback has Active Block */
682+
private hasActiveBlock(scrollback: ScrollbackState) {
683+
return scrollback.blocks.findIndex(b => isActive(b)) > -1
684+
}
685+
686+
/** return the index of the Active Block from a scrollback */
687+
private findActiveBlock(scrollback: ScrollbackState) {
688+
return scrollback.blocks.findIndex(b => isActive(b))
689+
}
690+
657691
private tabRefFor(scrollback: ScrollbackState, ref: HTMLElement) {
658692
if (ref) {
659693
ref['facade'] = scrollback.facade
@@ -814,17 +848,21 @@ export default class ScrollableTerminal extends React.PureComponent<Props, State
814848
tab={tab}
815849
noActiveInput={this.props.noActiveInput}
816850
onOutputRender={this.onOutputRender.bind(this, scrollback)}
851+
willInsertBlock={this.willInsertBlock.bind(this, scrollback.uuid, idx)}
817852
willRemove={this.willRemoveBlock.bind(this, scrollback.uuid, idx)}
818853
willLoseFocus={() => this.doFocus(scrollback)}
819854
isExperimental={hasCommand(_) && _.isExperimental}
820-
isFocused={sbidx === this.state.focusedIdx && isActive(_)}
855+
isFocused={
856+
// grab focus if this is the first active block in the scrollback
857+
sbidx === this.state.focusedIdx && idx === this.findActiveBlock(scrollback)
858+
}
821859
prefersTerminalPresentation={isOk(_) && _.prefersTerminalPresentation}
822860
isPartOfMiniSplit={isMiniSplit}
823861
isVisibleInMiniSplit={idx === showThisIdxInMiniSplit || idx === nBlocks - 1}
824862
isWidthConstrained={isWidthConstrained}
825863
navigateTo={this.navigateTo.bind(this, scrollback)}
826864
ref={c => {
827-
if (isActive(_)) {
865+
if (idx === this.findActiveBlock(scrollback)) {
828866
// grab a ref to the active block, to help us maintain focus
829867
scrollback._activeBlock = c
830868
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/*
2+
* Copyright 2020 IBM Corporation
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import * as assert from 'assert'
18+
19+
import { Common, CLI, ReplExpect, Selectors } from '@kui-shell/test'
20+
21+
describe(`insert command ${process.env.MOCHA_RUN_TARGET || ''}`, function(this: Common.ISuite) {
22+
before(Common.before(this))
23+
after(Common.after(this))
24+
25+
// here come the tests
26+
it(`should show version then insert a block before`, async () => {
27+
try {
28+
const res1 = await CLI.command('version', this.app)
29+
await ReplExpect.okWithCustom({ expect: Common.expectedVersion })(res1)
30+
31+
const N = res1.count
32+
this.app.client.click(Selectors.PROMPT_BLOCK_MENU(N))
33+
await this.app.client.waitForVisible(Selectors.BLOCK_INSERT_BUTTON)
34+
await this.app.client.click(Selectors.BLOCK_INSERT_BUTTON)
35+
36+
await CLI.command('# show version', this.app)
37+
await this.app.client.waitForVisible(`${Selectors.OUTPUT_N(N)} ${Selectors.TERMINAL_CARD}`)
38+
const title: string = await this.app.client.getText(`${Selectors.OUTPUT_N(N)} ${Selectors.TERMINAL_CARD}`)
39+
assert.strictEqual(title, 'show version')
40+
41+
await this.app.client.waitForVisible(`${Selectors.OUTPUT_N(N + 1)}`)
42+
const version: string = await this.app.client.getText(`${Selectors.OUTPUT_N(N + 1)}`)
43+
assert.strictEqual(version, Common.expectedVersion)
44+
} catch (err) {
45+
await Common.oops(this, true)(err)
46+
}
47+
})
48+
49+
it(`should report proper version`, () =>
50+
CLI.command('version', this.app)
51+
.then(ReplExpect.okWithCustom({ expect: Common.expectedVersion }))
52+
.catch(Common.oops(this, true)))
53+
})

0 commit comments

Comments
 (0)