Skip to content

Commit 5bcf1b6

Browse files
committed
feat(prompts): support maxItems in groupMultiselect
1 parent ab58ce5 commit 5bcf1b6

3 files changed

Lines changed: 783 additions & 98 deletions

File tree

packages/prompts/src/group-multi-select.ts

Lines changed: 55 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,14 @@ import {
99
S_CHECKBOX_SELECTED,
1010
symbol,
1111
} from './common.js';
12+
import { limitOptions } from './limit-options.js';
1213
import type { Option } from './select.js';
1314

1415
export interface GroupMultiSelectOptions<Value> extends CommonOptions {
1516
message: string;
1617
options: Record<string, Option<Value>[]>;
1718
initialValues?: Value[];
19+
maxItems?: number;
1820
required?: boolean;
1921
cursorAt?: Value;
2022
selectableGroups?: boolean;
@@ -42,8 +44,7 @@ export const groupMultiselect = <Value>(opts: GroupMultiSelectOptions<Value>) =>
4244
const prefix = isItem ? (selectableGroups ? `${isLast ? S_BAR_END : S_BAR} ` : ' ') : '';
4345
let spacingPrefix = '';
4446
if (groupSpacing > 0 && !isItem) {
45-
const spacingPrefixText = `\n${styleText('cyan', S_BAR)}`;
46-
spacingPrefix = `${spacingPrefixText.repeat(groupSpacing - 1)}${spacingPrefixText} `;
47+
spacingPrefix = '\n'.repeat(groupSpacing);
4748
}
4849

4950
if (state === 'active') {
@@ -108,6 +109,30 @@ export const groupMultiselect = <Value>(opts: GroupMultiSelectOptions<Value>) =>
108109
const title = `${hasGuide ? `${styleText('gray', S_BAR)}\n` : ''}${symbol(this.state)} ${opts.message}\n`;
109110
const value = this.value ?? [];
110111

112+
const styleOption = (
113+
option: Option<Value> & { group: string | boolean },
114+
active: boolean
115+
) => {
116+
const options = this.options;
117+
const selected =
118+
value.includes(option.value) ||
119+
(option.group === true && this.isGroupSelected(`${option.value}`));
120+
const groupActive =
121+
!active &&
122+
typeof option.group === 'string' &&
123+
this.options[this.cursor].value === option.group;
124+
if (groupActive) {
125+
return opt(option, selected ? 'group-active-selected' : 'group-active', options);
126+
}
127+
if (active && selected) {
128+
return opt(option, 'active-selected', options);
129+
}
130+
if (selected) {
131+
return opt(option, 'selected', options);
132+
}
133+
return opt(option, active ? 'active' : 'inactive', options);
134+
};
135+
111136
switch (this.state) {
112137
case 'submit': {
113138
const selectedOptions = this.options
@@ -127,6 +152,7 @@ export const groupMultiselect = <Value>(opts: GroupMultiSelectOptions<Value>) =>
127152
}`;
128153
}
129154
case 'error': {
155+
const guidePrefix = hasGuide ? `${styleText('yellow', S_BAR)} ` : '';
130156
const footer = this.error
131157
.split('\n')
132158
.map((ln, i) =>
@@ -135,60 +161,35 @@ export const groupMultiselect = <Value>(opts: GroupMultiSelectOptions<Value>) =>
135161
: ` ${ln}`
136162
)
137163
.join('\n');
138-
return `${title}${hasGuide ? `${styleText('yellow', S_BAR)} ` : ''}${this.options
139-
.map((option, i, options) => {
140-
const selected =
141-
value.includes(option.value) ||
142-
(option.group === true && this.isGroupSelected(`${option.value}`));
143-
const active = i === this.cursor;
144-
const groupActive =
145-
!active &&
146-
typeof option.group === 'string' &&
147-
this.options[this.cursor].value === option.group;
148-
if (groupActive) {
149-
return opt(option, selected ? 'group-active-selected' : 'group-active', options);
150-
}
151-
if (active && selected) {
152-
return opt(option, 'active-selected', options);
153-
}
154-
if (selected) {
155-
return opt(option, 'selected', options);
156-
}
157-
return opt(option, active ? 'active' : 'inactive', options);
158-
})
159-
.join(`\n${hasGuide ? `${styleText('yellow', S_BAR)} ` : ''}`)}\n${footer}\n`;
164+
// Calculate rowPadding: title lines + footer lines (error message + trailing newline)
165+
const titleLineCount = title.split('\n').length;
166+
const footerLineCount = footer.split('\n').length + 1; // footer + trailing newline
167+
const optionsText = limitOptions({
168+
output: opts.output,
169+
options: this.options,
170+
cursor: this.cursor,
171+
maxItems: opts.maxItems,
172+
columnPadding: guidePrefix.length,
173+
rowPadding: titleLineCount + footerLineCount,
174+
style: styleOption,
175+
}).join(`\n${guidePrefix}`);
176+
return `${title}${guidePrefix}${optionsText}\n${footer}\n`;
160177
}
161178
default: {
162-
const optionsText = this.options
163-
.map((option, i, options) => {
164-
const selected =
165-
value.includes(option.value) ||
166-
(option.group === true && this.isGroupSelected(`${option.value}`));
167-
const active = i === this.cursor;
168-
const groupActive =
169-
!active &&
170-
typeof option.group === 'string' &&
171-
this.options[this.cursor].value === option.group;
172-
let optionText = '';
173-
if (groupActive) {
174-
optionText = opt(
175-
option,
176-
selected ? 'group-active-selected' : 'group-active',
177-
options
178-
);
179-
} else if (active && selected) {
180-
optionText = opt(option, 'active-selected', options);
181-
} else if (selected) {
182-
optionText = opt(option, 'selected', options);
183-
} else {
184-
optionText = opt(option, active ? 'active' : 'inactive', options);
185-
}
186-
const prefix = i !== 0 && !optionText.startsWith('\n') ? ' ' : '';
187-
return `${prefix}${optionText}`;
188-
})
189-
.join(`\n${hasGuide ? styleText('cyan', S_BAR) : ''}`);
190-
const optionsPrefix = optionsText.startsWith('\n') ? '' : ' ';
191-
return `${title}${hasGuide ? styleText('cyan', S_BAR) : ''}${optionsPrefix}${optionsText}\n${
179+
const guidePrefix = hasGuide ? `${styleText('cyan', S_BAR)} ` : '';
180+
// Calculate rowPadding: title lines + footer lines (S_BAR_END + trailing newline)
181+
const titleLineCount = title.split('\n').length;
182+
const footerLineCount = (hasGuide ? 1 : 0) + 1; // guide line + trailing newline
183+
const optionsText = limitOptions({
184+
output: opts.output,
185+
options: this.options,
186+
cursor: this.cursor,
187+
maxItems: opts.maxItems,
188+
columnPadding: guidePrefix.length,
189+
rowPadding: titleLineCount + footerLineCount,
190+
style: styleOption,
191+
}).join(`\n${guidePrefix}`);
192+
return `${title}${guidePrefix}${optionsText}\n${
192193
hasGuide ? styleText('cyan', S_BAR_END) : ''
193194
}\n`;
194195
}

0 commit comments

Comments
 (0)