MultiSelectWithSearch initial implementation
Initial implementation of MultiSelectWithSearch: - Implement by survey and accessible prompters. They use the same internal func under the hood. - Implement in `gh preview prompter` for initial testing and demonstration - Implement interface changes across the codebase and mocks to satisfy compiler. - Implement tests for new MultiSelectWithSearch prompter
This commit is contained in:
parent
be1e21095c
commit
0beb74bf72
6 changed files with 478 additions and 31 deletions
|
|
@ -224,6 +224,167 @@ func TestAccessiblePrompter(t *testing.T) {
|
|||
assert.Equal(t, []int{1}, multiSelectValues)
|
||||
})
|
||||
|
||||
t.Run("MultiSelectWithSearch - basic flow", func(t *testing.T) {
|
||||
console := newTestVirtualTerminal(t)
|
||||
p := newTestAccessiblePrompter(t, console)
|
||||
persistentOptions := []string{"persistent-option-1"}
|
||||
searchFunc := func(input string) ([]string, []string, int, error) {
|
||||
var searchResultKeys []string
|
||||
var searchResultLabels []string
|
||||
|
||||
// Initial search with no input
|
||||
if input == "" {
|
||||
moreResults := 2
|
||||
searchResultKeys = []string{"initial-result-1", "initial-result-2"}
|
||||
searchResultLabels = []string{"Initial Result Label 1", "Initial Result Label 2"}
|
||||
return searchResultKeys, searchResultLabels, moreResults, nil
|
||||
}
|
||||
|
||||
// Subsequent search with input
|
||||
moreResults := 0
|
||||
searchResultKeys = []string{"search-result-1", "search-result-2"}
|
||||
searchResultLabels = []string{"Search Result Label 1", "Search Result Label 2"}
|
||||
return searchResultKeys, searchResultLabels, moreResults, nil
|
||||
}
|
||||
|
||||
go func() {
|
||||
// Wait for prompt to appear
|
||||
_, err := console.ExpectString("Select an option \r\n")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Select the search option, which will always be the first option
|
||||
_, err = console.SendLine("1")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Submit search
|
||||
_, err = console.SendLine("0")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Wait for the search prompt to appear
|
||||
_, err = console.ExpectString("Search for an option")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Enter some search text to trigger the search
|
||||
_, err = console.SendLine("search text")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Wait for the multiselect prompt to re-appear after search
|
||||
_, err = console.ExpectString("Select an option \r\n")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Select the first search result
|
||||
_, err = console.SendLine("2")
|
||||
require.NoError(t, err)
|
||||
|
||||
// This confirms selections
|
||||
_, err = console.SendLine("0")
|
||||
require.NoError(t, err)
|
||||
}()
|
||||
multiSelectValues, err := p.MultiSelectWithSearch("Select an option", "Search for an option", []string{}, persistentOptions, searchFunc)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, []string{"search-result-1"}, multiSelectValues)
|
||||
})
|
||||
|
||||
t.Run("MultiSelectWithSearch - defaults are pre-selected", func(t *testing.T) {
|
||||
console := newTestVirtualTerminal(t)
|
||||
p := newTestAccessiblePrompter(t, console)
|
||||
initialSearchResultKeys := []string{"initial-result-1"}
|
||||
initialSearchResultLabels := []string{"Initial Result Label 1"}
|
||||
defaultOptions := initialSearchResultKeys
|
||||
searchFunc := func(input string) ([]string, []string, int, error) {
|
||||
// Initial search with no input
|
||||
if input == "" {
|
||||
moreResults := 2
|
||||
return initialSearchResultKeys, initialSearchResultLabels, moreResults, nil
|
||||
}
|
||||
|
||||
// No search selected, so this should fail the test.
|
||||
t.FailNow()
|
||||
return nil, nil, 0, nil
|
||||
}
|
||||
|
||||
go func() {
|
||||
// Wait for prompt to appear
|
||||
_, err := console.ExpectString("Select an option (default: Initial Result Label 1) \r\n")
|
||||
require.NoError(t, err)
|
||||
|
||||
// This confirms default selections
|
||||
_, err = console.SendLine("0")
|
||||
require.NoError(t, err)
|
||||
}()
|
||||
multiSelectValues, err := p.MultiSelectWithSearch("Select an option", "Search for an option", defaultOptions, initialSearchResultKeys, searchFunc)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, defaultOptions, multiSelectValues)
|
||||
})
|
||||
|
||||
t.Run("MultiSelectWithSearch - selected options persist between searches", func(t *testing.T) {
|
||||
console := newTestVirtualTerminal(t)
|
||||
p := newTestAccessiblePrompter(t, console)
|
||||
initialSearchResultKeys := []string{"initial-result-1"}
|
||||
initialSearchResultLabels := []string{"Initial Result Label 1"}
|
||||
moreResultKeys := []string{"more-result-1"}
|
||||
moreResultLabels := []string{"More Result Label 1"}
|
||||
|
||||
searchFunc := func(input string) ([]string, []string, int, error) {
|
||||
// Initial search with no input
|
||||
if input == "" {
|
||||
moreResults := 2
|
||||
return initialSearchResultKeys, initialSearchResultLabels, moreResults, nil
|
||||
}
|
||||
|
||||
// Subsequent search with input "more"
|
||||
if input == "more" {
|
||||
return moreResultKeys, moreResultLabels, 0, nil
|
||||
}
|
||||
|
||||
// No other searches expected
|
||||
t.FailNow()
|
||||
return nil, nil, 0, nil
|
||||
}
|
||||
|
||||
go func() {
|
||||
// Wait for prompt to appear
|
||||
_, err := console.ExpectString("Select an option \r\n")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Select one of our initial search results
|
||||
_, err = console.SendLine("2")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Select to search
|
||||
_, err = console.SendLine("1")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Submit the search selection
|
||||
_, err = console.SendLine("0")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Wait for the search prompt to appear
|
||||
_, err = console.ExpectString("Search for an option")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Enter some search text to trigger the search
|
||||
_, err = console.SendLine("more")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Wait for the multiselect prompt to re-appear after search
|
||||
_, err = console.ExpectString("Select up to")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Select the new option from the new search results
|
||||
_, err = console.SendLine("3")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Submit selections
|
||||
_, err = console.SendLine("0")
|
||||
require.NoError(t, err)
|
||||
}()
|
||||
multiSelectValues, err := p.MultiSelectWithSearch("Select an option", "Search for an option", []string{}, []string{}, searchFunc)
|
||||
require.NoError(t, err)
|
||||
expectedValues := append(initialSearchResultKeys, moreResultKeys...)
|
||||
assert.Equal(t, expectedValues, multiSelectValues)
|
||||
})
|
||||
|
||||
t.Run("Input", func(t *testing.T) {
|
||||
console := newTestVirtualTerminal(t)
|
||||
p := newTestAccessiblePrompter(t, console)
|
||||
|
|
@ -642,6 +803,7 @@ func newTestVirtualTerminal(t *testing.T) *expect.Console {
|
|||
failOnExpectError(t),
|
||||
failOnSendError(t),
|
||||
expect.WithDefaultTimeout(time.Second),
|
||||
// expect.WithLogger(log.New(os.Stdout, "", 0)),
|
||||
}
|
||||
|
||||
console, err := expect.NewConsole(consoleOpts...)
|
||||
|
|
|
|||
|
|
@ -21,6 +21,15 @@ type Prompter interface {
|
|||
Select(prompt string, defaultValue string, options []string) (int, error)
|
||||
// MultiSelect prompts the user to select one or more options from a list of options.
|
||||
MultiSelect(prompt string, defaults []string, options []string) ([]int, error)
|
||||
// MultiSelectWithSearch is MultiSelect with an added search option to the list,
|
||||
// prompting the user for text input to filter the options via the searchFunc.
|
||||
// Items selected in the search are persisted in the list after subsequent searches.
|
||||
// Items passed in persistentOptions are always shown in the list, even when not selected.
|
||||
// Unlike MultiSelect, MultiselectWithSearch returns the selected option strings,
|
||||
// not their indices, since the list of options is dynamic.
|
||||
// The searchFunc args and return values are: func(query) (map[keys]labels, moreResultsCount, searchError)
|
||||
// Where the selected keys are eventually returned by MultiSelectWithSearch and the labels are what is shown to the user in the prompt.
|
||||
MultiSelectWithSearch(prompt, searchPrompt string, defaults []string, persistentOptions []string, searchFunc func(string) ([]string, []string, int, error)) ([]string, error)
|
||||
// Input prompts the user to enter a string value.
|
||||
Input(prompt string, defaultValue string) (string, error)
|
||||
// Password prompts the user to enter a password.
|
||||
|
|
@ -320,6 +329,10 @@ func (p *accessiblePrompter) MarkdownEditor(prompt, defaultValue string, blankAl
|
|||
return text, nil
|
||||
}
|
||||
|
||||
func (p *accessiblePrompter) MultiSelectWithSearch(prompt, searchPrompt string, defaultValues, persistentValues []string, searchFunc func(string) ([]string, []string, int, error)) ([]string, error) {
|
||||
return multiSelectWithSearch(p, prompt, searchPrompt, defaultValues, persistentValues, searchFunc)
|
||||
}
|
||||
|
||||
type surveyPrompter struct {
|
||||
prompter *ghPrompter.Prompter
|
||||
stdin ghPrompter.FileReader
|
||||
|
|
@ -336,6 +349,147 @@ func (p *surveyPrompter) MultiSelect(prompt string, defaultValues, options []str
|
|||
return p.prompter.MultiSelect(prompt, defaultValues, options)
|
||||
}
|
||||
|
||||
func (p *surveyPrompter) MultiSelectWithSearch(prompt string, searchPrompt string, defaultValues, persistentValues []string, searchFunc func(string) ([]string, []string, int, error)) ([]string, error) {
|
||||
return multiSelectWithSearch(p, prompt, searchPrompt, defaultValues, persistentValues, searchFunc)
|
||||
}
|
||||
|
||||
func multiSelectWithSearch(p Prompter, prompt, searchPrompt string, defaultValues, persistentValues []string, searchFunc func(string) ([]string, []string, int, error)) ([]string, error) {
|
||||
selectedOptions := defaultValues
|
||||
|
||||
// The optionKeyLabels map is used to uniquely identify optionKeyLabels
|
||||
// and provide optional display labels.
|
||||
optionKeyLabels := make(map[string]string)
|
||||
for _, k := range selectedOptions {
|
||||
optionKeyLabels[k] = k
|
||||
}
|
||||
|
||||
searchResultKeys, searchResultLabels, moreResults, err := searchFunc("")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to search: %w", err)
|
||||
}
|
||||
|
||||
for i, k := range searchResultKeys {
|
||||
optionKeyLabels[k] = searchResultLabels[i]
|
||||
}
|
||||
|
||||
for {
|
||||
// Build dynamic option list -> search sentinel, selections, search results, persistent options.
|
||||
optionKeys := make([]string, 0, 1+len(selectedOptions)+len(searchResultKeys)+len(persistentValues))
|
||||
optionLabels := make([]string, 0, len(optionKeys))
|
||||
|
||||
// 1. Search sentinel.
|
||||
optionKeys = append(optionKeys, "")
|
||||
if moreResults > 0 {
|
||||
optionLabels = append(optionLabels, fmt.Sprintf("Search (%d more)", moreResults))
|
||||
} else {
|
||||
optionLabels = append(optionLabels, "Search")
|
||||
}
|
||||
|
||||
// 2. Selections
|
||||
for _, k := range selectedOptions {
|
||||
l := optionKeyLabels[k]
|
||||
|
||||
if l == "" {
|
||||
l = k
|
||||
}
|
||||
|
||||
optionKeys = append(optionKeys, k)
|
||||
optionLabels = append(optionLabels, l)
|
||||
}
|
||||
|
||||
// 3. Search results
|
||||
for _, k := range searchResultKeys {
|
||||
// It's already selected or persistent, if we add here we'll have duplicates.
|
||||
if slices.Contains(selectedOptions, k) || slices.Contains(persistentValues, k) {
|
||||
continue
|
||||
}
|
||||
|
||||
l := optionKeyLabels[k]
|
||||
if l == "" {
|
||||
l = k
|
||||
}
|
||||
optionKeys = append(optionKeys, k)
|
||||
optionLabels = append(optionLabels, l)
|
||||
}
|
||||
|
||||
// 4. Persistent options
|
||||
for _, k := range persistentValues {
|
||||
if slices.Contains(selectedOptions, k) {
|
||||
continue
|
||||
}
|
||||
|
||||
l := optionKeyLabels[k]
|
||||
if l == "" {
|
||||
l = k
|
||||
}
|
||||
|
||||
optionKeys = append(optionKeys, k)
|
||||
optionLabels = append(optionLabels, l)
|
||||
}
|
||||
|
||||
selectedOptionLabels := make([]string, len(selectedOptions))
|
||||
for i, k := range selectedOptions {
|
||||
l := optionKeyLabels[k]
|
||||
if l == "" {
|
||||
l = k
|
||||
}
|
||||
selectedOptionLabels[i] = l
|
||||
}
|
||||
|
||||
selectedIdxs, err := p.MultiSelect(prompt, selectedOptionLabels, optionLabels)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pickedSearch := false
|
||||
var newSelectedOptions []string
|
||||
for _, idx := range selectedIdxs {
|
||||
if idx == 0 { // Search sentinel selected
|
||||
pickedSearch = true
|
||||
continue
|
||||
}
|
||||
|
||||
if idx < 0 || idx >= len(optionKeys) {
|
||||
continue
|
||||
}
|
||||
|
||||
key := optionKeys[idx]
|
||||
if key == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
newSelectedOptions = append(newSelectedOptions, key)
|
||||
}
|
||||
|
||||
selectedOptions = newSelectedOptions
|
||||
for _, k := range selectedOptions {
|
||||
if _, ok := optionKeyLabels[k]; !ok {
|
||||
optionKeyLabels[k] = k
|
||||
}
|
||||
}
|
||||
|
||||
if pickedSearch {
|
||||
query, err := p.Input(searchPrompt, "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
searchResultKeys, searchResultLabels, moreResults, err = searchFunc(query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for i, k := range searchResultKeys {
|
||||
optionKeyLabels[k] = searchResultLabels[i]
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
return selectedOptions, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (p *surveyPrompter) Input(prompt, defaultValue string) (string, error) {
|
||||
return p.prompter.Input(prompt, defaultValue)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,6 +38,9 @@ var _ Prompter = &PrompterMock{}
|
|||
// MultiSelectFunc: func(prompt string, defaults []string, options []string) ([]int, error) {
|
||||
// panic("mock out the MultiSelect method")
|
||||
// },
|
||||
// MultiSelectWithSearchFunc: func(prompt string, searchPrompt string, defaults []string, persistentOptions []string, searchFunc func(string) ([]string, []string, int, error)) ([]string, error) {
|
||||
// panic("mock out the MultiSelectWithSearch method")
|
||||
// },
|
||||
// PasswordFunc: func(prompt string) (string, error) {
|
||||
// panic("mock out the Password method")
|
||||
// },
|
||||
|
|
@ -72,6 +75,9 @@ type PrompterMock struct {
|
|||
// MultiSelectFunc mocks the MultiSelect method.
|
||||
MultiSelectFunc func(prompt string, defaults []string, options []string) ([]int, error)
|
||||
|
||||
// MultiSelectWithSearchFunc mocks the MultiSelectWithSearch method.
|
||||
MultiSelectWithSearchFunc func(prompt string, searchPrompt string, defaults []string, persistentOptions []string, searchFunc func(string) ([]string, []string, int, error)) ([]string, error)
|
||||
|
||||
// PasswordFunc mocks the Password method.
|
||||
PasswordFunc func(prompt string) (string, error)
|
||||
|
||||
|
|
@ -123,6 +129,19 @@ type PrompterMock struct {
|
|||
// Options is the options argument value.
|
||||
Options []string
|
||||
}
|
||||
// MultiSelectWithSearch holds details about calls to the MultiSelectWithSearch method.
|
||||
MultiSelectWithSearch []struct {
|
||||
// Prompt is the prompt argument value.
|
||||
Prompt string
|
||||
// SearchPrompt is the searchPrompt argument value.
|
||||
SearchPrompt string
|
||||
// Defaults is the defaults argument value.
|
||||
Defaults []string
|
||||
// PersistentOptions is the persistentOptions argument value.
|
||||
PersistentOptions []string
|
||||
// SearchFunc is the searchFunc argument value.
|
||||
SearchFunc func(string) ([]string, []string, int, error)
|
||||
}
|
||||
// Password holds details about calls to the Password method.
|
||||
Password []struct {
|
||||
// Prompt is the prompt argument value.
|
||||
|
|
@ -138,15 +157,16 @@ type PrompterMock struct {
|
|||
Options []string
|
||||
}
|
||||
}
|
||||
lockAuthToken sync.RWMutex
|
||||
lockConfirm sync.RWMutex
|
||||
lockConfirmDeletion sync.RWMutex
|
||||
lockInput sync.RWMutex
|
||||
lockInputHostname sync.RWMutex
|
||||
lockMarkdownEditor sync.RWMutex
|
||||
lockMultiSelect sync.RWMutex
|
||||
lockPassword sync.RWMutex
|
||||
lockSelect sync.RWMutex
|
||||
lockAuthToken sync.RWMutex
|
||||
lockConfirm sync.RWMutex
|
||||
lockConfirmDeletion sync.RWMutex
|
||||
lockInput sync.RWMutex
|
||||
lockInputHostname sync.RWMutex
|
||||
lockMarkdownEditor sync.RWMutex
|
||||
lockMultiSelect sync.RWMutex
|
||||
lockMultiSelectWithSearch sync.RWMutex
|
||||
lockPassword sync.RWMutex
|
||||
lockSelect sync.RWMutex
|
||||
}
|
||||
|
||||
// AuthToken calls AuthTokenFunc.
|
||||
|
|
@ -387,6 +407,54 @@ func (mock *PrompterMock) MultiSelectCalls() []struct {
|
|||
return calls
|
||||
}
|
||||
|
||||
// MultiSelectWithSearch calls MultiSelectWithSearchFunc.
|
||||
func (mock *PrompterMock) MultiSelectWithSearch(prompt string, searchPrompt string, defaults []string, persistentOptions []string, searchFunc func(string) ([]string, []string, int, error)) ([]string, error) {
|
||||
if mock.MultiSelectWithSearchFunc == nil {
|
||||
panic("PrompterMock.MultiSelectWithSearchFunc: method is nil but Prompter.MultiSelectWithSearch was just called")
|
||||
}
|
||||
callInfo := struct {
|
||||
Prompt string
|
||||
SearchPrompt string
|
||||
Defaults []string
|
||||
PersistentOptions []string
|
||||
SearchFunc func(string) ([]string, []string, int, error)
|
||||
}{
|
||||
Prompt: prompt,
|
||||
SearchPrompt: searchPrompt,
|
||||
Defaults: defaults,
|
||||
PersistentOptions: persistentOptions,
|
||||
SearchFunc: searchFunc,
|
||||
}
|
||||
mock.lockMultiSelectWithSearch.Lock()
|
||||
mock.calls.MultiSelectWithSearch = append(mock.calls.MultiSelectWithSearch, callInfo)
|
||||
mock.lockMultiSelectWithSearch.Unlock()
|
||||
return mock.MultiSelectWithSearchFunc(prompt, searchPrompt, defaults, persistentOptions, searchFunc)
|
||||
}
|
||||
|
||||
// MultiSelectWithSearchCalls gets all the calls that were made to MultiSelectWithSearch.
|
||||
// Check the length with:
|
||||
//
|
||||
// len(mockedPrompter.MultiSelectWithSearchCalls())
|
||||
func (mock *PrompterMock) MultiSelectWithSearchCalls() []struct {
|
||||
Prompt string
|
||||
SearchPrompt string
|
||||
Defaults []string
|
||||
PersistentOptions []string
|
||||
SearchFunc func(string) ([]string, []string, int, error)
|
||||
} {
|
||||
var calls []struct {
|
||||
Prompt string
|
||||
SearchPrompt string
|
||||
Defaults []string
|
||||
PersistentOptions []string
|
||||
SearchFunc func(string) ([]string, []string, int, error)
|
||||
}
|
||||
mock.lockMultiSelectWithSearch.RLock()
|
||||
calls = mock.calls.MultiSelectWithSearch
|
||||
mock.lockMultiSelectWithSearch.RUnlock()
|
||||
return calls
|
||||
}
|
||||
|
||||
// Password calls PasswordFunc.
|
||||
func (mock *PrompterMock) Password(prompt string) (string, error) {
|
||||
if mock.PasswordFunc == nil {
|
||||
|
|
|
|||
|
|
@ -25,10 +25,11 @@ func NewMockPrompter(t *testing.T) *MockPrompter {
|
|||
type MockPrompter struct {
|
||||
t *testing.T
|
||||
ghPrompter.PrompterMock
|
||||
authTokenStubs []authTokenStub
|
||||
confirmDeletionStubs []confirmDeletionStub
|
||||
inputHostnameStubs []inputHostnameStub
|
||||
markdownEditorStubs []markdownEditorStub
|
||||
authTokenStubs []authTokenStub
|
||||
confirmDeletionStubs []confirmDeletionStub
|
||||
inputHostnameStubs []inputHostnameStub
|
||||
markdownEditorStubs []markdownEditorStub
|
||||
multiSelectWithSearchStubs []multiSelectWithSearchStub
|
||||
}
|
||||
|
||||
type authTokenStub struct {
|
||||
|
|
@ -49,6 +50,12 @@ type markdownEditorStub struct {
|
|||
fn func(string, string, bool) (string, error)
|
||||
}
|
||||
|
||||
type multiSelectWithSearchStub struct {
|
||||
prompt string
|
||||
searchPrompt string
|
||||
fn func(string, string, []string, []string) ([]string, error)
|
||||
}
|
||||
|
||||
func (m *MockPrompter) AuthToken() (string, error) {
|
||||
var s authTokenStub
|
||||
if len(m.authTokenStubs) == 0 {
|
||||
|
|
@ -92,6 +99,16 @@ func (m *MockPrompter) MarkdownEditor(prompt, defaultValue string, blankAllowed
|
|||
return s.fn(prompt, defaultValue, blankAllowed)
|
||||
}
|
||||
|
||||
func (m *MockPrompter) MultiSelectWithSearch(prompt, searchPrompt string, defaults []string, persistentOptions []string, searchFunc func(string) ([]string, []string, int, error)) ([]string, error) {
|
||||
var s multiSelectWithSearchStub
|
||||
if len(m.multiSelectWithSearchStubs) == 0 {
|
||||
return nil, NoSuchPromptErr(prompt)
|
||||
}
|
||||
s = m.multiSelectWithSearchStubs[0]
|
||||
m.multiSelectWithSearchStubs = m.multiSelectWithSearchStubs[1:len(m.multiSelectWithSearchStubs)]
|
||||
return s.fn(prompt, searchPrompt, defaults, persistentOptions)
|
||||
}
|
||||
|
||||
func (m *MockPrompter) RegisterAuthToken(stub func() (string, error)) {
|
||||
m.authTokenStubs = append(m.authTokenStubs, authTokenStub{fn: stub})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ type Prompt interface {
|
|||
MarkdownEditor(prompt string, defaultValue string, blankAllowed bool) (string, error)
|
||||
Confirm(prompt string, defaultValue bool) (bool, error)
|
||||
MultiSelect(prompt string, defaults []string, options []string) ([]int, error)
|
||||
MultiSelectWithSearch(prompt, searchPrompt string, defaults []string, persistentOptions []string, searchFunc func(string) ([]string, []string, int, error)) ([]string, error)
|
||||
}
|
||||
|
||||
func ConfirmIssueSubmission(p Prompt, allowPreview bool, allowMetadata bool) (Action, error) {
|
||||
|
|
@ -207,6 +208,7 @@ func MetadataSurvey(p Prompt, io *iostreams.IOStreams, baseRepo ghrepo.Interface
|
|||
// Populate the list of selectable assignees and their default selections.
|
||||
// This logic maps the default assignees from `state` to the corresponding actors or users
|
||||
// so that the correct display names are preselected in the prompt.
|
||||
// TODO: KW21 This will need to go away since we're going to dynamically load assignees via search.
|
||||
var assignees []string
|
||||
var assigneesDefault []string
|
||||
if state.ActorAssignees {
|
||||
|
|
@ -264,6 +266,9 @@ func MetadataSurvey(p Prompt, io *iostreams.IOStreams, baseRepo ghrepo.Interface
|
|||
fmt.Fprintln(io.ErrOut, "warning: no available reviewers")
|
||||
}
|
||||
}
|
||||
// TODO: KW21 This will need to change to use MultiSelectWithSearch once it's implemented.
|
||||
// MultiSelectWithSearch will return the selected strings directly instead of indices,
|
||||
// so the logic here will need to be updated accordingly.
|
||||
if isChosen("Assignees") {
|
||||
if len(assignees) > 0 {
|
||||
selected, err := p.MultiSelect("Assignees", assigneesDefault, assignees)
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package prompter
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/MakeNowJust/heredoc"
|
||||
"github.com/cli/cli/v2/internal/gh"
|
||||
|
|
@ -25,32 +26,35 @@ func NewCmdPrompter(f *cmdutil.Factory, runF func(*prompterOptions) error) *cobr
|
|||
}
|
||||
|
||||
const (
|
||||
selectPrompt = "select"
|
||||
multiSelectPrompt = "multi-select"
|
||||
inputPrompt = "input"
|
||||
passwordPrompt = "password"
|
||||
confirmPrompt = "confirm"
|
||||
authTokenPrompt = "auth-token"
|
||||
confirmDeletionPrompt = "confirm-deletion"
|
||||
inputHostnamePrompt = "input-hostname"
|
||||
markdownEditorPrompt = "markdown-editor"
|
||||
selectPrompt = "select"
|
||||
multiSelectPrompt = "multi-select"
|
||||
multiSelectWithSearchPrompt = "multi-select-with-search"
|
||||
inputPrompt = "input"
|
||||
passwordPrompt = "password"
|
||||
confirmPrompt = "confirm"
|
||||
authTokenPrompt = "auth-token"
|
||||
confirmDeletionPrompt = "confirm-deletion"
|
||||
inputHostnamePrompt = "input-hostname"
|
||||
markdownEditorPrompt = "markdown-editor"
|
||||
)
|
||||
|
||||
prompterTypeFuncMap := map[string]func(prompter.Prompter, *iostreams.IOStreams) error{
|
||||
selectPrompt: runSelect,
|
||||
multiSelectPrompt: runMultiSelect,
|
||||
inputPrompt: runInput,
|
||||
passwordPrompt: runPassword,
|
||||
confirmPrompt: runConfirm,
|
||||
authTokenPrompt: runAuthToken,
|
||||
confirmDeletionPrompt: runConfirmDeletion,
|
||||
inputHostnamePrompt: runInputHostname,
|
||||
markdownEditorPrompt: runMarkdownEditor,
|
||||
selectPrompt: runSelect,
|
||||
multiSelectPrompt: runMultiSelect,
|
||||
multiSelectWithSearchPrompt: runMultiSelectWithSearch,
|
||||
inputPrompt: runInput,
|
||||
passwordPrompt: runPassword,
|
||||
confirmPrompt: runConfirm,
|
||||
authTokenPrompt: runAuthToken,
|
||||
confirmDeletionPrompt: runConfirmDeletion,
|
||||
inputHostnamePrompt: runInputHostname,
|
||||
markdownEditorPrompt: runMarkdownEditor,
|
||||
}
|
||||
|
||||
allPromptsOrder := []string{
|
||||
selectPrompt,
|
||||
multiSelectPrompt,
|
||||
multiSelectWithSearchPrompt,
|
||||
inputPrompt,
|
||||
passwordPrompt,
|
||||
confirmPrompt,
|
||||
|
|
@ -70,6 +74,7 @@ func NewCmdPrompter(f *cmdutil.Factory, runF func(*prompterOptions) error) *cobr
|
|||
Available prompt types:
|
||||
- select
|
||||
- multi-select
|
||||
- multi-select-with-search
|
||||
- input
|
||||
- password
|
||||
- confirm
|
||||
|
|
@ -149,6 +154,42 @@ func runMultiSelect(p prompter.Prompter, io *iostreams.IOStreams) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func runMultiSelectWithSearch(p prompter.Prompter, io *iostreams.IOStreams) error {
|
||||
fmt.Fprintln(io.Out, "Demonstrating Multi Select With Search")
|
||||
persistentOptions := []string{"persistent-option-1"}
|
||||
searchFunc := func(input string) ([]string, []string, int, error) {
|
||||
var searchResultKeys []string
|
||||
var searchResultLabels []string
|
||||
|
||||
if input == "" {
|
||||
moreResults := 2 // Indicate that there are more results available
|
||||
searchResultKeys = []string{"initial-result-1", "initial-result-2"}
|
||||
searchResultLabels = []string{"Initial Result Label 1", "Initial Result Label 2"}
|
||||
return searchResultKeys, searchResultLabels, moreResults, nil
|
||||
}
|
||||
|
||||
// In a real implementation, this function would perform a search based on the input.
|
||||
// Here, we return a static set of options for demonstration purposes.
|
||||
moreResults := 0
|
||||
searchResultKeys = []string{"search-result-1", "search-result-2"}
|
||||
searchResultLabels = []string{"Search Result Label 1", "Search Result Label 2"}
|
||||
return searchResultKeys, searchResultLabels, moreResults, nil
|
||||
}
|
||||
|
||||
selections, err := p.MultiSelectWithSearch("Select an option", "Search for an option", []string{}, persistentOptions, searchFunc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(selections) == 0 {
|
||||
fmt.Fprintln(io.Out, "No options selected.")
|
||||
return nil
|
||||
}
|
||||
|
||||
fmt.Fprintf(io.Out, "Selected options: %s\n", strings.Join(selections, ", "))
|
||||
return nil
|
||||
}
|
||||
|
||||
func runInput(p prompter.Prompter, io *iostreams.IOStreams) error {
|
||||
fmt.Fprintln(io.Out, "Demonstrating Text Input")
|
||||
text, err := p.Input("Favorite meal?", "Breakfast")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue