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
32 changes: 15 additions & 17 deletions cmd/project/create_samples.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"sort"
"strings"

"github.com/slackapi/slack-cli/internal/experiment"
"github.com/slackapi/slack-cli/internal/iostreams"
"github.com/slackapi/slack-cli/internal/pkg/create"
"github.com/slackapi/slack-cli/internal/shared"
Expand Down Expand Up @@ -64,14 +65,26 @@ func promptSampleSelection(ctx context.Context, clients *shared.ClientFactory, s
}

sortedRepos := sortRepos(filteredRepos)
selectOptions := createSelectOptions(sortedRepos)
selectOptions := make([]string, len(sortedRepos))
for i, r := range sortedRepos {
if !clients.Config.WithExperimentOn(experiment.Charm) {
selectOptions[i] = fmt.Sprint(i+1, ". ", r.Name)
} else {
selectOptions[i] = r.Name
}
}

var selectedTemplate string
selection, err = clients.IO.SelectPrompt(ctx, "Select a sample to build upon:", selectOptions, iostreams.SelectPromptConfig{
Description: func(value string, index int) string {
return sortedRepos[index].Description + "\n https://github.com/" + sortedRepos[index].FullName
desc := sortedRepos[index].Description
if !clients.Config.WithExperimentOn(experiment.Charm) {
desc += "\n https://github.com/" + sortedRepos[index].FullName
}
return desc
},
Flag: clients.Config.Flags.Lookup("template"),
Help: fmt.Sprintf("Guided tutorials can be found at %s", style.LinkText("https://docs.slack.dev/samples")),
PageSize: 4, // Supports standard terminal height (24 rows)
Required: true,
Template: embedPromptSamplesTmpl,
Expand Down Expand Up @@ -127,18 +140,3 @@ func sortRepos(sampleRepos []create.GithubRepo) []create.GithubRepo {
})
return sortedRepos
}

// createSelectOptions takes in a list of repositories
// and returns an array of strings, each value being
// equal to the repository name (ie, deno-starter-template)
// and prepended with a number for a prompt visual aid
func createSelectOptions(filteredRepos []create.GithubRepo) []string {
// Create a slice of repository names to use as
// the primary item selection in the prompt
selectOptions := make([]string, 0)
for i, f := range filteredRepos {
selectOption := fmt.Sprint(i+1, ". ", f.Name)
selectOptions = append(selectOptions, selectOption)
}
return selectOptions
}
6 changes: 0 additions & 6 deletions cmd/project/create_samples_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -174,9 +174,3 @@ func TestSamples_SortRepos(t *testing.T) {
assert.Equal(t, sortedRepos[3].StargazersCount, 0, "Expected sortedRepos[3].StargazersCount to equal 0")
assert.Equal(t, sortedRepos[3].Description, "This is a new sample")
}

func TestSamples_CreateSelectOptions(t *testing.T) {
selectOptions := createSelectOptions(mockGitHubRepos)
assert.Equal(t, len(selectOptions), 4, "Expected selectOptions length to be 4")
assert.Contains(t, selectOptions[0], mockGitHubRepos[0].Name, "Expected selectOptions[0] to contain mockGitHubRepos[0].Name")
}
6 changes: 0 additions & 6 deletions cmd/project/create_template.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,9 +132,6 @@ func promptTemplateSelection(cmd *cobra.Command, clients *shared.ClientFactory,

// Prompt to choose a category
selection, err := clients.IO.SelectPrompt(ctx, promptForCategory, titlesForCategory, iostreams.SelectPromptConfig{
Description: func(value string, index int) string {
return optionsForCategory[index].Description
},
Comment on lines -135 to -137
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

๐Ÿ”ญ note: These descriptions aren't used for create selections so it's removed with these changes!

๐Ÿชฌ question: I notice samples does use descriptions and this causes outputs between "values" and "descriptions" different and am curious if we'd prefer to match that here?

func getSelectionOptions(clients *shared.ClientFactory, categoryID string) []promptObject {
if strings.TrimSpace(categoryID) == "" {
categoryID = "slack-cli#getting-started"
}
// App categories and templates
templatePromptObjects := map[string]([]promptObject){
"slack-cli#getting-started": []promptObject{
{
Title: fmt.Sprintf("Bolt for JavaScript %s", style.Secondary("Node.js")),
Repository: "slack-samples/bolt-js-starter-template",
},
{
Title: fmt.Sprintf("Bolt for Python %s", style.Secondary("Python")),
Repository: "slack-samples/bolt-python-starter-template",
},
},
"slack-cli#automation-apps": {
{
Title: fmt.Sprintf("Bolt for JavaScript %s", style.Secondary("Node.js")),
Repository: "slack-samples/bolt-js-custom-function-template",
},
{
Title: fmt.Sprintf("Bolt for Python %s", style.Secondary("Python")),
Repository: "slack-samples/bolt-python-custom-function-template",
},
{
Title: fmt.Sprintf("Deno Slack SDK %s", style.Secondary("Deno")),
Repository: "slack-samples/deno-starter-template",
},
},
"slack-cli#ai-apps": {
{
Title: fmt.Sprintf("Bolt for JavaScript %s", style.Secondary("Node.js")),
Repository: "slack-samples/bolt-js-assistant-template",
},
{
Title: fmt.Sprintf("Bolt for Python %s", style.Secondary("Python")),
Repository: "slack-samples/bolt-python-assistant-template",
},
},
}
return templatePromptObjects[categoryID]
}
func getSelectionOptionsForCategory(clients *shared.ClientFactory) []promptObject {
return []promptObject{
{
Title: fmt.Sprintf("Starter app %s", style.Secondary("Getting started Slack app")),
Repository: "slack-cli#getting-started",
},
{
Title: fmt.Sprintf("AI Agent app %s", style.Secondary("Slack agents and assistants")),
Repository: "slack-cli#ai-apps",
},
{
Title: fmt.Sprintf("Automation app %s", style.Secondary("Custom steps and workflows")),
Repository: "slack-cli#automation-apps",
},
{
Title: "View more samples",
Repository: viewMoreSamples,
},
}
}

Flag: clients.Config.Flags.Lookup("template"),
Required: true,
Template: templateForCategory,
Expand Down Expand Up @@ -168,9 +165,6 @@ func promptTemplateSelection(cmd *cobra.Command, clients *shared.ClientFactory,

// Prompt to choose a template
selection, err := clients.IO.SelectPrompt(ctx, prompt, titles, iostreams.SelectPromptConfig{
Description: func(value string, index int) string {
return options[index].Description
},
Flag: clients.Config.Flags.Lookup("template"),
Required: true,
Template: template,
Expand Down
5 changes: 3 additions & 2 deletions internal/iostreams/charm.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,15 +72,16 @@ func buildSelectForm(msg string, options []string, cfg SelectPromptConfig, selec
for _, opt := range options {
key := opt
if cfg.Description != nil {
if desc := cfg.Description(opt, len(opts)); desc != "" {
key = opt + "\n " + desc
if desc := style.RemoveEmoji(cfg.Description(opt, len(opts))); desc != "" {
key = opt + " - " + desc
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

๐Ÿญ note: It'd be curious in a different separator but am unsure what reads best for most descriptions...

}
}
opts = append(opts, huh.NewOption(key, opt))
}

field := huh.NewSelect[string]().
Title(msg).
Description(cfg.Help).
Options(opts...).
Value(selected)

Expand Down
3 changes: 2 additions & 1 deletion internal/iostreams/survey.go
Original file line number Diff line number Diff line change
Expand Up @@ -409,9 +409,10 @@ var selectQuestionTemplate = fmt.Sprintf(`

// SelectPromptConfig holds additional config for a survey.Select prompt
type SelectPromptConfig struct {
Description func(value string, index int) string // Optional text displayed below each prompt option
Description func(value string, index int) string // Optional text displayed with each prompt option
Flag *pflag.Flag // The single flag substitute for this prompt
Flags []*pflag.Flag // Otherwise multiple flag substitutes for this prompt
Help string // Optional help text displayed below the select title
PageSize int // The number of options displayed before the user needs to scroll
Required bool // If a response is required
Template string // Custom formatting of the selection prompt
Expand Down
14 changes: 14 additions & 0 deletions internal/style/style.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,20 @@ func RemoveANSI(str string) string {
return ansiRegex.ReplaceAllString(str, "")
}

// RemoveEmoji strips non-ASCII characters (such as emoji) from a string
// and collapses any resulting extra whitespace.
//
// https://en.wikipedia.org/wiki/ASCII#Printable_character_table
func RemoveEmoji(str string) string {
var b strings.Builder
for _, r := range str {
if r <= 127 {
b.WriteRune(r)
}
}
return strings.Join(strings.Fields(b.String()), " ")
}

// ToggleStyles sets styles and formatting values to the active state
func ToggleStyles(active bool) {
isStyleEnabled = active
Expand Down
30 changes: 30 additions & 0 deletions internal/style/style_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,36 @@ func TestRemoveANSI(t *testing.T) {
}
}

func TestRemoveEmoji(t *testing.T) {
tests := map[string]struct {
input string
expected string
}{
"plain text is unchanged": {
input: "A simple description",
expected: "A simple description",
},
"emoji flags are removed": {
input: "A translation bot ๐Ÿ‡จ๐Ÿ‡ณ ๐Ÿ‡ฎ๐Ÿ‡น ๐Ÿ‡น๐Ÿ‡ญ ๐Ÿ‡ซ๐Ÿ‡ท",
expected: "A translation bot",
},
"mixed emoji and text": {
input: "Hello ๐ŸŒ world ๐Ÿš€ test",
expected: "Hello world test",
},
"empty string": {
input: "",
expected: "",
},
}
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
actual := RemoveEmoji(tc.input)
assert.Equal(t, tc.expected, actual)
})
}
}

func TestToggleStyles(t *testing.T) {
defer func() {
ToggleStyles(false)
Expand Down
Loading