Skip to content

Commit

Permalink
fix(multiselect): prevent duplicates (#108)
Browse files Browse the repository at this point in the history
  • Loading branch information
PierreDemailly authored Apr 26, 2024
1 parent bf3089b commit b6dc5a2
Show file tree
Hide file tree
Showing 2 changed files with 54 additions and 10 deletions.
20 changes: 10 additions & 10 deletions src/prompts/multiselect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export class MultiselectPrompt extends AbstractPrompt<string | string[]> {
#showHint: boolean;

activeIndex = 0;
selectedIndexes: number[] = [];
selectedIndexes: Set<number> = new Set();
questionMessage: string;
autocompleteValue = "";
options: MultiselectOptions;
Expand Down Expand Up @@ -136,7 +136,7 @@ export class MultiselectPrompt extends AbstractPrompt<string | string[]> {
throw new Error(`Invalid pre-selected choice: ${typeof choice === "string" ? choice : choice.value}`);
}

this.selectedIndexes.push(choiceIndex);
this.selectedIndexes.add(choiceIndex);
}
}

Expand Down Expand Up @@ -172,7 +172,7 @@ export class MultiselectPrompt extends AbstractPrompt<string | string[]> {
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;

Expand All @@ -197,7 +197,7 @@ export class MultiselectPrompt extends AbstractPrompt<string | string[]> {
}

#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);

Expand All @@ -223,24 +223,24 @@ export class MultiselectPrompt extends AbstractPrompt<string | string[]> {
}
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;
Expand Down Expand Up @@ -270,7 +270,7 @@ export class MultiselectPrompt extends AbstractPrompt<string | string[]> {
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);
Expand Down
44 changes: 44 additions & 0 deletions test/multi-select-prompt.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 <Ctrl+A> to toggle all, <Left/Right> to toggle, <Return> to submit)",
" ○ foo",
" ○ bar",
// we press <right> so the first choice 'foo' is selected
" ● foo",
" ○ bar",
// we press <right> multiple times, it should not add duplicates
" ● foo",
" ○ bar",
" ● foo",
" ○ bar",
// we press <return> so the first choice 'foo' is returned
"✔ Choose between foo & bar › foo"
]);
assert.deepStrictEqual(input, ["foo"]);
})
});

0 comments on commit b6dc5a2

Please sign in to comment.