Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/early-deers-cough.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@clack/prompts': minor
'@clack/core': minor
---

feat: added `selectableGroups` option for inactive group headers for `GroupMultiSelectPrompt`
15 changes: 12 additions & 3 deletions packages/core/src/prompts/group-multiselect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,20 @@ interface GroupMultiSelectOptions<T extends { value: any }>
initialValues?: T['value'][];
required?: boolean;
cursorAt?: T['value'];
selectableGroups?: boolean;
}
export default class GroupMultiSelectPrompt<T extends { value: any }> extends Prompt {
options: (T & { group: string | boolean })[];
cursor: number = 0;
#selectableGroups: boolean;

getGroupItems(group: string): T[] {
return this.options.filter((o) => o.group === group);
}

isGroupSelected(group: string) {
const items = this.getGroupItems(group);
return items.every((i) => this.value.includes(i.value));
return this.#selectableGroups && items.every((i) => this.value.includes(i.value));
}

private toggleValue() {
Expand All @@ -44,25 +46,32 @@ export default class GroupMultiSelectPrompt<T extends { value: any }> extends Pr
constructor(opts: GroupMultiSelectOptions<T>) {
super(opts, false);
const { options } = opts;
this.#selectableGroups = opts.selectableGroups ?? true;
this.options = Object.entries(options).flatMap(([key, option]) => [
{ value: key, group: true, label: key },
...option.map((opt) => ({ ...opt, group: key })),
]) as any;
this.value = [...(opts.initialValues ?? [])];
this.cursor = Math.max(
this.options.findIndex(({ value }) => value === opts.cursorAt),
0
this.#selectableGroups ? 0 : 1
);

this.on('cursor', (key) => {
switch (key) {
case 'left':
case 'up':
this.cursor = this.cursor === 0 ? this.options.length - 1 : this.cursor - 1;
if (!this.#selectableGroups && this.options[this.cursor].group === true) {
this.cursor = this.cursor === 0 ? this.options.length - 1 : this.cursor - 1;
}
break;
case 'down':
case 'right':
this.cursor = this.cursor === this.options.length - 1 ? 0 : this.cursor + 1;
this.cursor = this.cursor === this.options.length - 1 ? 0 : this.cursor + 1;
if (!this.#selectableGroups && this.options[this.cursor].group === true) {
this.cursor = this.cursor === this.options.length - 1 ? 0 : this.cursor + 1;
}
break;
case 'space':
this.toggleValue();
Expand Down
5 changes: 4 additions & 1 deletion packages/prompts/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -428,8 +428,10 @@ export interface GroupMultiSelectOptions<Value> {
initialValues?: Value[];
required?: boolean;
cursorAt?: Value;
selectableGroups?: boolean;
}
export const groupMultiselect = <Value>(opts: GroupMultiSelectOptions<Value>) => {
const selectableGroups = opts.selectableGroups ?? true;
const opt = (
option: Option<Value>,
state:
Expand Down Expand Up @@ -468,14 +470,15 @@ export const groupMultiselect = <Value>(opts: GroupMultiSelectOptions<Value>) =>
} else if (state === 'submitted') {
return `${color.dim(label)}`;
}
return `${color.dim(prefix)}${color.dim(S_CHECKBOX_INACTIVE)} ${color.dim(label)}`;
return `${color.dim(prefix)}${isItem || selectableGroups ? color.dim(S_CHECKBOX_INACTIVE) : ""} ${color.dim(label)}`;
};

return new GroupMultiSelectPrompt({
options: opts.options,
initialValues: opts.initialValues,
required: opts.required ?? true,
cursorAt: opts.cursorAt,
selectableGroups: selectableGroups,
validate(selected: Value[]) {
if (this.required && selected.length === 0)
return `Please select at least one option.\n${color.reset(
Expand Down