Skip to content

Commit 1d23e94

Browse files
zimegmwbrooks
andauthored
feat: output a list of samples for a language with the samples command (#192)
Co-authored-by: Michael Brooks <[email protected]>
1 parent c85c86c commit 1d23e94

File tree

5 files changed

+173
-40
lines changed

5 files changed

+173
-40
lines changed

cmd/project/create_samples.go

Lines changed: 33 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -29,16 +29,11 @@ import (
2929
)
3030

3131
//go:embed samples.tmpl
32-
var embedSamplesTmpl string
32+
var embedPromptSamplesTmpl string
3333

34-
// PromptSampleSelection gathers upstream samples to select from
35-
func PromptSampleSelection(ctx context.Context, clients *shared.ClientFactory, samples create.Sampler) (string, error) {
36-
sampleRepos, err := create.GetSampleRepos(samples)
37-
if err != nil {
38-
return "", err
39-
}
40-
41-
projectTypes := []string{}
34+
// promptSampleSelection gathers upstream samples to select from
35+
func promptSampleSelection(ctx context.Context, clients *shared.ClientFactory, sampleRepos []create.GithubRepo) (string, error) {
36+
filteredRepos := []create.GithubRepo{}
4237
selection, err := clients.IO.SelectPrompt(ctx, "Select a language:",
4338
[]string{
4439
fmt.Sprintf("Bolt for JavaScript %s", style.Secondary("Node.js")),
@@ -58,32 +53,16 @@ func PromptSampleSelection(ctx context.Context, clients *shared.ClientFactory, s
5853
} else if selection.Prompt {
5954
switch selection.Index {
6055
case 0:
61-
projectTypes = []string{"bolt-js", "bolt-ts"}
56+
filteredRepos = filterRepos(sampleRepos, "node")
6257
case 1:
63-
projectTypes = []string{"bolt-python"}
58+
filteredRepos = filterRepos(sampleRepos, "python")
6459
case 2:
65-
projectTypes = []string{"deno"}
60+
filteredRepos = filterRepos(sampleRepos, "deno")
6661
}
6762
} else if selection.Flag {
68-
switch strings.ToLower(strings.TrimSpace(selection.Option)) {
69-
case "node":
70-
projectTypes = []string{"bolt-js", "bolt-ts"}
71-
case "python":
72-
projectTypes = []string{"bolt-python"}
73-
case "deno":
74-
projectTypes = []string{"deno"}
75-
default:
76-
projectTypes = []string{selection.Option}
77-
}
63+
filteredRepos = filterRepos(sampleRepos, selection.Option)
7864
}
7965

80-
filteredRepos := []create.GithubRepo{}
81-
if len(projectTypes) <= 0 {
82-
filteredRepos = sampleRepos
83-
}
84-
for _, language := range projectTypes {
85-
filteredRepos = append(filteredRepos, filterRepos(sampleRepos, language)...)
86-
}
8766
sortedRepos := sortRepos(filteredRepos)
8867
selectOptions := createSelectOptions(sortedRepos)
8968

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

110-
// filterRepos takes in a list of repositories and returns a filtered list
111-
// based on the prepended runtime/framework naming convention for
112-
// repositories in the Slack Samples Org (ie, deno-*, bolt-js-*, etc.)
89+
// filterRepos returns a list of samples matching the provided project type
90+
// according to the project naming conventions of @slack-samples.
91+
//
92+
// Ex: "node" matches both "bolt-js" and "bolt-ts" prefixed samples.
11393
func filterRepos(sampleRepos []create.GithubRepo, projectType string) []create.GithubRepo {
11494
filteredRepos := make([]create.GithubRepo, 0)
11595
for _, s := range sampleRepos {
116-
if strings.HasPrefix(s.Name, projectType) {
117-
filteredRepos = append(filteredRepos, s)
96+
search := strings.TrimSpace(strings.ToLower(projectType))
97+
switch search {
98+
case "java":
99+
if strings.HasPrefix(s.Name, "bolt-java") {
100+
filteredRepos = append(filteredRepos, s)
101+
}
102+
case "node":
103+
if strings.HasPrefix(s.Name, "bolt-js") || strings.HasPrefix(s.Name, "bolt-ts") {
104+
filteredRepos = append(filteredRepos, s)
105+
}
106+
case "python":
107+
if strings.HasPrefix(s.Name, "bolt-python") {
108+
filteredRepos = append(filteredRepos, s)
109+
}
110+
case "deno":
111+
fallthrough
112+
default:
113+
if strings.HasPrefix(s.Name, search) || search == "" {
114+
filteredRepos = append(filteredRepos, s)
115+
}
118116
}
119117
}
120118
return filteredRepos

cmd/project/create_samples_test.go

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import (
2525
"github.com/slackapi/slack-cli/internal/slackcontext"
2626
"github.com/stretchr/testify/assert"
2727
"github.com/stretchr/testify/mock"
28+
"github.com/stretchr/testify/require"
2829
)
2930

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

133134
// Execute test
134-
repoName, err := PromptSampleSelection(ctx, clients, sampler)
135+
samples, err := create.GetSampleRepos(sampler)
136+
require.NoError(t, err)
137+
repoName, err := promptSampleSelection(ctx, clients, samples)
135138
assert.Equal(t, tt.expectedError, err)
136139
assert.Equal(t, tt.expectedRepository, repoName)
137140
})
138141
}
139142
}
140143

141144
func TestSamples_FilterRepos(t *testing.T) {
142-
filteredRepos := filterRepos(mockGitHubRepos, "deno")
143-
assert.Equal(t, len(filteredRepos), 2, "Expected filteredRepos length to be 2")
145+
tests := map[string]struct {
146+
language string
147+
expectedRepos int
148+
}{
149+
"deno matches deno": {
150+
language: "deno",
151+
expectedRepos: 2,
152+
},
153+
"node matches bolt-js and bolt-ts": {
154+
language: "node",
155+
expectedRepos: 1,
156+
},
157+
"no filter returns all options": {
158+
language: "",
159+
expectedRepos: 4,
160+
},
161+
}
162+
for name, tt := range tests {
163+
t.Run(name, func(t *testing.T) {
164+
filteredRepos := filterRepos(mockGitHubRepos, tt.language)
165+
assert.Equal(t, tt.expectedRepos, len(filteredRepos))
166+
})
167+
}
144168
}
145169

146170
func TestSamples_SortRepos(t *testing.T) {

cmd/project/create_template.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,11 @@ func promptTemplateSelection(cmd *cobra.Command, clients *shared.ClientFactory)
173173
sampler := api.NewHTTPClient(api.HTTPClientOptions{
174174
TotalTimeOut: 60 * time.Second,
175175
})
176-
selectedSample, err := PromptSampleSelection(ctx, clients, sampler)
176+
samples, err := create.GetSampleRepos(sampler)
177+
if err != nil {
178+
return create.Template{}, err
179+
}
180+
selectedSample, err := promptSampleSelection(ctx, clients, samples)
177181
if err != nil {
178182
return create.Template{}, err
179183
}

cmd/project/samples.go

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,13 @@
1515
package project
1616

1717
import (
18+
"context"
19+
"fmt"
20+
"strings"
1821
"time"
1922

2023
"github.com/slackapi/slack-cli/internal/api"
24+
"github.com/slackapi/slack-cli/internal/pkg/create"
2125
"github.com/slackapi/slack-cli/internal/shared"
2226
"github.com/slackapi/slack-cli/internal/style"
2327
"github.com/spf13/cobra"
@@ -26,6 +30,7 @@ import (
2630
// Flags
2731
var samplesTemplateURLFlag string
2832
var samplesGitBranchFlag string
33+
var samplesListFlag bool
2934
var samplesLanguageFlag string
3035

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

5464
return cmd
5565
}
@@ -60,7 +70,18 @@ func runSamplesCommand(clients *shared.ClientFactory, cmd *cobra.Command, args [
6070
sampler := api.NewHTTPClient(api.HTTPClientOptions{
6171
TotalTimeOut: 60 * time.Second,
6272
})
63-
selectedSample, err := PromptSampleSelection(ctx, clients, sampler)
73+
samples, err := create.GetSampleRepos(sampler)
74+
if err != nil {
75+
return err
76+
}
77+
if samplesListFlag || !clients.IO.IsTTY() {
78+
err := listSampleSelection(ctx, clients, samples)
79+
if err != nil {
80+
return err
81+
}
82+
return nil
83+
}
84+
selectedSample, err := promptSampleSelection(ctx, clients, samples)
6485
if err != nil {
6586
return err
6687
}
@@ -85,3 +106,55 @@ func runSamplesCommand(clients *shared.ClientFactory, cmd *cobra.Command, args [
85106
// Execute the `create` command with the set flag
86107
return createCmd.ExecuteContext(ctx)
87108
}
109+
110+
// listSampleSelection outputs available samples matching a language flag filter
111+
func listSampleSelection(ctx context.Context, clients *shared.ClientFactory, sampleRepos []create.GithubRepo) error {
112+
filteredRepos := filterRepos(sampleRepos, samplesLanguageFlag)
113+
sortedRepos := sortRepos(filteredRepos)
114+
templateRepos := []create.GithubRepo{}
115+
exampleRepos := []create.GithubRepo{}
116+
for _, repo := range sortedRepos {
117+
if strings.Contains(repo.FullName, "template") {
118+
templateRepos = append(templateRepos, repo)
119+
} else {
120+
exampleRepos = append(exampleRepos, repo)
121+
}
122+
}
123+
message := ""
124+
if samplesLanguageFlag != "" {
125+
message = fmt.Sprintf(
126+
"Listing %d \"%s\" templates and project samples",
127+
len(filteredRepos),
128+
samplesLanguageFlag,
129+
)
130+
} else {
131+
message = fmt.Sprintf("Listing %d template and project samples", len(filteredRepos))
132+
}
133+
clients.IO.PrintInfo(ctx, false, "\n%s", style.Sectionf(style.TextSection{
134+
Emoji: "toolbox",
135+
Text: "Samples",
136+
Secondary: []string{
137+
message,
138+
},
139+
}))
140+
samples := append(
141+
templateRepos,
142+
exampleRepos...,
143+
)
144+
for _, sample := range samples {
145+
clients.IO.PrintInfo(ctx, false, style.Sectionf(style.TextSection{
146+
Emoji: "hammer_and_wrench",
147+
Text: fmt.Sprintf(
148+
" %s | %s | %d %s",
149+
style.Bold(sample.Name),
150+
sample.Description,
151+
sample.StargazersCount,
152+
style.Pluralize("star", "stars", sample.StargazersCount),
153+
),
154+
Secondary: []string{
155+
fmt.Sprintf("https://github.com/%s", sample.FullName),
156+
},
157+
}))
158+
}
159+
return nil
160+
}

cmd/project/samples_test.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ func TestSamplesCommand(t *testing.T) {
3434
"creates a template from a trusted sample": {
3535
CmdArgs: []string{"my-sample-app"},
3636
Setup: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock, cf *shared.ClientFactory) {
37+
cm.IO.On("IsTTY").Return(true)
3738
createPkg.GetSampleRepos = func(client createPkg.Sampler) ([]createPkg.GithubRepo, error) {
3839
repos := []createPkg.GithubRepo{
3940
{
@@ -97,6 +98,39 @@ func TestSamplesCommand(t *testing.T) {
9798
}
9899
},
99100
},
101+
"lists available samples matching a language": {
102+
CmdArgs: []string{"--list", "--language", "node"},
103+
Setup: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock, cf *shared.ClientFactory) {
104+
cm.IO.On("IsTTY").Return(true)
105+
createPkg.GetSampleRepos = func(client createPkg.Sampler) ([]createPkg.GithubRepo, error) {
106+
repos := []createPkg.GithubRepo{
107+
{
108+
Name: "deno-starter-template",
109+
FullName: "slack-samples/deno-starter-template",
110+
CreatedAt: "2025-02-11T12:34:56Z",
111+
StargazersCount: 4,
112+
Description: "a mock starter template for deno",
113+
Language: "typescript",
114+
},
115+
{
116+
Name: "bolt-js-starter-template",
117+
FullName: "slack-samples/bolt-js-starter-template",
118+
CreatedAt: "2025-02-11T12:34:56Z",
119+
StargazersCount: 12,
120+
Description: "a mock starter template for bolt js",
121+
Language: "javascript",
122+
},
123+
}
124+
return repos, nil
125+
}
126+
},
127+
ExpectedOutputs: []string{
128+
"https://github.com/slack-samples/bolt-js-starter-template",
129+
},
130+
ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) {
131+
assert.NotContains(t, cm.GetStdoutOutput(), "deno-starter-template")
132+
},
133+
},
100134
}, func(cf *shared.ClientFactory) *cobra.Command {
101135
return NewSamplesCommand(cf)
102136
})

0 commit comments

Comments
 (0)