From 49cf08b05952f197154c9545afa531259792dee1 Mon Sep 17 00:00:00 2001 From: Bruno Rocha Date: Mon, 28 Aug 2023 10:48:03 -0300 Subject: [PATCH 1/3] feat: select prompt disabled option --- packages/prompts/src/index.ts | 83 +++++++++++++++++++---------------- 1 file changed, 45 insertions(+), 38 deletions(-) diff --git a/packages/prompts/src/index.ts b/packages/prompts/src/index.ts index c4595561..c420a02d 100644 --- a/packages/prompts/src/index.ts +++ b/packages/prompts/src/index.ts @@ -8,7 +8,7 @@ import { SelectKeyPrompt, SelectPrompt, State, - TextPrompt + TextPrompt, } from '@clack/core'; import isUnicodeSupported from 'is-unicode-supported'; import color from 'picocolors'; @@ -171,8 +171,8 @@ export const confirm = (opts: ConfirmOptions) => { type Primitive = Readonly; type Option = Value extends Primitive - ? { value: Value; label?: string; hint?: string } - : { value: Value; label: string; hint?: string }; + ? { value: Value; label?: string; hint?: string; disabled?: boolean } + : { value: Value; label: string; hint?: string; disabled?: boolean }; export interface SelectOptions { message: string; @@ -185,7 +185,7 @@ export const select = (opts: SelectOptions) => { const opt = (option: Option, state: 'inactive' | 'active' | 'selected' | 'cancelled') => { const label = option.label ?? String(option.value); if (state === 'active') { - return `${color.green(S_RADIO_ACTIVE)} ${label} ${ + return `${color.green(S_RADIO_ACTIVE)} ${option.disabled ? color.dim(label) : label} ${ option.hint ? color.dim(`(${option.hint})`) : '' }`; } else if (state === 'selected') { @@ -196,14 +196,46 @@ export const select = (opts: SelectOptions) => { return `${color.dim(S_RADIO_INACTIVE)} ${color.dim(label)}`; }; - let slidingWindowLocation = 0; - return new SelectPrompt({ options: opts.options, initialValue: opts.initialValue, + validate(value) { + if (this.options.find((o) => o.value === value)?.disabled) { + return 'Selected option is disabled.'; + } + }, render() { const title = `${color.gray(S_BAR)}\n${symbol(this.state)} ${opts.message}\n`; + const limitOptions = (options: Option[]): string[] => { + // We clamp to minimum 5 because anything less doesn't make sense UX wise + const maxItems = opts.maxItems === undefined ? Infinity : Math.max(opts.maxItems, 5); + let slidingWindowLocation = 0; + + if (this.cursor >= slidingWindowLocation + maxItems - 3) { + slidingWindowLocation = Math.max( + Math.min(this.cursor - maxItems + 3, options.length - maxItems), + 0 + ); + } else if (this.cursor < slidingWindowLocation + 2) { + slidingWindowLocation = Math.max(this.cursor - 2, 0); + } + + const shouldRenderTopEllipsis = maxItems < options.length && slidingWindowLocation > 0; + const shouldRenderBottomEllipsis = + maxItems < options.length && slidingWindowLocation + maxItems < options.length; + + return options + .slice(slidingWindowLocation, slidingWindowLocation + maxItems) + .map((option, i, arr) => { + const isTopLimit = i === 0 && shouldRenderTopEllipsis; + const isBottomLimit = i === arr.length - 1 && shouldRenderBottomEllipsis; + return isTopLimit || isBottomLimit + ? color.dim('...') + : opt(option, i + slidingWindowLocation === this.cursor ? 'active' : 'inactive'); + }); + }; + switch (this.state) { case 'submit': return `${title}${color.gray(S_BAR)} ${opt(this.options[this.cursor], 'selected')}`; @@ -212,39 +244,14 @@ export const select = (opts: SelectOptions) => { this.options[this.cursor], 'cancelled' )}\n${color.gray(S_BAR)}`; + case 'error': + return `${title}${color.yellow(S_BAR)} ${limitOptions(this.options).join( + `\n${color.yellow(S_BAR)} ` + )}\n${color.yellow(S_BAR_END)} ${color.yellow(this.error)}\n`; default: { - // We clamp to minimum 5 because anything less doesn't make sense UX wise - const maxItems = opts.maxItems === undefined ? Infinity : Math.max(opts.maxItems, 5); - if (this.cursor >= slidingWindowLocation + maxItems - 3) { - slidingWindowLocation = Math.max( - Math.min(this.cursor - maxItems + 3, this.options.length - maxItems), - 0 - ); - } else if (this.cursor < slidingWindowLocation + 2) { - slidingWindowLocation = Math.max(this.cursor - 2, 0); - } - - const shouldRenderTopEllipsis = - maxItems < this.options.length && slidingWindowLocation > 0; - const shouldRenderBottomEllipsis = - maxItems < this.options.length && - slidingWindowLocation + maxItems < this.options.length; - - return `${title}${color.cyan(S_BAR)} ${this.options - .slice(slidingWindowLocation, slidingWindowLocation + maxItems) - .map((option, i, arr) => { - if (i === 0 && shouldRenderTopEllipsis) { - return color.dim('...'); - } else if (i === arr.length - 1 && shouldRenderBottomEllipsis) { - return color.dim('...'); - } else { - return opt( - option, - i + slidingWindowLocation === this.cursor ? 'active' : 'inactive' - ); - } - }) - .join(`\n${color.cyan(S_BAR)} `)}\n${color.cyan(S_BAR_END)}\n`; + return `${title}${color.cyan(S_BAR)} ${limitOptions(this.options).join( + `\n${color.cyan(S_BAR)} ` + )}\n${color.cyan(S_BAR_END)}\n`; } } }, From 0e6ce60dcc3d62be4d36834579368b69d7cb8549 Mon Sep 17 00:00:00 2001 From: Bruno Rocha Date: Mon, 28 Aug 2023 12:32:21 -0300 Subject: [PATCH 2/3] feat: multiselect prompt disabled option --- packages/prompts/src/index.ts | 59 ++++++++++++++++++++++++----------- 1 file changed, 40 insertions(+), 19 deletions(-) diff --git a/packages/prompts/src/index.ts b/packages/prompts/src/index.ts index c420a02d..50a37663 100644 --- a/packages/prompts/src/index.ts +++ b/packages/prompts/src/index.ts @@ -8,7 +8,7 @@ import { SelectKeyPrompt, SelectPrompt, State, - TextPrompt, + TextPrompt } from '@clack/core'; import isUnicodeSupported from 'is-unicode-supported'; import color from 'picocolors'; @@ -171,23 +171,28 @@ export const confirm = (opts: ConfirmOptions) => { type Primitive = Readonly; type Option = Value extends Primitive - ? { value: Value; label?: string; hint?: string; disabled?: boolean } - : { value: Value; label: string; hint?: string; disabled?: boolean }; + ? { value: Value; label?: string; hint?: string } + : { value: Value; label: string; hint?: string }; + +type DisableableOption = Option & { disabled?: boolean }; export interface SelectOptions { message: string; - options: Option[]; + options: DisableableOption[]; initialValue?: Value; maxItems?: number; } export const select = (opts: SelectOptions) => { - const opt = (option: Option, state: 'inactive' | 'active' | 'selected' | 'cancelled') => { + const opt = ( + option: DisableableOption, + state: 'inactive' | 'active' | 'selected' | 'cancelled' + ) => { const label = option.label ?? String(option.value); if (state === 'active') { - return `${color.green(S_RADIO_ACTIVE)} ${option.disabled ? color.dim(label) : label} ${ - option.hint ? color.dim(`(${option.hint})`) : '' - }`; + return `${ + option.disabled ? color.dim(S_RADIO_ACTIVE) : color.green(S_RADIO_ACTIVE) + } ${label} ${option.hint ? color.dim(`(${option.hint})`) : ''}`; } else if (state === 'selected') { return `${color.dim(label)}`; } else if (state === 'cancelled') { @@ -207,7 +212,7 @@ export const select = (opts: SelectOptions) => { render() { const title = `${color.gray(S_BAR)}\n${symbol(this.state)} ${opts.message}\n`; - const limitOptions = (options: Option[]): string[] => { + const limitOptions = (options: DisableableOption[]): string[] => { // We clamp to minimum 5 because anything less doesn't make sense UX wise const maxItems = opts.maxItems === undefined ? Infinity : Math.max(opts.maxItems, 5); let slidingWindowLocation = 0; @@ -306,29 +311,31 @@ export const selectKey = (opts: SelectOptions) => { export interface MultiSelectOptions { message: string; - options: Option[]; + options: DisableableOption[]; initialValues?: Value[]; required?: boolean; cursorAt?: Value; } export const multiselect = (opts: MultiSelectOptions) => { const opt = ( - option: Option, + option: DisableableOption, state: 'inactive' | 'active' | 'selected' | 'active-selected' | 'submitted' | 'cancelled' ) => { const label = option.label ?? String(option.value); if (state === 'active') { - return `${color.cyan(S_CHECKBOX_ACTIVE)} ${label} ${ - option.hint ? color.dim(`(${option.hint})`) : '' - }`; + return `${ + option.disabled ? color.dim(S_CHECKBOX_ACTIVE) : color.cyan(S_CHECKBOX_ACTIVE) + } ${label} ${option.hint ? color.dim(`(${option.hint})`) : ''}`; } else if (state === 'selected') { - return `${color.green(S_CHECKBOX_SELECTED)} ${color.dim(label)}`; + return `${ + option.disabled ? color.dim(S_CHECKBOX_SELECTED) : color.green(S_CHECKBOX_SELECTED) + } ${color.dim(label)}`; } else if (state === 'cancelled') { return `${color.strikethrough(color.dim(label))}`; } else if (state === 'active-selected') { - return `${color.green(S_CHECKBOX_SELECTED)} ${label} ${ - option.hint ? color.dim(`(${option.hint})`) : '' - }`; + return `${ + option.disabled ? color.dim(S_CHECKBOX_SELECTED) : color.green(S_CHECKBOX_SELECTED) + } ${label} ${option.hint ? color.dim(`(${option.hint})`) : ''}`; } else if (state === 'submitted') { return `${color.dim(label)}`; } @@ -341,7 +348,7 @@ export const multiselect = (opts: MultiSelectOptions) => { required: opts.required ?? true, cursorAt: opts.cursorAt, validate(selected: Value[]) { - if (this.required && selected.length === 0) + if (this.required && selected.length === 0) { return `Please select at least one option.\n${color.reset( color.dim( `Press ${color.gray(color.bgWhite(color.inverse(' space ')))} to select, ${color.gray( @@ -349,6 +356,20 @@ export const multiselect = (opts: MultiSelectOptions) => { )} to submit` ) )}`; + } + const disabledOptions = opts.options + .map((option) => { + if (selected.includes(option.value) && option.disabled) { + return option.label ?? option.value; + } + return undefined; + }) + .filter(Boolean); + if (disabledOptions.length) { + return `${disabledOptions.join(', ')} ${ + disabledOptions.length > 1 ? 'options are' : 'option is' + } disabled.`; + } }, render() { let title = `${color.gray(S_BAR)}\n${symbol(this.state)} ${opts.message}\n`; From 2740d3f8930bff02b8361f70ef039431e2c7bbeb Mon Sep 17 00:00:00 2001 From: Bruno Rocha Date: Tue, 5 Sep 2023 18:15:11 -0300 Subject: [PATCH 3/3] chore: add changeset --- .changeset/little-horses-sort.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/little-horses-sort.md diff --git a/.changeset/little-horses-sort.md b/.changeset/little-horses-sort.md new file mode 100644 index 00000000..efaf8ab3 --- /dev/null +++ b/.changeset/little-horses-sort.md @@ -0,0 +1,5 @@ +--- +'@clack/prompts': patch +--- + +feat: add `disabled` option to `select` and `multiselect` prompts