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
68 changes: 33 additions & 35 deletions cmd/project/create_samples.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,16 +29,11 @@ import (
)

//go:embed samples.tmpl
var embedSamplesTmpl string
var embedPromptSamplesTmpl string

// PromptSampleSelection gathers upstream samples to select from
func PromptSampleSelection(ctx context.Context, clients *shared.ClientFactory, samples create.Sampler) (string, error) {
sampleRepos, err := create.GetSampleRepos(samples)
if err != nil {
return "", err
}

projectTypes := []string{}
// promptSampleSelection gathers upstream samples to select from
func promptSampleSelection(ctx context.Context, clients *shared.ClientFactory, sampleRepos []create.GithubRepo) (string, error) {
filteredRepos := []create.GithubRepo{}
selection, err := clients.IO.SelectPrompt(ctx, "Select a language:",
[]string{
fmt.Sprintf("Bolt for JavaScript %s", style.Secondary("Node.js")),
Expand All @@ -58,32 +53,16 @@ func PromptSampleSelection(ctx context.Context, clients *shared.ClientFactory, s
} else if selection.Prompt {
switch selection.Index {
case 0:
projectTypes = []string{"bolt-js", "bolt-ts"}
filteredRepos = filterRepos(sampleRepos, "node")
case 1:
projectTypes = []string{"bolt-python"}
filteredRepos = filterRepos(sampleRepos, "python")
case 2:
projectTypes = []string{"deno"}
filteredRepos = filterRepos(sampleRepos, "deno")
}
} else if selection.Flag {
switch strings.ToLower(strings.TrimSpace(selection.Option)) {
case "node":
projectTypes = []string{"bolt-js", "bolt-ts"}
case "python":
projectTypes = []string{"bolt-python"}
case "deno":
projectTypes = []string{"deno"}
default:
projectTypes = []string{selection.Option}
}
filteredRepos = filterRepos(sampleRepos, selection.Option)
}

filteredRepos := []create.GithubRepo{}
if len(projectTypes) <= 0 {
filteredRepos = sampleRepos
}
for _, language := range projectTypes {
filteredRepos = append(filteredRepos, filterRepos(sampleRepos, language)...)
}
sortedRepos := sortRepos(filteredRepos)
selectOptions := createSelectOptions(sortedRepos)

Expand All @@ -95,7 +74,7 @@ func PromptSampleSelection(ctx context.Context, clients *shared.ClientFactory, s
Flag: clients.Config.Flags.Lookup("template"),
PageSize: 4, // Supports standard terminal height (24 rows)
Required: true,
Template: embedSamplesTmpl,
Template: embedPromptSamplesTmpl,
})
if err != nil {
return "", err
Expand All @@ -107,14 +86,33 @@ func PromptSampleSelection(ctx context.Context, clients *shared.ClientFactory, s
return selectedTemplate, nil
}

// filterRepos takes in a list of repositories and returns a filtered list
// based on the prepended runtime/framework naming convention for
// repositories in the Slack Samples Org (ie, deno-*, bolt-js-*, etc.)
// filterRepos returns a list of samples matching the provided project type
// according to the project naming conventions of @slack-samples.
//
// Ex: "node" matches both "bolt-js" and "bolt-ts" prefixed samples.
func filterRepos(sampleRepos []create.GithubRepo, projectType string) []create.GithubRepo {
filteredRepos := make([]create.GithubRepo, 0)
for _, s := range sampleRepos {
if strings.HasPrefix(s.Name, projectType) {
filteredRepos = append(filteredRepos, s)
search := strings.TrimSpace(strings.ToLower(projectType))
switch search {
case "java":
if strings.HasPrefix(s.Name, "bolt-java") {
filteredRepos = append(filteredRepos, s)
}
case "node":
if strings.HasPrefix(s.Name, "bolt-js") || strings.HasPrefix(s.Name, "bolt-ts") {
filteredRepos = append(filteredRepos, s)
}
case "python":
if strings.HasPrefix(s.Name, "bolt-python") {
filteredRepos = append(filteredRepos, s)
}
case "deno":
fallthrough
default:
if strings.HasPrefix(s.Name, search) || search == "" {
filteredRepos = append(filteredRepos, s)
}
}
}
return filteredRepos
Expand Down
30 changes: 27 additions & 3 deletions cmd/project/create_samples_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
"github.com/slackapi/slack-cli/internal/slackcontext"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
)

var mockGitHubRepos = []create.GithubRepo{
Expand Down Expand Up @@ -131,16 +132,39 @@ func TestSamples_PromptSampleSelection(t *testing.T) {
clients := shared.NewClientFactory(clientsMock.MockClientFactory())

// Execute test
repoName, err := PromptSampleSelection(ctx, clients, sampler)
samples, err := create.GetSampleRepos(sampler)
require.NoError(t, err)
repoName, err := promptSampleSelection(ctx, clients, samples)
assert.Equal(t, tt.expectedError, err)
assert.Equal(t, tt.expectedRepository, repoName)
})
}
}

func TestSamples_FilterRepos(t *testing.T) {
filteredRepos := filterRepos(mockGitHubRepos, "deno")
assert.Equal(t, len(filteredRepos), 2, "Expected filteredRepos length to be 2")
tests := map[string]struct {
language string
expectedRepos int
}{
"deno matches deno": {
language: "deno",
expectedRepos: 2,
},
"node matches bolt-js and bolt-ts": {
language: "node",
expectedRepos: 1,
},
"no filter returns all options": {
language: "",
expectedRepos: 4,
},
}
for name, tt := range tests {
t.Run(name, func(t *testing.T) {
filteredRepos := filterRepos(mockGitHubRepos, tt.language)
assert.Equal(t, tt.expectedRepos, len(filteredRepos))
})
}
}

func TestSamples_SortRepos(t *testing.T) {
Expand Down
6 changes: 5 additions & 1 deletion cmd/project/create_template.go
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,11 @@ func promptTemplateSelection(cmd *cobra.Command, clients *shared.ClientFactory)
sampler := api.NewHTTPClient(api.HTTPClientOptions{
TotalTimeOut: 60 * time.Second,
})
selectedSample, err := PromptSampleSelection(ctx, clients, sampler)
samples, err := create.GetSampleRepos(sampler)
if err != nil {
return create.Template{}, err
}
selectedSample, err := promptSampleSelection(ctx, clients, samples)
if err != nil {
return create.Template{}, err
}
Expand Down
75 changes: 74 additions & 1 deletion cmd/project/samples.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,13 @@
package project

import (
"context"
"fmt"
"strings"
"time"

"github.com/slackapi/slack-cli/internal/api"
"github.com/slackapi/slack-cli/internal/pkg/create"
"github.com/slackapi/slack-cli/internal/shared"
"github.com/slackapi/slack-cli/internal/style"
"github.com/spf13/cobra"
Expand All @@ -26,6 +30,7 @@ import (
// Flags
var samplesTemplateURLFlag string
var samplesGitBranchFlag string
var samplesListFlag bool
var samplesLanguageFlag string

func NewSamplesCommand(clients *shared.ClientFactory) *cobra.Command {
Expand All @@ -35,6 +40,10 @@ func NewSamplesCommand(clients *shared.ClientFactory) *cobra.Command {
Short: "List available sample apps",
Long: "List and create an app from the available samples",
Example: style.ExampleCommandsf([]style.ExampleCommand{
{
Meaning: "List Bolt for JavaScript samples",
Command: "samples --list --language node",
},
{
Meaning: "Select a sample app to create",
Command: "samples my-project",
Expand All @@ -50,6 +59,7 @@ func NewSamplesCommand(clients *shared.ClientFactory) *cobra.Command {
cmd.Flags().StringVarP(&samplesTemplateURLFlag, "template", "t", "", "template URL for your app")
cmd.Flags().StringVarP(&samplesGitBranchFlag, "branch", "b", "", "name of git branch to checkout")
cmd.Flags().StringVar(&samplesLanguageFlag, "language", "", "runtime for the app framework\n ex: \"deno\", \"node\", \"python\"")
cmd.Flags().BoolVar(&samplesListFlag, "list", false, "prints samples without interactivity")

return cmd
}
Expand All @@ -60,7 +70,18 @@ func runSamplesCommand(clients *shared.ClientFactory, cmd *cobra.Command, args [
sampler := api.NewHTTPClient(api.HTTPClientOptions{
TotalTimeOut: 60 * time.Second,
})
selectedSample, err := PromptSampleSelection(ctx, clients, sampler)
samples, err := create.GetSampleRepos(sampler)
if err != nil {
return err
}
if samplesListFlag || !clients.IO.IsTTY() {
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: We avoid attempting prompts in a non-interactive environment and instead list samples!

🗣️ note: The changes of #193 make this behavior more clear IMO since flag substitutes will then have matching prompts or the create command should be used.

err := listSampleSelection(ctx, clients, samples)
if err != nil {
return err
}
return nil
}
selectedSample, err := promptSampleSelection(ctx, clients, samples)
if err != nil {
return err
}
Expand All @@ -85,3 +106,55 @@ func runSamplesCommand(clients *shared.ClientFactory, cmd *cobra.Command, args [
// Execute the `create` command with the set flag
return createCmd.ExecuteContext(ctx)
}

// listSampleSelection outputs available samples matching a language flag filter
func listSampleSelection(ctx context.Context, clients *shared.ClientFactory, sampleRepos []create.GithubRepo) error {
filteredRepos := filterRepos(sampleRepos, samplesLanguageFlag)
sortedRepos := sortRepos(filteredRepos)
Copy link
Member

Choose a reason for hiding this comment

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

question: Are you noticing that the repos aren't sorted?

Image

Copy link
Member Author

Choose a reason for hiding this comment

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

@mwbrooks This confused me too but these might find order from the stars? ⭐

I wonder if showing the number of stars would be helpful? I might expect this to prefix the sample, but am not sure at all if that'd be right:

⭐ 60 - deno-timesheet-approval
   Collect timesheet information from users and store it in a Google Sheet
   https://github.com/slack-samples/deno-timesheet-approval

templateRepos := []create.GithubRepo{}
exampleRepos := []create.GithubRepo{}
for _, repo := range sortedRepos {
if strings.Contains(repo.FullName, "template") {
templateRepos = append(templateRepos, repo)
} else {
exampleRepos = append(exampleRepos, repo)
}
}
message := ""
if samplesLanguageFlag != "" {
message = fmt.Sprintf(
"Listing %d \"%s\" templates and project samples",
len(filteredRepos),
samplesLanguageFlag,
)
} else {
message = fmt.Sprintf("Listing %d template and project samples", len(filteredRepos))
}
clients.IO.PrintInfo(ctx, false, "\n%s", style.Sectionf(style.TextSection{
Emoji: "toolbox",
Text: "Samples",
Secondary: []string{
message,
},
}))
samples := append(
templateRepos,
exampleRepos...,
)
for _, sample := range samples {
clients.IO.PrintInfo(ctx, false, style.Sectionf(style.TextSection{
Emoji: "hammer_and_wrench",
Text: fmt.Sprintf(
" %s | %s | %d %s",
style.Bold(sample.Name),
sample.Description,
sample.StargazersCount,
style.Pluralize("star", "stars", sample.StargazersCount),
),
Secondary: []string{
fmt.Sprintf("https://github.com/%s", sample.FullName),
},
}))
}
return nil
}
34 changes: 34 additions & 0 deletions cmd/project/samples_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ func TestSamplesCommand(t *testing.T) {
"creates a template from a trusted sample": {
CmdArgs: []string{"my-sample-app"},
Setup: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock, cf *shared.ClientFactory) {
cm.IO.On("IsTTY").Return(true)
createPkg.GetSampleRepos = func(client createPkg.Sampler) ([]createPkg.GithubRepo, error) {
repos := []createPkg.GithubRepo{
{
Expand Down Expand Up @@ -97,6 +98,39 @@ func TestSamplesCommand(t *testing.T) {
}
},
},
"lists available samples matching a language": {
CmdArgs: []string{"--list", "--language", "node"},
Setup: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock, cf *shared.ClientFactory) {
cm.IO.On("IsTTY").Return(true)
createPkg.GetSampleRepos = func(client createPkg.Sampler) ([]createPkg.GithubRepo, error) {
repos := []createPkg.GithubRepo{
{
Name: "deno-starter-template",
FullName: "slack-samples/deno-starter-template",
CreatedAt: "2025-02-11T12:34:56Z",
StargazersCount: 4,
Description: "a mock starter template for deno",
Language: "typescript",
},
{
Name: "bolt-js-starter-template",
FullName: "slack-samples/bolt-js-starter-template",
CreatedAt: "2025-02-11T12:34:56Z",
StargazersCount: 12,
Description: "a mock starter template for bolt js",
Language: "javascript",
},
}
return repos, nil
}
},
ExpectedOutputs: []string{
"https://github.com/slack-samples/bolt-js-starter-template",
},
ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) {
assert.NotContains(t, cm.GetStdoutOutput(), "deno-starter-template")
},
},
}, func(cf *shared.ClientFactory) *cobra.Command {
return NewSamplesCommand(cf)
})
Expand Down
Loading