From b6dc5a274584f5e843abb78618b16d6fddb06793 Mon Sep 17 00:00:00 2001 From: PierreDemailly <39910767+PierreDemailly@users.noreply.github.com> Date: Fri, 26 Apr 2024 19:05:55 +0200 Subject: [PATCH] fix(multiselect): prevent duplicates (#108) --- src/prompts/multiselect.ts | 20 +++++++-------- test/multi-select-prompt.test.ts | 44 ++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 10 deletions(-) diff --git a/src/prompts/multiselect.ts b/src/prompts/multiselect.ts index 8ec5259..6b131ae 100644 --- a/src/prompts/multiselect.ts +++ b/src/prompts/multiselect.ts @@ -32,7 +32,7 @@ export class MultiselectPrompt extends AbstractPrompt { #showHint: boolean; activeIndex = 0; - selectedIndexes: number[] = []; + selectedIndexes: Set = new Set(); questionMessage: string; autocompleteValue = ""; options: MultiselectOptions; @@ -136,7 +136,7 @@ export class MultiselectPrompt extends AbstractPrompt { throw new Error(`Invalid pre-selected choice: ${typeof choice === "string" ? choice : choice.value}`); } - this.selectedIndexes.push(choiceIndex); + this.selectedIndexes.add(choiceIndex); } } @@ -172,7 +172,7 @@ export class MultiselectPrompt extends AbstractPrompt { for (let choiceIndex = startIndex; choiceIndex < endIndex; choiceIndex++) { const choice = this.#getFormattedChoice(choiceIndex); const isChoiceActive = choiceIndex === this.activeIndex; - const isChoiceSelected = this.selectedIndexes.includes(choiceIndex); + const isChoiceSelected = this.selectedIndexes.has(choiceIndex); const showPreviousChoicesArrow = startIndex > 0 && choiceIndex === startIndex; const showNextChoicesArrow = endIndex < this.filteredChoices.length && choiceIndex === endIndex - 1; @@ -197,7 +197,7 @@ export class MultiselectPrompt extends AbstractPrompt { } #showAnsweredQuestion(choices: string, isAgentAnswer = false) { - const prefixSymbol = this.selectedIndexes.length === 0 && !isAgentAnswer ? SYMBOLS.Cross : SYMBOLS.Tick; + const prefixSymbol = this.selectedIndexes.size === 0 && !isAgentAnswer ? SYMBOLS.Cross : SYMBOLS.Tick; const prefix = `${prefixSymbol} ${kleur.bold(this.message)} ${SYMBOLS.Pointer}`; const formattedChoice = kleur.yellow(choices); @@ -223,24 +223,24 @@ export class MultiselectPrompt extends AbstractPrompt { } else if (key.ctrl && key.name === "a") { // eslint-disable-next-line max-len - this.selectedIndexes = this.selectedIndexes.length === this.filteredChoices.length ? [] : this.filteredChoices.map((_, index) => index); + this.selectedIndexes = this.selectedIndexes.size === this.filteredChoices.length ? new Set() : new Set(this.filteredChoices.map((_, index) => index)); render(); } else if (key.name === "right") { - this.selectedIndexes.push(this.activeIndex); + this.selectedIndexes.add(this.activeIndex); render(); } else if (key.name === "left") { - this.selectedIndexes = this.selectedIndexes.filter((index) => index !== this.activeIndex); + this.selectedIndexes = new Set([...this.selectedIndexes].filter((index) => index !== this.activeIndex)); render(); } else if (key.name === "return") { - const labels = this.selectedIndexes.map((index) => { + const labels = [...this.selectedIndexes].map((index) => { const choice = this.filteredChoices[index]; return typeof choice === "string" ? choice : choice.label; }); - const values = this.selectedIndexes.map((index) => { + const values = [...this.selectedIndexes].map((index) => { const choice = this.filteredChoices[index]; return typeof choice === "string" ? choice : choice.value; @@ -270,7 +270,7 @@ export class MultiselectPrompt extends AbstractPrompt { else { if (!key.ctrl && this.options.autocomplete) { // reset selected choices when user type - this.selectedIndexes = []; + this.selectedIndexes.clear(); this.activeIndex = 0; if (key.name === "backspace" && this.autocompleteValue.length > 0) { this.autocompleteValue = this.autocompleteValue.slice(0, -1); diff --git a/test/multi-select-prompt.test.ts b/test/multi-select-prompt.test.ts index 7ea0812..35d09d1 100644 --- a/test/multi-select-prompt.test.ts +++ b/test/multi-select-prompt.test.ts @@ -873,4 +873,48 @@ describe("MultiselectPrompt", () => { ]); assert.deepEqual(input, ["foo"]); }); + + it("should not have duplicates", async() => { + const logs: string[] = []; + const message = "Choose between foo & bar"; + const options = { + choices: [ + { value: "foo", label: "foo" }, + { value: "bar", label: "bar" } + ] + }; + const inputs = [ + kInputs.right, + kInputs.right, + kInputs.right, + kInputs.return + ]; + const multiselectPrompt = await TestingPrompt.MultiselectPrompt( + message, + { + ...options, + inputs, + onStdoutWrite: (log) => logs.push(log) + } + ); + + const input = await multiselectPrompt.multiselect(); + + assert.deepStrictEqual(logs, [ + "? Choose between foo & bar (Press to toggle all, to toggle, to submit)", + " ○ foo", + " ○ bar", + // we press so the first choice 'foo' is selected + " ● foo", + " ○ bar", + // we press multiple times, it should not add duplicates + " ● foo", + " ○ bar", + " ● foo", + " ○ bar", + // we press so the first choice 'foo' is returned + "✔ Choose between foo & bar › foo" + ]); + assert.deepStrictEqual(input, ["foo"]); + }) });