From f4000cff72e415e3946c2e66e972ac1e99e397d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20=C5=A0renkel?= Date: Sun, 8 Oct 2023 21:39:14 +0200 Subject: [PATCH 1/6] added validation in case no choice is selected --- packages/checkbox/src/index.mts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/packages/checkbox/src/index.mts b/packages/checkbox/src/index.mts index b222182d9..d4500edc1 100644 --- a/packages/checkbox/src/index.mts +++ b/packages/checkbox/src/index.mts @@ -96,9 +96,14 @@ export default createPrompt( const [active, setActive] = useState(bounds.first); const [showHelpTip, setShowHelpTip] = useState(true); + const [showValidationMessage, setShowValidationMessage] = useState(false); useKeypress((key) => { if (isEnterKey(key)) { + if (items.filter(isChecked).length === 0) { + setShowValidationMessage(true); + return; + } setStatus('done'); done(items.filter(isChecked).map((choice) => choice.value)); } else if (isUpKey(key) || isDownKey(key)) { @@ -111,6 +116,7 @@ export default createPrompt( } while (!isSelectable(items[next]!)); setActive(next); } else if (isSpaceKey(key)) { + setShowValidationMessage(false); setShowHelpTip(false); setItems(items.map((choice, i) => (i === active ? toggle(choice) : choice))); } else if (key.name === 'a') { @@ -162,7 +168,12 @@ export default createPrompt( } } - return `${prefix} ${message}${helpTip}\n${page}${ansiEscapes.cursorHide}`; + let validationMessage = ''; + if (showValidationMessage) { + validationMessage = `${chalk.red('!')} You need to select at least one choice`; + } + + return `${prefix} ${message}${helpTip}\n${page}\n${validationMessage}${ansiEscapes.cursorHide}`; }, ); From 3b4e0e74bcc76ced4ca54949c91b5d797cd9b6e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20=C5=A0renkel?= Date: Sun, 8 Oct 2023 21:40:18 +0200 Subject: [PATCH 2/6] added unit test for validation logic, edited existing unit tests since they were failing because of new validation logic --- packages/checkbox/checkbox.test.mts | 67 ++++++++++++++++++++++++----- 1 file changed, 56 insertions(+), 11 deletions(-) diff --git a/packages/checkbox/checkbox.test.mts b/packages/checkbox/checkbox.test.mts index d0818e429..c66c50baf 100644 --- a/packages/checkbox/checkbox.test.mts +++ b/packages/checkbox/checkbox.test.mts @@ -262,8 +262,9 @@ describe('checkbox prompt', () => { (Use arrow keys to reveal more choices)" `); + events.keypress('space'); events.keypress('enter'); - await expect(answer).resolves.toEqual([]); + await expect(answer).resolves.toEqual([1]); }); it('allow setting a bigger page size', async () => { @@ -289,8 +290,9 @@ describe('checkbox prompt', () => { (Use arrow keys to reveal more choices)" `); + events.keypress('space'); events.keypress('enter'); - await expect(answer).resolves.toEqual([]); + await expect(answer).resolves.toEqual([1]); }); it('cycles through options', async () => { @@ -383,10 +385,11 @@ describe('checkbox prompt', () => { ◯ Pepperoni" `); + events.keypress('space'); events.keypress('enter'); - expect(getScreen()).toMatchInlineSnapshot('"? Select a topping"'); + expect(getScreen()).toMatchInlineSnapshot('"? Select a topping Ham"'); - await expect(answer).resolves.toEqual([]); + await expect(answer).resolves.toEqual(['ham']); }); it('skip separator by arrow keys', async () => { @@ -449,10 +452,11 @@ describe('checkbox prompt', () => { ◯ Pepperoni" `); + events.keypress('space'); events.keypress('enter'); - expect(getScreen()).toMatchInlineSnapshot('"? Select a topping"'); + expect(getScreen()).toMatchInlineSnapshot('"? Select a topping Ham"'); - await expect(answer).resolves.toEqual([]); + await expect(answer).resolves.toEqual(['ham']); }); it('allow select all', async () => { @@ -530,8 +534,9 @@ describe('checkbox prompt', () => { events.keypress('a'); events.keypress('a'); + events.keypress('space'); events.keypress('enter'); - await expect(answer).resolves.toEqual([]); + await expect(answer).resolves.toEqual([4]); }); it('allow inverting selection', async () => { @@ -581,10 +586,11 @@ describe('checkbox prompt', () => { (Use arrow keys to reveal more choices)" `); + events.keypress('space'); events.keypress('enter'); - expect(getScreen()).toMatchInlineSnapshot('"? Select a number"'); + expect(getScreen()).toMatchInlineSnapshot('"? Select a number 1"'); - await expect(answer).resolves.toEqual([]); + await expect(answer).resolves.toEqual([1]); }); it('allow customizing help tip', async () => { @@ -608,10 +614,11 @@ describe('checkbox prompt', () => { (Use arrow keys to reveal more choices)" `); + events.keypress('space'); events.keypress('enter'); - expect(getScreen()).toMatchInlineSnapshot('"? Select a number"'); + expect(getScreen()).toMatchInlineSnapshot('"? Select a number 1"'); - await expect(answer).resolves.toEqual([]); + await expect(answer).resolves.toEqual([1]); }); it('throws if all choices are disabled', async () => { @@ -624,4 +631,42 @@ describe('checkbox prompt', () => { '"[checkbox prompt] No selectable choices. All choices are disabled."', ); }); + + it('shows validation message if user did not select any choice', async () => { + const { answer, events, getScreen } = await render(checkbox, { + message: 'Select a number', + choices: numberedChoices, + }); + + events.keypress('enter'); + expect(getScreen()).toMatchInlineSnapshot(` + "? Select a number (Press to select, to toggle all, to invert + selection, and to proceed) + ❯◯ 1 + ◯ 2 + ◯ 3 + ◯ 4 + ◯ 5 + ◯ 6 + ◯ 7 + (Use arrow keys to reveal more choices) + ! You need to select at least one choice" + `); + + events.keypress('space'); + expect(getScreen()).toMatchInlineSnapshot(` + "? Select a number + ❯◉ 1 + ◯ 2 + ◯ 3 + ◯ 4 + ◯ 5 + ◯ 6 + ◯ 7 + (Use arrow keys to reveal more choices)" + `); + + events.keypress('enter'); + await expect(answer).resolves.toEqual([1]); + }); }); From 09c8deb9fcb516d64838cf9a3ef81d948d4fcc0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20=C5=A0renkel?= Date: Tue, 10 Oct 2023 21:53:41 +0200 Subject: [PATCH 3/6] Revert "added unit test for validation logic, edited existing unit tests since they were failing because of new validation logic" This reverts commit 3b4e0e74bcc76ced4ca54949c91b5d797cd9b6e9. --- packages/checkbox/checkbox.test.mts | 67 +++++------------------------ 1 file changed, 11 insertions(+), 56 deletions(-) diff --git a/packages/checkbox/checkbox.test.mts b/packages/checkbox/checkbox.test.mts index c66c50baf..d0818e429 100644 --- a/packages/checkbox/checkbox.test.mts +++ b/packages/checkbox/checkbox.test.mts @@ -262,9 +262,8 @@ describe('checkbox prompt', () => { (Use arrow keys to reveal more choices)" `); - events.keypress('space'); events.keypress('enter'); - await expect(answer).resolves.toEqual([1]); + await expect(answer).resolves.toEqual([]); }); it('allow setting a bigger page size', async () => { @@ -290,9 +289,8 @@ describe('checkbox prompt', () => { (Use arrow keys to reveal more choices)" `); - events.keypress('space'); events.keypress('enter'); - await expect(answer).resolves.toEqual([1]); + await expect(answer).resolves.toEqual([]); }); it('cycles through options', async () => { @@ -385,11 +383,10 @@ describe('checkbox prompt', () => { ◯ Pepperoni" `); - events.keypress('space'); events.keypress('enter'); - expect(getScreen()).toMatchInlineSnapshot('"? Select a topping Ham"'); + expect(getScreen()).toMatchInlineSnapshot('"? Select a topping"'); - await expect(answer).resolves.toEqual(['ham']); + await expect(answer).resolves.toEqual([]); }); it('skip separator by arrow keys', async () => { @@ -452,11 +449,10 @@ describe('checkbox prompt', () => { ◯ Pepperoni" `); - events.keypress('space'); events.keypress('enter'); - expect(getScreen()).toMatchInlineSnapshot('"? Select a topping Ham"'); + expect(getScreen()).toMatchInlineSnapshot('"? Select a topping"'); - await expect(answer).resolves.toEqual(['ham']); + await expect(answer).resolves.toEqual([]); }); it('allow select all', async () => { @@ -534,9 +530,8 @@ describe('checkbox prompt', () => { events.keypress('a'); events.keypress('a'); - events.keypress('space'); events.keypress('enter'); - await expect(answer).resolves.toEqual([4]); + await expect(answer).resolves.toEqual([]); }); it('allow inverting selection', async () => { @@ -586,11 +581,10 @@ describe('checkbox prompt', () => { (Use arrow keys to reveal more choices)" `); - events.keypress('space'); events.keypress('enter'); - expect(getScreen()).toMatchInlineSnapshot('"? Select a number 1"'); + expect(getScreen()).toMatchInlineSnapshot('"? Select a number"'); - await expect(answer).resolves.toEqual([1]); + await expect(answer).resolves.toEqual([]); }); it('allow customizing help tip', async () => { @@ -614,11 +608,10 @@ describe('checkbox prompt', () => { (Use arrow keys to reveal more choices)" `); - events.keypress('space'); events.keypress('enter'); - expect(getScreen()).toMatchInlineSnapshot('"? Select a number 1"'); + expect(getScreen()).toMatchInlineSnapshot('"? Select a number"'); - await expect(answer).resolves.toEqual([1]); + await expect(answer).resolves.toEqual([]); }); it('throws if all choices are disabled', async () => { @@ -631,42 +624,4 @@ describe('checkbox prompt', () => { '"[checkbox prompt] No selectable choices. All choices are disabled."', ); }); - - it('shows validation message if user did not select any choice', async () => { - const { answer, events, getScreen } = await render(checkbox, { - message: 'Select a number', - choices: numberedChoices, - }); - - events.keypress('enter'); - expect(getScreen()).toMatchInlineSnapshot(` - "? Select a number (Press to select, to toggle all, to invert - selection, and to proceed) - ❯◯ 1 - ◯ 2 - ◯ 3 - ◯ 4 - ◯ 5 - ◯ 6 - ◯ 7 - (Use arrow keys to reveal more choices) - ! You need to select at least one choice" - `); - - events.keypress('space'); - expect(getScreen()).toMatchInlineSnapshot(` - "? Select a number - ❯◉ 1 - ◯ 2 - ◯ 3 - ◯ 4 - ◯ 5 - ◯ 6 - ◯ 7 - (Use arrow keys to reveal more choices)" - `); - - events.keypress('enter'); - await expect(answer).resolves.toEqual([1]); - }); }); From b80bee9ce82a15b722274bc2ca06424d82c732f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20=C5=A0renkel?= Date: Tue, 10 Oct 2023 22:21:48 +0200 Subject: [PATCH 4/6] added 'required' prop, changed validation logic so that its more flexible, fixed unit tests --- packages/checkbox/checkbox.test.mts | 39 +++++++++++++++++++++++++++++ packages/checkbox/src/index.mts | 32 ++++++++++++++--------- 2 files changed, 59 insertions(+), 12 deletions(-) diff --git a/packages/checkbox/checkbox.test.mts b/packages/checkbox/checkbox.test.mts index d0818e429..9ba19f1fd 100644 --- a/packages/checkbox/checkbox.test.mts +++ b/packages/checkbox/checkbox.test.mts @@ -624,4 +624,43 @@ describe('checkbox prompt', () => { '"[checkbox prompt] No selectable choices. All choices are disabled."', ); }); + + it('shows validation message if user did not select any choice', async () => { + const { answer, events, getScreen } = await render(checkbox, { + message: 'Select a number', + choices: numberedChoices, + required: true, + }); + + events.keypress('enter'); + expect(getScreen()).toMatchInlineSnapshot(` + "? Select a number (Press to select, to toggle all, to invert + selection, and to proceed) + ❯◯ 1 + ◯ 2 + ◯ 3 + ◯ 4 + ◯ 5 + ◯ 6 + ◯ 7 + (Use arrow keys to reveal more choices) + > At least one choice must be selected" + `); + + events.keypress('space'); + expect(getScreen()).toMatchInlineSnapshot(` + "? Select a number + ❯◉ 1 + ◯ 2 + ◯ 3 + ◯ 4 + ◯ 5 + ◯ 6 + ◯ 7 + (Use arrow keys to reveal more choices)" + `); + + events.keypress('enter'); + await expect(answer).resolves.toEqual([1]); + }); }); diff --git a/packages/checkbox/src/index.mts b/packages/checkbox/src/index.mts index d4500edc1..23856a6ac 100644 --- a/packages/checkbox/src/index.mts +++ b/packages/checkbox/src/index.mts @@ -32,6 +32,7 @@ type Config = PromptConfig<{ instructions?: string | boolean; choices: ReadonlyArray | Separator>; loop?: boolean; + required?: string | boolean; }>; type Item = Separator | Choice; @@ -74,7 +75,14 @@ function renderItem({ item, isActive }: { item: Item; isActive: bo export default createPrompt( (config: Config, done: (value: Array) => void) => { - const { prefix = usePrefix(), instructions, pageSize, loop = true, choices } = config; + const { + prefix = usePrefix(), + instructions, + pageSize, + loop = true, + choices, + required, + } = config; const [status, setStatus] = useState('pending'); const [items, setItems] = useState>>( choices.map((choice) => ({ ...choice })), @@ -96,16 +104,16 @@ export default createPrompt( const [active, setActive] = useState(bounds.first); const [showHelpTip, setShowHelpTip] = useState(true); - const [showValidationMessage, setShowValidationMessage] = useState(false); + const [errorMsg, setError] = useState(undefined); useKeypress((key) => { if (isEnterKey(key)) { - if (items.filter(isChecked).length === 0) { - setShowValidationMessage(true); - return; + if (required && items.filter(isChecked).length === 0) { + setError('At least one choice must be selected'); + } else { + setStatus('done'); + done(items.filter(isChecked).map((choice) => choice.value)); } - setStatus('done'); - done(items.filter(isChecked).map((choice) => choice.value)); } else if (isUpKey(key) || isDownKey(key)) { if (!loop && active === bounds.first && isUpKey(key)) return; if (!loop && active === bounds.last && isDownKey(key)) return; @@ -116,7 +124,7 @@ export default createPrompt( } while (!isSelectable(items[next]!)); setActive(next); } else if (isSpaceKey(key)) { - setShowValidationMessage(false); + setError(undefined); setShowHelpTip(false); setItems(items.map((choice, i) => (i === active ? toggle(choice) : choice))); } else if (key.name === 'a') { @@ -168,12 +176,12 @@ export default createPrompt( } } - let validationMessage = ''; - if (showValidationMessage) { - validationMessage = `${chalk.red('!')} You need to select at least one choice`; + let error = ''; + if (errorMsg) { + error = chalk.red(`> ${errorMsg}`); } - return `${prefix} ${message}${helpTip}\n${page}\n${validationMessage}${ansiEscapes.cursorHide}`; + return `${prefix} ${message}${helpTip}\n${page}\n${error}${ansiEscapes.cursorHide}`; }, ); From 4c390f51e9a0ef12393d2f5a1769df72e0d67c19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20=C5=A0renkel?= Date: Wed, 11 Oct 2023 13:05:53 +0200 Subject: [PATCH 5/6] updated readme, fixed type of required --- packages/checkbox/README.md | 1 + packages/checkbox/src/index.mts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/checkbox/README.md b/packages/checkbox/README.md index 4394758ca..a8ff806b8 100644 --- a/packages/checkbox/README.md +++ b/packages/checkbox/README.md @@ -41,6 +41,7 @@ const answer = await checkbox({ | choices | `Array<{ value: string, name?: string, disabled?: boolean \| string, checked?: boolean } \| Separator>` | yes | List of the available choices. The `value` will be returned as the answer, and used as display if no `name` is defined. Choices who're `disabled` will be displayed, but not selectable. | | pageSize | `number` | no | By default, lists of choice longer than 7 will be paginated. Use this option to control how many choices will appear on the screen at once. | | loop | `boolean` | no | Defaults to `true`. When set to `false`, the cursor will be constrained to the top and bottom of the choice list without looping. | +| required | `boolean` | no | When set to `true`, ensures at least one choice must be selected. | The `Separator` object can be used to render non-selectable lines in the choice list. By default it'll render a line, but you can provide the text as argument (`new Separator('-- Dependencies --')`). This option is often used to add labels to groups within long list of options. diff --git a/packages/checkbox/src/index.mts b/packages/checkbox/src/index.mts index 23856a6ac..2fa3592f5 100644 --- a/packages/checkbox/src/index.mts +++ b/packages/checkbox/src/index.mts @@ -32,7 +32,7 @@ type Config = PromptConfig<{ instructions?: string | boolean; choices: ReadonlyArray | Separator>; loop?: boolean; - required?: string | boolean; + required?: boolean; }>; type Item = Separator | Choice; From 6fde1d636ee543e6783632170ad17b6e14136a9f Mon Sep 17 00:00:00 2001 From: Simon Boudrias Date: Sun, 15 Oct 2023 15:53:29 -0700 Subject: [PATCH 6/6] Update packages/checkbox/src/index.mts --- packages/checkbox/src/index.mts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/checkbox/src/index.mts b/packages/checkbox/src/index.mts index 2fa3592f5..ce8c3f550 100644 --- a/packages/checkbox/src/index.mts +++ b/packages/checkbox/src/index.mts @@ -108,7 +108,7 @@ export default createPrompt( useKeypress((key) => { if (isEnterKey(key)) { - if (required && items.filter(isChecked).length === 0) { + if (required && !items.some(isChecked)) { setError('At least one choice must be selected'); } else { setStatus('done');