mirror of
https://github.com/LukeHagar/vercel.git
synced 2025-12-10 04:22:12 +00:00
[cli] Improve UX for text input validation (#11388)
Originally, I was focused on fixing this visual bug: <img width="565" alt="Screenshot 2024-04-04 at 9 50 56 AM" src="https://github.com/vercel/vercel/assets/8485687/eb526fae-69d4-4636-ac8b-4cf6a3475c58"> When digging into this issue, I realized there was an opportunity to improve text input validation UX and remove hand-rolled validation code. ## Before: https://github.com/vercel/vercel/assets/8485687/5e264696-8b60-4863-8dc2-1a797f508074 https://github.com/vercel/vercel/assets/8485687/2104603a-331e-4679-9ec2-a4235fad252e ## After: https://github.com/vercel/vercel/assets/8485687/72c51831-2b94-4cbe-b1f6-a5822bf9f1a8 https://github.com/vercel/vercel/assets/8485687/a371c7be-f7f5-4d3a-9451-58e7f572d25b ## Additional considerations [input-root-directory](https://github.com/vercel/vercel/blob/main/packages/cli/src/util/input/input-root-directory.ts#L17) remains an instance of hand-rolled validation since it's trickier to force into inquirer's validation pattern. Also, we have a [hand-rolled text input function](https://github.com/vercel/vercel/blob/main/packages/cli/src/util/input/text.ts#L43) that will get similar benefits when replaced with `client.input.text`.
This commit is contained in:
5
.changeset/orange-houses-rest.md
Normal file
5
.changeset/orange-houses-rest.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
'vercel': minor
|
||||||
|
---
|
||||||
|
|
||||||
|
improve UX for text input validation
|
||||||
@@ -45,10 +45,16 @@ export default async function bisect(client: Client): Promise<number> {
|
|||||||
|
|
||||||
let bad =
|
let bad =
|
||||||
argv['--bad'] ||
|
argv['--bad'] ||
|
||||||
(await prompt(client, `Specify a URL where the bug occurs:`));
|
(await client.input.text({
|
||||||
|
message: `Specify a URL where the bug occurs:`,
|
||||||
|
validate: val => (val ? true : 'A URL must be provided'),
|
||||||
|
}));
|
||||||
let good =
|
let good =
|
||||||
argv['--good'] ||
|
argv['--good'] ||
|
||||||
(await prompt(client, `Specify a URL where the bug does not occur:`));
|
(await client.input.text({
|
||||||
|
message: `Specify a URL where the bug does not occur:`,
|
||||||
|
validate: val => (val ? true : 'A URL must be provided'),
|
||||||
|
}));
|
||||||
let subpath = argv['--path'] || '';
|
let subpath = argv['--path'] || '';
|
||||||
let run = argv['--run'] || '';
|
let run = argv['--run'] || '';
|
||||||
const openEnabled = argv['--open'] || false;
|
const openEnabled = argv['--open'] || false;
|
||||||
@@ -97,10 +103,10 @@ export default async function bisect(client: Client): Promise<number> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!subpath) {
|
if (!subpath) {
|
||||||
subpath = await prompt(
|
subpath = await client.input.text({
|
||||||
client,
|
message: `Specify the URL subpath where the bug occurs:`,
|
||||||
`Specify the URL subpath where the bug occurs:`
|
validate: val => (val ? true : 'A subpath must be provided'),
|
||||||
);
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
output.spinner('Retrieving deployments…');
|
output.spinner('Retrieving deployments…');
|
||||||
@@ -335,15 +341,3 @@ function getCommit(deployment: Deployment) {
|
|||||||
deployment.meta?.bitbucketCommitMessage;
|
deployment.meta?.bitbucketCommitMessage;
|
||||||
return { sha, message };
|
return { sha, message };
|
||||||
}
|
}
|
||||||
|
|
||||||
async function prompt(client: Client, message: string): Promise<string> {
|
|
||||||
// eslint-disable-next-line no-constant-condition
|
|
||||||
while (true) {
|
|
||||||
const val = await client.input.text({ message });
|
|
||||||
if (val) {
|
|
||||||
return val;
|
|
||||||
} else {
|
|
||||||
client.output.error('A value must be specified');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
14
packages/cli/src/commands/env/add.ts
vendored
14
packages/cli/src/commands/env/add.ts
vendored
@@ -63,14 +63,11 @@ export default async function add(
|
|||||||
envTargets.push(envTargetArg);
|
envTargets.push(envTargetArg);
|
||||||
}
|
}
|
||||||
|
|
||||||
while (!envName) {
|
if (!envName) {
|
||||||
envName = await client.input.text({
|
envName = await client.input.text({
|
||||||
message: `What’s the name of the variable?`,
|
message: `What’s the name of the variable?`,
|
||||||
|
validate: val => (val ? true : 'Name cannot be empty'),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!envName) {
|
|
||||||
output.error('Name cannot be empty');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const { envs } = await getEnvRecords(
|
const { envs } = await getEnvRecords(
|
||||||
@@ -100,11 +97,9 @@ export default async function add(
|
|||||||
if (stdInput) {
|
if (stdInput) {
|
||||||
envValue = stdInput;
|
envValue = stdInput;
|
||||||
} else {
|
} else {
|
||||||
const inputValue = await client.input.text({
|
envValue = await client.input.text({
|
||||||
message: `What’s the value of ${envName}?`,
|
message: `What’s the value of ${envName}?`,
|
||||||
});
|
});
|
||||||
|
|
||||||
envValue = inputValue || '';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
while (envTargets.length === 0) {
|
while (envTargets.length === 0) {
|
||||||
@@ -124,10 +119,9 @@ export default async function add(
|
|||||||
envTargets.length === 1 &&
|
envTargets.length === 1 &&
|
||||||
envTargets[0] === 'preview'
|
envTargets[0] === 'preview'
|
||||||
) {
|
) {
|
||||||
const inputValue = await client.input.text({
|
envGitBranch = await client.input.text({
|
||||||
message: `Add ${envName} to which Git branch? (leave empty for all Preview branches)?`,
|
message: `Add ${envName} to which Git branch? (leave empty for all Preview branches)?`,
|
||||||
});
|
});
|
||||||
envGitBranch = inputValue || '';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const type = opts['--sensitive'] ? 'sensitive' : 'encrypted';
|
const type = opts['--sensitive'] ? 'sensitive' : 'encrypted';
|
||||||
|
|||||||
12
packages/cli/src/commands/env/rm.ts
vendored
12
packages/cli/src/commands/env/rm.ts
vendored
@@ -40,17 +40,11 @@ export default async function rm(
|
|||||||
|
|
||||||
let [envName, envTarget, envGitBranch] = args;
|
let [envName, envTarget, envGitBranch] = args;
|
||||||
|
|
||||||
while (!envName) {
|
if (!envName) {
|
||||||
const inputName = await client.input.text({
|
envName = await client.input.text({
|
||||||
message: `What’s the name of the variable?`,
|
message: `What’s the name of the variable?`,
|
||||||
|
validate: val => (val ? true : 'Name cannot be empty'),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!inputName) {
|
|
||||||
output.error(`Name cannot be empty`);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
envName = inputName;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isValidEnvTarget(envTarget)) {
|
if (!isValidEnvTarget(envTarget)) {
|
||||||
|
|||||||
@@ -75,64 +75,37 @@ export default async function inputProject(
|
|||||||
|
|
||||||
if (shouldLinkProject) {
|
if (shouldLinkProject) {
|
||||||
// user wants to link a project
|
// user wants to link a project
|
||||||
let project: Project | ProjectNotFound | null = null;
|
let toLink: Project;
|
||||||
|
await client.input.text({
|
||||||
while (!project || project instanceof ProjectNotFound) {
|
|
||||||
const projectName = await client.input.text({
|
|
||||||
message: 'What’s the name of your existing project?',
|
message: 'What’s the name of your existing project?',
|
||||||
});
|
validate: async val => {
|
||||||
|
if (!val) {
|
||||||
if (!projectName) {
|
return 'Project name cannot be empty';
|
||||||
output.error(`Project name cannot be empty`);
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
const project = await getProjectByIdOrName(client, val, org.id);
|
||||||
output.spinner('Verifying project name…', 1000);
|
|
||||||
try {
|
|
||||||
project = await getProjectByIdOrName(client, projectName, org.id);
|
|
||||||
} finally {
|
|
||||||
output.stopSpinner();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (project instanceof ProjectNotFound) {
|
if (project instanceof ProjectNotFound) {
|
||||||
output.error(`Project not found`);
|
return 'Project not found';
|
||||||
}
|
}
|
||||||
}
|
toLink = project;
|
||||||
|
return true;
|
||||||
return project;
|
},
|
||||||
|
});
|
||||||
|
return toLink!;
|
||||||
}
|
}
|
||||||
|
|
||||||
// user wants to create a new project
|
// user wants to create a new project
|
||||||
let newProjectName: string | null = null;
|
return await client.input.text({
|
||||||
|
|
||||||
while (!newProjectName) {
|
|
||||||
newProjectName = await client.input.text({
|
|
||||||
message: `What’s your project’s name?`,
|
message: `What’s your project’s name?`,
|
||||||
default: !detectedProject ? slugifiedName : undefined,
|
default: !detectedProject ? slugifiedName : undefined,
|
||||||
|
validate: async val => {
|
||||||
|
if (!val) {
|
||||||
|
return 'Project name cannot be empty';
|
||||||
|
}
|
||||||
|
const project = await getProjectByIdOrName(client, val, org.id);
|
||||||
|
if (!(project instanceof ProjectNotFound)) {
|
||||||
|
return 'Project already exists';
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!newProjectName) {
|
|
||||||
output.error(`Project name cannot be empty`);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
output.spinner('Verifying project name…', 1000);
|
|
||||||
let existingProject: Project | ProjectNotFound;
|
|
||||||
try {
|
|
||||||
existingProject = await getProjectByIdOrName(
|
|
||||||
client,
|
|
||||||
newProjectName,
|
|
||||||
org.id
|
|
||||||
);
|
|
||||||
} finally {
|
|
||||||
output.stopSpinner();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (existingProject && !(existingProject instanceof ProjectNotFound)) {
|
|
||||||
output.print(`Project already exists`);
|
|
||||||
newProjectName = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return newProjectName;
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user