Skip to content
Merged
113 changes: 40 additions & 73 deletions cmd/app/link.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,6 @@ import (
"github.com/spf13/cobra"
)

// LinkAppConfirmPromptText is displayed when prompting to add an existing app
const LinkAppConfirmPromptText = "Do you want to add an existing app?"

// appLinkFlagSet contains flag values to reference
type appLinkFlagSet struct {
environmentFlag string
Expand Down Expand Up @@ -97,58 +94,8 @@ func LinkCommandRunE(ctx context.Context, clients *shared.ClientFactory, app *ty
// Add empty line between executed command and first output
clients.IO.PrintInfo(ctx, false, "")

err = LinkExistingApp(ctx, clients, app, false)
if err != nil {
return err
}

return nil
}

// LinkAppHeaderSection displays a section explaining how to find existing apps.
// External callers can use extraSecondaryText to show additional information.
// When shouldConfirm is true, additional information is included in the header
// explaining how to link apps, in case the user declines.
func LinkAppHeaderSection(ctx context.Context, clients *shared.ClientFactory, shouldConfirm bool) {
var secondaryText = []string{
"Add an existing app from app settings",
"Find your existing apps at: " + style.Underline("https://api.slack.com/apps"),
}

if shouldConfirm {
secondaryText = append(secondaryText, "Manually add apps later with "+style.Commandf("app link", true))
}

clients.IO.PrintInfo(ctx, false, "%s", style.Sectionf(style.TextSection{
Emoji: "house",
Text: "App Link",
Secondary: secondaryText,
}))
}

// LinkExistingApp prompts for an existing App ID and saves the details to the project.
// When shouldConfirm is true, a confirmation prompt will ask the user if they want to
// link an existing app and additional information is included in the header.
// The shouldConfirm option is encouraged for third-party callers.
func LinkExistingApp(ctx context.Context, clients *shared.ClientFactory, app *types.App, shouldConfirm bool) (err error) {
// Header section
LinkAppHeaderSection(ctx, clients, shouldConfirm)

// Confirm to add an existing app; useful for third-party callers
if shouldConfirm {
proceed, err := clients.IO.ConfirmPrompt(ctx, LinkAppConfirmPromptText, true)
if err != nil {
clients.IO.PrintDebug(ctx, "Error prompting to add an existing app: %s", err)
return err
}

// Add newline to match the trailing newline inserted from the footer section
clients.IO.PrintInfo(ctx, false, "")

if !proceed {
return nil
}
}
LinkAppHeaderSection(ctx, clients)

// App Manifest section
manifestSource, err := clients.Config.ProjectConfig.GetManifestSource(ctx)
Expand All @@ -166,6 +113,45 @@ func LinkExistingApp(ctx context.Context, clients *shared.ClientFactory, app *ty
},
}))

err = LinkExistingApp(ctx, clients, app)
if err != nil {
return err
}

// App summary section
clients.IO.PrintInfo(ctx, false, "%s", style.Sectionf(style.TextSection{
Emoji: "house",
Text: "App",
Secondary: FormatListSuccess([]types.App{*app}),
}))
Comment thread
zimeg marked this conversation as resolved.

// Footer section
clients.IO.PrintInfo(ctx, false, "%s", style.Sectionf(style.TextSection{
Emoji: "house_with_garden",
Text: "App Link",
Secondary: []string{
"Added existing app to project",
},
}))

return nil
}

// LinkAppHeaderSection displays a section explaining how to find existing apps.
func LinkAppHeaderSection(ctx context.Context, clients *shared.ClientFactory) {
clients.IO.PrintInfo(ctx, false, "%s", style.Sectionf(style.TextSection{
Emoji: "house",
Text: "App Link",
Secondary: []string{
"Add an existing app from app settings",
"Find your existing apps at: " + style.Underline("https://api.slack.com/apps"),
},
}))
}

// LinkExistingApp resolves app details, validates the app, and saves it to the
// project. It produces no output — callers handle their own display.
func LinkExistingApp(ctx context.Context, clients *shared.ClientFactory, app *types.App) (err error) {
// Prompt to get app details
var auth *types.SlackAuth
*app, auth, err = promptExistingApp(ctx, clients)
Expand All @@ -186,28 +172,9 @@ func LinkExistingApp(ctx context.Context, clients *shared.ClientFactory, app *ty
return err
}

// Footer section
LinkAppFooterSection(ctx, clients, app)

return nil
}

// LinkAppFooterSection displays the details of app that was added to the project.
func LinkAppFooterSection(ctx context.Context, clients *shared.ClientFactory, app *types.App) {
clients.IO.PrintInfo(ctx, false, "\n%s", style.Sectionf(style.TextSection{
Emoji: "house",
Text: "App",
Secondary: formatListSuccess([]types.App{*app}),
}))
clients.IO.PrintInfo(ctx, false, "%s", style.Sectionf(style.TextSection{
Emoji: "house_with_garden",
Text: "App Link",
Secondary: []string{
"Added existing app to project",
},
}))
}

// promptExistingApp gathers details to represent app information
func promptExistingApp(ctx context.Context, clients *shared.ClientFactory) (types.App, *types.SlackAuth, error) {
slackAuth, err := prompts.PromptTeamSlackAuth(ctx, clients, "Select the existing app team", nil)
Expand Down
28 changes: 3 additions & 25 deletions cmd/app/link_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -573,50 +573,28 @@ func Test_Apps_Link(t *testing.T) {

func Test_Apps_LinkAppHeaderSection(t *testing.T) {
tests := map[string]struct {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

🏗️ quibble: Keeping table tests structured - even if one case - instead of a flattened single case might encourage additional cases

shouldConfirm bool
expectedOutputs []string
unexpectedOutputs []string
expectedOutputs []string
}{
"When shouldConfirm is false": {
shouldConfirm: false,
"Displays app link header with base information": {
expectedOutputs: []string{
"Add an existing app from app settings",
"Find your existing apps at: https://api.slack.com/apps",
},
unexpectedOutputs: []string{
"Manually add apps later with",
},
},
"When shouldConfirm is true": {
shouldConfirm: true,
expectedOutputs: []string{
"Add an existing app from app settings",
"Find your existing apps at: https://api.slack.com/apps",
"Manually add apps later with",
},
},
}
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
// Create mocks
ctx := slackcontext.MockContext(t.Context())
clientsMock := shared.NewClientsMock()
clientsMock.AddDefaultMocks()

// Create clients that is mocked for testing
clients := shared.NewClientFactory(clientsMock.MockClientFactory())

// Run the test
LinkAppHeaderSection(ctx, clients, tc.shouldConfirm)
LinkAppHeaderSection(ctx, clients)

// Assertions
output := clientsMock.GetCombinedOutput()
for _, expectedOutput := range tc.expectedOutputs {
require.Contains(t, output, expectedOutput)
}
for _, unexpectedOutput := range tc.unexpectedOutputs {
require.NotContains(t, output, unexpectedOutput)
}
})
}
}
Expand Down
6 changes: 3 additions & 3 deletions cmd/app/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,13 +73,13 @@ func runListCommand(cmd *cobra.Command, clients *shared.ClientFactory) error {
clients.IO.PrintInfo(ctx, false, "\n%s", style.Sectionf(style.TextSection{
Emoji: "house_buildings",
Text: "Apps",
Secondary: formatListSuccess(envs),
Secondary: FormatListSuccess(envs),
}))
return nil
}

// formatListSuccess formats details about the list of project apps
func formatListSuccess(apps []types.App) (secondaryText []string) {
// FormatListSuccess formats details about the list of project apps
func FormatListSuccess(apps []types.App) (secondaryText []string) {
for _, app := range apps {
if app.AppID == "" {
continue
Expand Down
2 changes: 1 addition & 1 deletion cmd/app/list_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@ func TestAppsListFormat(t *testing.T) {
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
listFlags = tc.Flags
formattedList := formatListSuccess(tc.Apps)
formattedList := FormatListSuccess(tc.Apps)
for ii, value := range formattedList {
formattedList[ii] = strings.TrimRight(value, ":")
}
Expand Down
8 changes: 7 additions & 1 deletion cmd/project/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -229,9 +229,15 @@ func runCreateCommand(clients *shared.ClientFactory, cmd *cobra.Command, args []
defer func() {
_ = os.Chdir(originalDir)
}()
if err := app.LinkExistingApp(ctx, clients, &types.App{}, false); err != nil {
linkedApp := &types.App{}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

🔭 note: IIRC adjacent PR has discussion about returning instead of providing an app argument. No blocker but it has support in eventual follow up!

if err := app.LinkExistingApp(ctx, clients, linkedApp); err != nil {
return err
}
clients.IO.PrintInfo(ctx, false, "%s", style.Sectionf(style.TextSection{
Emoji: "house",
Text: "App",
Secondary: app.FormatListSuccess([]types.App{*linkedApp}),
}))
Comment on lines +236 to +240

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

🌟 praise: Inlining this output within create seems to've fixed an extra linebreak!

}

printCreateSuccess(ctx, clients, appDirPath)
Expand Down
24 changes: 20 additions & 4 deletions cmd/project/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ import (
"github.com/spf13/cobra"
)

const linkAppConfirmPromptText = "Do you want to add an existing app?"

// NewInitCommand returns a Cobra command to initialize projects with Slack CLI support
func NewInitCommand(clients *shared.ClientFactory) *cobra.Command {
cmd := &cobra.Command{
Expand Down Expand Up @@ -109,11 +111,25 @@ func projectInitCommandRunE(clients *shared.ClientFactory, cmd *cobra.Command, a
// Existing projects initialized always default to config.ManifestSourceLocal.
_ = create.InstallProjectDependencies(ctx, clients, projectDirPath)

// Add an existing app to the project
err = app.LinkExistingApp(ctx, clients, &types.App{}, true)
// Prompt to add an existing app to the project
app.LinkAppHeaderSection(ctx, clients)
proceed, err := clients.IO.ConfirmPrompt(ctx, linkAppConfirmPromptText, true)
if err != nil {
// Display the error but continue to init
clients.IO.PrintError(ctx, "%s", err.Error())
return err
}
if proceed {
linkedApp := &types.App{}
err = app.LinkExistingApp(ctx, clients, linkedApp)
if err != nil {
// Display the error but continue to init
clients.IO.PrintError(ctx, "%s", err.Error())
} else {
clients.IO.PrintInfo(ctx, false, "%s", style.Sectionf(style.TextSection{
Emoji: "house",
Text: "App",
Secondary: app.FormatListSuccess([]types.App{*linkedApp}),
}))
}
}

printNextStepSection(ctx, clients, projectDirPath)
Expand Down
9 changes: 4 additions & 5 deletions cmd/project/init_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import (
"path/filepath"
"testing"

"github.com/slackapi/slack-cli/cmd/app"
"github.com/slackapi/slack-cli/internal/api"
internalApp "github.com/slackapi/slack-cli/internal/app"
"github.com/slackapi/slack-cli/internal/iostreams"
Expand Down Expand Up @@ -58,7 +57,7 @@ func Test_Project_InitCommand(t *testing.T) {
Setup: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock, cf *shared.ClientFactory) {
setupProjectInitCommandMocks(t, ctx, cm, cf)
// Do not link an existing app
cm.IO.On("ConfirmPrompt", mock.Anything, app.LinkAppConfirmPromptText, mock.Anything).Return(false, nil)
cm.IO.On("ConfirmPrompt", mock.Anything, linkAppConfirmPromptText, mock.Anything).Return(false, nil)
},
ExpectedStdoutOutputs: []string{
"Project Initialization", // Assert section header
Expand All @@ -78,7 +77,7 @@ func Test_Project_InitCommand(t *testing.T) {
t,
"ConfirmPrompt",
mock.Anything,
app.LinkAppConfirmPromptText,
linkAppConfirmPromptText,
mock.Anything,
)
// Assert Trace
Expand Down Expand Up @@ -109,7 +108,7 @@ func Test_Project_InitCommand(t *testing.T) {
// Default setup
setupProjectInitCommandMocks(t, ctx, cm, cf)
// Link an existing app
cm.IO.On("ConfirmPrompt", mock.Anything, app.LinkAppConfirmPromptText, mock.Anything).Return(true, nil)
cm.IO.On("ConfirmPrompt", mock.Anything, linkAppConfirmPromptText, mock.Anything).Return(true, nil)
// Mock prompt for team
cm.IO.On("SelectPrompt",
mock.Anything,
Expand Down Expand Up @@ -156,7 +155,7 @@ func Test_Project_InitCommand(t *testing.T) {
// Assert prompt to add existing apps was called
cm.IO.AssertCalled(t, "ConfirmPrompt",
mock.Anything,
app.LinkAppConfirmPromptText,
linkAppConfirmPromptText,
mock.Anything,
)
// Assert prompt for team
Expand Down
Loading