Skip to content

Commit b3032b8

Browse files
committed
Feat(editor): Add option to control behavior on validation failure. Fix #907
1 parent 2942263 commit b3032b8

File tree

3 files changed

+131
-10
lines changed

3 files changed

+131
-10
lines changed

packages/editor/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,9 +87,12 @@ type Theme = {
8787
help: (text: string) => string;
8888
key: (text: string) => string;
8989
};
90+
validationFailureMode: 'keep' | 'clear';
9091
};
9192
```
9293

94+
`validationFailureMode` defines the behavior of the prompt when the value submitted is invalid. By default, we'll keep the value allowing the user to edit it. When the theme option is set to `clear`, we'll remove and reset to the default value or empty string.
95+
9396
# License
9497

9598
Copyright (c) 2023 Simon Boudrias (twitter: [@vaxilart](https://twitter.com/Vaxilart))<br/>

packages/editor/editor.test.ts

Lines changed: 111 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,9 @@ describe('editor prompt', () => {
3232
expect(editAsync).not.toHaveBeenCalled();
3333

3434
events.keypress('enter');
35-
expect(editAsync).toHaveBeenCalledWith('', expect.any(Function), { postfix: '.txt' });
35+
expect(editAsync).toHaveBeenLastCalledWith('', expect.any(Function), {
36+
postfix: '.txt',
37+
});
3638

3739
await editorAction(undefined, 'value from editor');
3840

@@ -45,7 +47,9 @@ describe('editor prompt', () => {
4547
message: 'Add a description',
4648
waitForUseInput: false,
4749
});
48-
expect(editAsync).toHaveBeenCalledWith('', expect.any(Function), { postfix: '.txt' });
50+
expect(editAsync).toHaveBeenLastCalledWith('', expect.any(Function), {
51+
postfix: '.txt',
52+
});
4953

5054
await editorAction(undefined, 'value from editor');
5155

@@ -63,9 +67,13 @@ describe('editor prompt', () => {
6367
expect(editAsync).not.toHaveBeenCalled();
6468

6569
events.keypress('enter');
66-
expect(editAsync).toHaveBeenCalledWith('default description', expect.any(Function), {
67-
postfix: '.md',
68-
});
70+
expect(editAsync).toHaveBeenLastCalledWith(
71+
'default description',
72+
expect.any(Function),
73+
{
74+
postfix: '.md',
75+
},
76+
);
6977

7078
await editorAction(undefined, 'value from editor');
7179

@@ -85,7 +93,7 @@ describe('editor prompt', () => {
8593
expect(editAsync).not.toHaveBeenCalled();
8694

8795
events.keypress('enter');
88-
expect(editAsync).toHaveBeenCalledWith('', expect.any(Function), {
96+
expect(editAsync).toHaveBeenLastCalledWith('', expect.any(Function), {
8997
postfix: '.md',
9098
dir: '/tmp',
9199
});
@@ -129,6 +137,10 @@ describe('editor prompt', () => {
129137
expect(editAsync).toHaveBeenCalledOnce();
130138
events.keypress('enter');
131139
expect(editAsync).toHaveBeenCalledTimes(2);
140+
// Previous answer is passed in the second time for editing
141+
expect(editAsync).toHaveBeenLastCalledWith('3', expect.any(Function), {
142+
postfix: '.txt',
143+
});
132144

133145
// Test user defined error message
134146
await editorAction(undefined, '2');
@@ -145,6 +157,99 @@ describe('editor prompt', () => {
145157
expect(getScreen()).toMatchInlineSnapshot(`"✔ Add a description"`);
146158
});
147159

160+
it('clear value on failed validation', async () => {
161+
const { answer, events, getScreen } = await render(editor, {
162+
message: 'Add a description',
163+
validate: (value: string) => {
164+
switch (value) {
165+
case '1': {
166+
return true;
167+
}
168+
case '2': {
169+
return '"2" is not an allowed value';
170+
}
171+
default: {
172+
return false;
173+
}
174+
}
175+
},
176+
theme: {
177+
validationFailureMode: 'clear',
178+
},
179+
});
180+
181+
expect(editAsync).not.toHaveBeenCalled();
182+
events.keypress('enter');
183+
184+
expect(editAsync).toHaveBeenCalledOnce();
185+
expect(editAsync).toHaveBeenLastCalledWith('', expect.any(Function), {
186+
postfix: '.txt',
187+
});
188+
await editorAction(undefined, 'foo bar');
189+
expect(getScreen()).toMatchInlineSnapshot(`
190+
"? Add a description Press <enter> to launch your preferred editor.
191+
> You must provide a valid value"
192+
`);
193+
194+
events.keypress('enter');
195+
expect(editAsync).toHaveBeenCalledTimes(2);
196+
// Because we clear, the second call goes back to an empty string
197+
expect(editAsync).toHaveBeenLastCalledWith('', expect.any(Function), {
198+
postfix: '.txt',
199+
});
200+
201+
await editorAction(undefined, '1');
202+
await expect(answer).resolves.toEqual('1');
203+
expect(getScreen()).toMatchInlineSnapshot(`"✔ Add a description"`);
204+
});
205+
206+
it('goes back to default value on failed validation', async () => {
207+
const { answer, events, getScreen } = await render(editor, {
208+
message: 'Add a description',
209+
default: 'default value',
210+
validate: (value: string) => {
211+
switch (value) {
212+
case '1': {
213+
return true;
214+
}
215+
case '2': {
216+
return '"2" is not an allowed value';
217+
}
218+
default: {
219+
return false;
220+
}
221+
}
222+
},
223+
theme: {
224+
validationFailureMode: 'clear',
225+
},
226+
});
227+
228+
expect(editAsync).not.toHaveBeenCalled();
229+
events.keypress('enter');
230+
231+
expect(editAsync).toHaveBeenCalledOnce();
232+
expect(editAsync).toHaveBeenLastCalledWith('default value', expect.any(Function), {
233+
postfix: '.txt',
234+
});
235+
await editorAction(undefined, 'foo bar');
236+
expect(getScreen()).toMatchInlineSnapshot(`
237+
"? Add a description Press <enter> to launch your preferred editor.
238+
> You must provide a valid value"
239+
`);
240+
241+
events.keypress('enter');
242+
expect(editAsync).toHaveBeenCalledTimes(2);
243+
// Because we clear, the second call goes back to the default value
244+
expect(editAsync).toHaveBeenLastCalledWith('default value', expect.any(Function), {
245+
postfix: '.txt',
246+
});
247+
248+
await editorAction(undefined, '1');
249+
await expect(answer).resolves.toEqual('1');
250+
expect(getScreen()).toMatchInlineSnapshot(`"✔ Add a description"`);
251+
});
252+
148253
it('surfaces external-editor errors', async () => {
149254
const { answer, events, getScreen } = await render(editor, {
150255
message: 'Add a description',

packages/editor/src/index.ts

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,22 @@ import {
1313
} from '@inquirer/core';
1414
import type { PartialDeep, InquirerReadline } from '@inquirer/type';
1515

16+
type EditorTheme = {
17+
validationFailureMode: 'keep' | 'clear';
18+
};
19+
20+
const editorTheme: EditorTheme = {
21+
validationFailureMode: 'keep',
22+
};
23+
1624
type EditorConfig = {
1725
message: string;
1826
default?: string;
1927
postfix?: string;
2028
waitForUseInput?: boolean;
2129
validate?: (value: string) => boolean | string | Promise<string | boolean>;
2230
file?: IFileOptions;
23-
theme?: PartialDeep<Theme>;
31+
theme?: PartialDeep<Theme<EditorTheme>>;
2432
};
2533

2634
export default createPrompt<string, EditorConfig>((config, done) => {
@@ -29,10 +37,10 @@ export default createPrompt<string, EditorConfig>((config, done) => {
2937
file: { postfix = config.postfix ?? '.txt', ...fileProps } = {},
3038
validate = () => true,
3139
} = config;
32-
const theme = makeTheme(config.theme);
40+
const theme = makeTheme<EditorTheme>(editorTheme, config.theme);
3341

3442
const [status, setStatus] = useState<Status>('idle');
35-
const [value, setValue] = useState<string>(config.default || '');
43+
const [value = '', setValue] = useState<string | undefined>(config.default);
3644
const [errorMsg, setError] = useState<string>();
3745

3846
const prefix = usePrefix({ status, theme });
@@ -54,7 +62,12 @@ export default createPrompt<string, EditorConfig>((config, done) => {
5462
setStatus('done');
5563
done(answer);
5664
} else {
57-
setValue(answer);
65+
if (theme.validationFailureMode === 'clear') {
66+
setValue(config.default);
67+
} else {
68+
setValue(answer);
69+
}
70+
5871
setError(isValid || 'You must provide a valid value');
5972
setStatus('idle');
6073
}

0 commit comments

Comments
 (0)