Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
3 changes: 2 additions & 1 deletion src/lib/interact-for-server-selection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ export async function interactForServerSelection() {

const { server } = await prompt([
{
type: 'select',
// Searchable, as selecting a device or a command is.
type: 'autocomplete',
name: 'server',
message: 'Select a server:',
choices: servers.map((server) => ({ title: server, value: server })),
Expand Down
4 changes: 3 additions & 1 deletion src/lib/interact-for-workspace-id.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ export const interactForWorkspaceId = async (personalAccessToken?: string) => {
)
const { workspaceId } = await prompt({
name: 'workspaceId',
type: 'select',
// Searchable, as selecting a device or a command is: an account may have
// more workspaces than fit on a screen.
type: 'autocomplete',
message: 'Select a workspace:',
choices: workspaces.map((workspace: any) => ({
title: workspace.name,
Expand Down
34 changes: 34 additions & 0 deletions src/lib/util/prompt.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { expect, test } from 'vitest'

import { searchChoices } from './prompt.js'

const workspaces = [
{ title: 'Sandbox', description: 'ws_1' },
{ title: 'Production Europe', description: 'ws_2' },
{ title: 'Production US', description: 'ws_3' },
]

test('searchChoices: matches every term against the title and description', async () => {
await expect(searchChoices('prod us', workspaces)).resolves.toEqual([
workspaces[2],
])
await expect(searchChoices('WS_1', workspaces)).resolves.toEqual([
workspaces[0],
])
await expect(searchChoices('nope', workspaces)).resolves.toEqual([])
})

test('searchChoices: matches any part of a name, not only its start', async () => {
const servers = [
{ title: 'http://localhost:3020' },
{ title: 'https://connect.getseam.com' },
{ title: 'https://fakeseamconnect.seam.vc' },
]

await expect(searchChoices('fake', servers)).resolves.toEqual([servers[2]])
})

test('searchChoices: offers every choice until something is typed', async () => {
await expect(searchChoices('', workspaces)).resolves.toEqual(workspaces)
await expect(searchChoices(' ', workspaces)).resolves.toEqual(workspaces)
})
38 changes: 37 additions & 1 deletion src/lib/util/prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,43 @@ export const prompt = async <T extends string = string>(
const questionList = Array.isArray(questions) ? questions : [questions]

return await prompts(
questionList.map((question) => ({ ...question, stdout: process.stderr })),
questionList.map((question) => ({
...question,
// Search a list by any part of a name, rather than only by what it
// starts with, which is all prompts does for itself.
...(question.type === 'autocomplete' && question.suggest == null
? { suggest: searchChoices }
: {}),
stdout: process.stderr,
})),
options,
)
}

export interface SearchableChoice {
title?: string | undefined
description?: string | undefined
}

/**
* Filter choices by every whitespace separated term of the input, matched
* case insensitively against the title and the description.
*/
export const searchChoices = async <Choice extends SearchableChoice>(
input: string,
choices: Choice[],
): Promise<Choice[]> => {
const terms = input
.toLowerCase()
.split(/\s+/)
.filter((term) => term.length > 0)

if (terms.length === 0) return choices

return choices.filter((choice) => {
const searchable = `${choice.title ?? ''} ${choice.description ?? ''}`
.toLowerCase()
.trim()
return terms.every((term) => searchable.includes(term))
})
}
Loading