mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
feat: implement rich multi-selector (#19201)
Fixes: https://github.com/coder/coder/issues/19182
This commit is contained in:
@@ -38,15 +38,16 @@ func RichParameter(inv *serpent.Invocation, templateVersionParameter codersdk.Te
|
||||
// Move the cursor up a single line for nicer display!
|
||||
_, _ = fmt.Fprint(inv.Stdout, "\033[1A")
|
||||
|
||||
var options []string
|
||||
err = json.Unmarshal([]byte(templateVersionParameter.DefaultValue), &options)
|
||||
var defaults []string
|
||||
err = json.Unmarshal([]byte(templateVersionParameter.DefaultValue), &defaults)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
values, err := MultiSelect(inv, MultiSelectOptions{
|
||||
Options: options,
|
||||
Defaults: options,
|
||||
values, err := RichMultiSelect(inv, RichMultiSelectOptions{
|
||||
Options: templateVersionParameter.Options,
|
||||
Defaults: defaults,
|
||||
EnableCustomInput: templateVersionParameter.FormType == "tag-select",
|
||||
})
|
||||
if err == nil {
|
||||
v, err := json.Marshal(&values)
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"slices"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
@@ -299,6 +300,73 @@ func (m selectModel) filteredOptions() []string {
|
||||
return options
|
||||
}
|
||||
|
||||
type RichMultiSelectOptions struct {
|
||||
Message string
|
||||
Options []codersdk.TemplateVersionParameterOption
|
||||
Defaults []string
|
||||
EnableCustomInput bool
|
||||
}
|
||||
|
||||
func RichMultiSelect(inv *serpent.Invocation, richOptions RichMultiSelectOptions) ([]string, error) {
|
||||
var opts []string
|
||||
var defaultOpts []string
|
||||
|
||||
asLine := func(option codersdk.TemplateVersionParameterOption) string {
|
||||
line := option.Name
|
||||
if len(option.Description) > 0 {
|
||||
line += ": " + option.Description
|
||||
}
|
||||
return line
|
||||
}
|
||||
|
||||
var predefinedOpts []string
|
||||
for i, option := range richOptions.Options {
|
||||
opts = append(opts, asLine(option)) // Some options may have description defined.
|
||||
|
||||
// Check if option is selected by default
|
||||
if slices.Contains(richOptions.Defaults, option.Value) {
|
||||
defaultOpts = append(defaultOpts, opts[i])
|
||||
predefinedOpts = append(predefinedOpts, option.Value)
|
||||
}
|
||||
}
|
||||
|
||||
// Check if "defaults" contains extra/custom options, user could select them.
|
||||
for _, def := range richOptions.Defaults {
|
||||
if !slices.Contains(predefinedOpts, def) {
|
||||
opts = append(opts, def)
|
||||
defaultOpts = append(defaultOpts, def)
|
||||
}
|
||||
}
|
||||
|
||||
selected, err := MultiSelect(inv, MultiSelectOptions{
|
||||
Message: richOptions.Message,
|
||||
Options: opts,
|
||||
Defaults: defaultOpts,
|
||||
EnableCustomInput: richOptions.EnableCustomInput,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Check selected option, convert descriptions (line) to values
|
||||
var results []string
|
||||
for _, sel := range selected {
|
||||
custom := true
|
||||
for i, option := range richOptions.Options {
|
||||
if asLine(option) == sel {
|
||||
results = append(results, richOptions.Options[i].Value)
|
||||
custom = false
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if custom {
|
||||
results = append(results, sel)
|
||||
}
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
|
||||
type MultiSelectOptions struct {
|
||||
Message string
|
||||
Options []string
|
||||
|
||||
+105
-56
@@ -52,15 +52,8 @@ func TestRichSelect(t *testing.T) {
|
||||
go func() {
|
||||
resp, err := newRichSelect(ptty, cliui.RichSelectOptions{
|
||||
Options: []codersdk.TemplateVersionParameterOption{
|
||||
{
|
||||
Name: "A-Name",
|
||||
Value: "A-Value",
|
||||
Description: "A-Description.",
|
||||
}, {
|
||||
Name: "B-Name",
|
||||
Value: "B-Value",
|
||||
Description: "B-Description.",
|
||||
},
|
||||
{Name: "A-Name", Value: "A-Value", Description: "A-Description."},
|
||||
{Name: "B-Name", Value: "B-Value", Description: "B-Description."},
|
||||
},
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
@@ -86,44 +79,119 @@ func newRichSelect(ptty *ptytest.PTY, opts cliui.RichSelectOptions) (string, err
|
||||
return value, inv.Run()
|
||||
}
|
||||
|
||||
func TestMultiSelect(t *testing.T) {
|
||||
func TestRichMultiSelect(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("MultiSelect", func(t *testing.T) {
|
||||
items := []string{"aaa", "bbb", "ccc"}
|
||||
|
||||
t.Parallel()
|
||||
ptty := ptytest.New(t)
|
||||
msgChan := make(chan []string)
|
||||
go func() {
|
||||
resp, err := newMultiSelect(ptty, items)
|
||||
assert.NoError(t, err)
|
||||
msgChan <- resp
|
||||
}()
|
||||
require.Equal(t, items, <-msgChan)
|
||||
})
|
||||
tests := []struct {
|
||||
name string
|
||||
options []codersdk.TemplateVersionParameterOption
|
||||
defaults []string
|
||||
allowCustom bool
|
||||
want []string
|
||||
}{
|
||||
{
|
||||
name: "Predefined",
|
||||
options: []codersdk.TemplateVersionParameterOption{
|
||||
{Name: "AAA", Description: "This is AAA", Value: "aaa"},
|
||||
{Name: "BBB", Description: "This is BBB", Value: "bbb"},
|
||||
{Name: "CCC", Description: "This is CCC", Value: "ccc"},
|
||||
},
|
||||
defaults: []string{"bbb", "ccc"},
|
||||
allowCustom: false,
|
||||
want: []string{"bbb", "ccc"},
|
||||
},
|
||||
{
|
||||
name: "Custom",
|
||||
options: []codersdk.TemplateVersionParameterOption{
|
||||
{Name: "AAA", Description: "This is AAA", Value: "aaa"},
|
||||
{Name: "BBB", Description: "This is BBB", Value: "bbb"},
|
||||
{Name: "CCC", Description: "This is CCC", Value: "ccc"},
|
||||
},
|
||||
defaults: []string{"aaa", "bbb"},
|
||||
allowCustom: true,
|
||||
want: []string{"aaa", "bbb"},
|
||||
},
|
||||
}
|
||||
|
||||
t.Run("MultiSelectWithCustomInput", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
items := []string{"Code", "Chairs", "Whale", "Diamond", "Carrot"}
|
||||
ptty := ptytest.New(t)
|
||||
msgChan := make(chan []string)
|
||||
go func() {
|
||||
resp, err := newMultiSelectWithCustomInput(ptty, items)
|
||||
assert.NoError(t, err)
|
||||
msgChan <- resp
|
||||
}()
|
||||
require.Equal(t, items, <-msgChan)
|
||||
})
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var selectedItems []string
|
||||
var err error
|
||||
cmd := &serpent.Command{
|
||||
Handler: func(inv *serpent.Invocation) error {
|
||||
selectedItems, err = cliui.RichMultiSelect(inv, cliui.RichMultiSelectOptions{
|
||||
Options: tt.options,
|
||||
Defaults: tt.defaults,
|
||||
EnableCustomInput: tt.allowCustom,
|
||||
})
|
||||
return err
|
||||
},
|
||||
}
|
||||
|
||||
doneChan := make(chan struct{})
|
||||
go func() {
|
||||
defer close(doneChan)
|
||||
err := cmd.Invoke().Run()
|
||||
assert.NoError(t, err)
|
||||
}()
|
||||
<-doneChan
|
||||
|
||||
require.Equal(t, tt.want, selectedItems)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func newMultiSelectWithCustomInput(ptty *ptytest.PTY, items []string) ([]string, error) {
|
||||
func TestMultiSelect(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
items []string
|
||||
allowCustom bool
|
||||
want []string
|
||||
}{
|
||||
{
|
||||
name: "MultiSelect",
|
||||
items: []string{"aaa", "bbb", "ccc"},
|
||||
allowCustom: false,
|
||||
want: []string{"aaa", "bbb", "ccc"},
|
||||
},
|
||||
{
|
||||
name: "MultiSelectWithCustomInput",
|
||||
items: []string{"Code", "Chairs", "Whale", "Diamond", "Carrot"},
|
||||
allowCustom: true,
|
||||
want: []string{"Code", "Chairs", "Whale", "Diamond", "Carrot"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ptty := ptytest.New(t)
|
||||
msgChan := make(chan []string)
|
||||
|
||||
go func() {
|
||||
resp, err := newMultiSelect(ptty, tt.items, tt.allowCustom)
|
||||
assert.NoError(t, err)
|
||||
msgChan <- resp
|
||||
}()
|
||||
|
||||
require.Equal(t, tt.want, <-msgChan)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func newMultiSelect(pty *ptytest.PTY, items []string, custom bool) ([]string, error) {
|
||||
var values []string
|
||||
cmd := &serpent.Command{
|
||||
Handler: func(inv *serpent.Invocation) error {
|
||||
selectedItems, err := cliui.MultiSelect(inv, cliui.MultiSelectOptions{
|
||||
Options: items,
|
||||
Defaults: items,
|
||||
EnableCustomInput: true,
|
||||
EnableCustomInput: custom,
|
||||
})
|
||||
if err == nil {
|
||||
values = selectedItems
|
||||
@@ -132,25 +200,6 @@ func newMultiSelectWithCustomInput(ptty *ptytest.PTY, items []string) ([]string,
|
||||
},
|
||||
}
|
||||
inv := cmd.Invoke()
|
||||
ptty.Attach(inv)
|
||||
return values, inv.Run()
|
||||
}
|
||||
|
||||
func newMultiSelect(ptty *ptytest.PTY, items []string) ([]string, error) {
|
||||
var values []string
|
||||
cmd := &serpent.Command{
|
||||
Handler: func(inv *serpent.Invocation) error {
|
||||
selectedItems, err := cliui.MultiSelect(inv, cliui.MultiSelectOptions{
|
||||
Options: items,
|
||||
Defaults: items,
|
||||
})
|
||||
if err == nil {
|
||||
values = selectedItems
|
||||
}
|
||||
return err
|
||||
},
|
||||
}
|
||||
inv := cmd.Invoke()
|
||||
ptty.Attach(inv)
|
||||
pty.Attach(inv)
|
||||
return values, inv.Run()
|
||||
}
|
||||
|
||||
@@ -174,6 +174,20 @@ func (RootCmd) promptExample() *serpent.Command {
|
||||
_, _ = fmt.Fprintf(inv.Stdout, "%q are nice choices.\n", strings.Join(multiSelectValues, ", "))
|
||||
return multiSelectError
|
||||
}, useThingsOption, enableCustomInputOption),
|
||||
promptCmd("rich-multi-select", func(inv *serpent.Invocation) error {
|
||||
if len(multiSelectValues) == 0 {
|
||||
multiSelectValues, multiSelectError = cliui.MultiSelect(inv, cliui.MultiSelectOptions{
|
||||
Message: "Select some things:",
|
||||
Options: []string{
|
||||
"Apples", "Plums", "Grapes", "Oranges", "Bananas",
|
||||
},
|
||||
Defaults: []string{"Grapes", "Plums"},
|
||||
EnableCustomInput: enableCustomInput,
|
||||
})
|
||||
}
|
||||
_, _ = fmt.Fprintf(inv.Stdout, "%q are nice choices.\n", strings.Join(multiSelectValues, ", "))
|
||||
return multiSelectError
|
||||
}, useThingsOption, enableCustomInputOption),
|
||||
promptCmd("rich-parameter", func(inv *serpent.Invocation) error {
|
||||
value, err := cliui.RichSelect(inv, cliui.RichSelectOptions{
|
||||
Options: []codersdk.TemplateVersionParameterOption{
|
||||
|
||||
Reference in New Issue
Block a user