Skip to content

Commit

Permalink
Fix(@inquirer/expand): [Typescript] Make the value type a generic
Browse files Browse the repository at this point in the history
  • Loading branch information
SBoudrias committed Sep 1, 2024
1 parent f5ded86 commit 7fb8aea
Showing 1 changed file with 97 additions and 87 deletions.
184 changes: 97 additions & 87 deletions packages/expand/src/index.mts
Original file line number Diff line number Diff line change
Expand Up @@ -11,31 +11,37 @@ import {
import type { PartialDeep } from '@inquirer/type';
import colors from 'yoctocolors-cjs';

type Choice =
| { key: string; name: string }
| { key: string; value: string }
| { key: string; name: string; value: string };
type Choice<Value> =
| { key: string; value: Value }
| { key: string; name: string; value: Value };

type NormalizedChoice = {
value: string;
type NormalizedChoice<Value> = {
value: Value;
name: string;
key: string;
};

type ExpandConfig = {
type ExpandConfig<
Value,
ChoicesObject = readonly { key: string; name: string }[] | readonly Choice<Value>[],
> = {
message: string;
choices: ReadonlyArray<Choice>;
choices: ChoicesObject extends readonly { key: string; name: string }[]
? ChoicesObject
: readonly Choice<Value>[];
default?: string;
expanded?: boolean;
theme?: PartialDeep<Theme>;
};

function normalizeChoices(choices: readonly Choice[]): NormalizedChoice[] {
function normalizeChoices<Value>(
choices: readonly { key: string; name: string }[] | readonly Choice<Value>[],
): NormalizedChoice<Value>[] {
return choices.map((choice) => {
const name: string = 'name' in choice ? choice.name : String(choice.value);
const value = 'value' in choice ? choice.value : name;
return {
value,
value: value as Value,
name,
key: choice.key.toLowerCase(),
};
Expand All @@ -48,91 +54,95 @@ const helpChoice = {
value: undefined,
};

export default createPrompt<string, ExpandConfig>((config, done) => {
const { default: defaultKey = 'h' } = config;
const choices = useMemo(() => normalizeChoices(config.choices), [config.choices]);
const [status, setStatus] = useState<string>('pending');
const [value, setValue] = useState<string>('');
const [expanded, setExpanded] = useState<boolean>(config.expanded ?? false);
const [errorMsg, setError] = useState<string>();
const theme = makeTheme(config.theme);
const prefix = usePrefix({ theme });

useKeypress((event, rl) => {
if (isEnterKey(event)) {
const answer = (value || defaultKey).toLowerCase();
if (answer === 'h' && !expanded) {
setExpanded(true);
} else {
const selectedChoice = choices.find(({ key }) => key === answer);
if (selectedChoice) {
setStatus('done');
// Set the value as we might've selected the default one.
setValue(answer);
done(selectedChoice.value);
} else if (value === '') {
setError('Please input a value');
export default createPrompt(
<Value,>(config: ExpandConfig<Value>, done: (value: Value) => void) => {
const { default: defaultKey = 'h' } = config;
const choices = useMemo(() => normalizeChoices(config.choices), [config.choices]);
const [status, setStatus] = useState<string>('pending');
const [value, setValue] = useState<string>('');
const [expanded, setExpanded] = useState<boolean>(config.expanded ?? false);
const [errorMsg, setError] = useState<string>();
const theme = makeTheme(config.theme);
const prefix = usePrefix({ theme });

useKeypress((event, rl) => {
if (isEnterKey(event)) {
const answer = (value || defaultKey).toLowerCase();
if (answer === 'h' && !expanded) {
setExpanded(true);
} else {
setError(`"${colors.red(value)}" isn't an available option`);
const selectedChoice = choices.find(({ key }) => key === answer);
if (selectedChoice) {
setStatus('done');
// Set the value as we might've selected the default one.
setValue(answer);
done(selectedChoice.value);
} else if (value === '') {
setError('Please input a value');
} else {
setError(`"${colors.red(value)}" isn't an available option`);
}
}
} else {
setValue(rl.line);
setError(undefined);
}
} else {
setValue(rl.line);
setError(undefined);
}
});
});

const message = theme.style.message(config.message);
const message = theme.style.message(config.message);

if (status === 'done') {
// If the prompt is done, it's safe to assume there is a selected value.
const selectedChoice = choices.find(({ key }) => key === value) as NormalizedChoice;
return `${prefix} ${message} ${theme.style.answer(selectedChoice.name)}`;
}

const allChoices = expanded ? choices : [...choices, helpChoice];

// Collapsed display style
let longChoices = '';
let shortChoices = allChoices
.map((choice) => {
if (choice.key === defaultKey) {
return choice.key.toUpperCase();
}
if (status === 'done') {
// If the prompt is done, it's safe to assume there is a selected value.
const selectedChoice = choices.find(
({ key }) => key === value,
) as NormalizedChoice<Value>;
return `${prefix} ${message} ${theme.style.answer(selectedChoice.name)}`;
}

return choice.key;
})
.join('');
shortChoices = ` ${theme.style.defaultAnswer(shortChoices)}`;
const allChoices = expanded ? choices : [...choices, helpChoice];

// Expanded display style
if (expanded) {
shortChoices = '';
longChoices = allChoices
// Collapsed display style
let longChoices = '';
let shortChoices = allChoices
.map((choice) => {
const line = ` ${choice.key}) ${choice.name}`;
if (choice.key === value.toLowerCase()) {
return theme.style.highlight(line);
if (choice.key === defaultKey) {
return choice.key.toUpperCase();
}

return line;
return choice.key;
})
.join('\n');
}

let helpTip = '';
const currentOption = allChoices.find(({ key }) => key === value.toLowerCase());
if (currentOption) {
helpTip = `${colors.cyan('>>')} ${currentOption.name}`;
}

let error = '';
if (errorMsg) {
error = theme.style.error(errorMsg);
}

return [
`${prefix} ${message}${shortChoices} ${value}`,
[longChoices, helpTip, error].filter(Boolean).join('\n'),
];
});
.join('');
shortChoices = ` ${theme.style.defaultAnswer(shortChoices)}`;

// Expanded display style
if (expanded) {
shortChoices = '';
longChoices = allChoices
.map((choice) => {
const line = ` ${choice.key}) ${choice.name}`;
if (choice.key === value.toLowerCase()) {
return theme.style.highlight(line);
}

return line;
})
.join('\n');
}

let helpTip = '';
const currentOption = allChoices.find(({ key }) => key === value.toLowerCase());
if (currentOption) {
helpTip = `${colors.cyan('>>')} ${currentOption.name}`;
}

let error = '';
if (errorMsg) {
error = theme.style.error(errorMsg);
}

return [
`${prefix} ${message}${shortChoices} ${value}`,
[longChoices, helpTip, error].filter(Boolean).join('\n'),
];
},
);

0 comments on commit 7fb8aea

Please sign in to comment.