From 277f886b3ffb483cc1f1d9f077672bc8d687efa2 Mon Sep 17 00:00:00 2001 From: Doug Lauder Date: Fri, 13 Nov 2020 21:10:07 -0500 Subject: [PATCH] MM-30212 Support dynamic autocomplete for built-in slash commands (#16196) Support added for dynamic auto-completion lists for built-in slash commands. * a moniker within the fetch url determines whether the request should be sent via HTTP(S) or routed internally to a built-in command. The moniker is of the form builtin: followed by the name of the built-in command to use. e.g. builtin:share would be become a call to the share command to fetch auto-completion items. * command providers implement the optional DynamicListProvider interface to indicate they can provide dynamic auto-completion. --- app/command_autocomplete.go | 37 ++++++++++++++++++ app/command_autocomplete_test.go | 65 ++++++++++++++++++++++++++++++++ 2 files changed, 102 insertions(+) diff --git a/app/command_autocomplete.go b/app/command_autocomplete.go index 80adbbef15..dbf8c03952 100644 --- a/app/command_autocomplete.go +++ b/app/command_autocomplete.go @@ -4,6 +4,8 @@ package app import ( + "errors" + "fmt" "net/url" "sort" "strings" @@ -12,6 +14,11 @@ import ( "github.com/mattermost/mattermost-server/v5/model" ) +// AutocompleteDynamicArgProvider dynamically provides auto-completion args for built-in commands. +type AutocompleteDynamicArgProvider interface { + GetAutoCompleteListItems(a *App, commandArgs *model.CommandArgs, arg *model.AutocompleteArg, parsed, toBeParsed string) ([]model.AutocompleteListItem, error) +} + // GetSuggestions returns suggestions for user input. func (a *App) GetSuggestions(commandArgs *model.CommandArgs, commands []*model.Command, roleID string) []model.AutocompleteSuggestion { sort.Slice(commands, func(i, j int) bool { @@ -233,6 +240,15 @@ func parseStaticListArgument(arg *model.AutocompleteArg, parsed, toBeParsed stri func (a *App) getDynamicListArgument(commandArgs *model.CommandArgs, arg *model.AutocompleteArg, parsed, toBeParsed string) (found bool, alreadyParsed string, yetToBeParsed string, suggestions []model.AutocompleteSuggestion) { dynamicArg := arg.Data.(*model.AutocompleteDynamicListArg) + if strings.HasPrefix(dynamicArg.FetchURL, "builtin:") { + listItems, err := a.getBuiltinDynamicListArgument(commandArgs, arg, parsed, toBeParsed) + if err != nil { + a.Log().Error("Can't fetch dynamic list arguments for", mlog.String("url", dynamicArg.FetchURL), mlog.Err(err)) + return false, parsed, toBeParsed, []model.AutocompleteSuggestion{} + } + return parseListItems(listItems, parsed, toBeParsed) + } + params := url.Values{} params.Add("user_input", parsed+toBeParsed) params.Add("parsed", parsed) @@ -286,3 +302,24 @@ func parseListItems(items []model.AutocompleteListItem, parsed, toBeParsed strin } return true, parsed + toBeParsed, "", suggestions } + +func (a *App) getBuiltinDynamicListArgument(commandArgs *model.CommandArgs, arg *model.AutocompleteArg, parsed, toBeParsed string) ([]model.AutocompleteListItem, error) { + dynamicArg := arg.Data.(*model.AutocompleteDynamicListArg) + arr := strings.Split(dynamicArg.FetchURL, ":") + if len(arr) < 2 { + return nil, errors.New("Dynamic list URL missing built-in command name") + } + cmdName := arr[1] + + provider := GetCommandProvider(cmdName) + if provider == nil { + return nil, fmt.Errorf("No command provider for %s", cmdName) + } + + dp, ok := provider.(AutocompleteDynamicArgProvider) + if !ok { + return nil, fmt.Errorf("Auto-completion not available for built-in command %s", cmdName) + } + + return dp.GetAutoCompleteListItems(a, commandArgs, arg, parsed, toBeParsed) +} diff --git a/app/command_autocomplete_test.go b/app/command_autocomplete_test.go index 211c9bc35f..622ded3d79 100644 --- a/app/command_autocomplete_test.go +++ b/app/command_autocomplete_test.go @@ -4,8 +4,10 @@ package app import ( + "fmt" "testing" + goi18n "github.com/mattermost/go-i18n/i18n" "github.com/mattermost/mattermost-server/v5/model" "github.com/stretchr/testify/assert" ) @@ -609,3 +611,66 @@ func createJiraAutocompleteData() *model.AutocompleteData { return jira } + +func TestDynamicListArgsForBuiltin(t *testing.T) { + th := Setup(t) + defer th.TearDown() + + provider := &testProvider{} + RegisterCommandProvider(provider) + + command := provider.GetCommand(th.App, nil) + emptyCmdArgs := &model.CommandArgs{} + + t.Run("GetAutoCompleteListItems", func(t *testing.T) { + suggestions := th.App.getSuggestions(emptyCmdArgs, []*model.AutocompleteData{command.AutocompleteData}, "", "bogus --dynaArg ", model.SYSTEM_ADMIN_ROLE_ID) + assert.Len(t, suggestions, 3) + assert.Equal(t, "this is hint 1", suggestions[0].Hint) + assert.Equal(t, "this is hint 2", suggestions[1].Hint) + assert.Equal(t, "this is hint 3", suggestions[2].Hint) + }) + + t.Run("GetAutoCompleteListItems bad arg", func(t *testing.T) { + suggestions := th.App.getSuggestions(emptyCmdArgs, []*model.AutocompleteData{command.AutocompleteData}, "", "bogus --badArg ", model.SYSTEM_ADMIN_ROLE_ID) + assert.Len(t, suggestions, 0) + }) +} + +type testProvider struct { +} + +func (p *testProvider) GetTrigger() string { + return "bogus" +} + +func (p *testProvider) GetCommand(a *App, T goi18n.TranslateFunc) *model.Command { + top := model.NewAutocompleteData(p.GetTrigger(), "[command]", "Just a test.") + top.AddNamedDynamicListArgument("dynaArg", "A dynamic list", "builtin:bogus", true) + + return &model.Command{ + Trigger: p.GetTrigger(), + AutoComplete: true, + AutoCompleteDesc: "Test description", + AutoCompleteHint: "Test hint.", + DisplayName: "test display name", + AutocompleteData: top, + } +} + +func (p *testProvider) DoCommand(a *App, args *model.CommandArgs, message string) *model.CommandResponse { + return &model.CommandResponse{ + Text: "I do nothing!", + ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL, + } +} + +func (p *testProvider) GetAutoCompleteListItems(a *App, commandArgs *model.CommandArgs, arg *model.AutocompleteArg, parsed, toBeParsed string) ([]model.AutocompleteListItem, error) { + if arg.Name == "dynaArg" { + return []model.AutocompleteListItem{ + {Item: "item1", Hint: "this is hint 1", HelpText: "This is help text 1."}, + {Item: "item2", Hint: "this is hint 2", HelpText: "This is help text 2."}, + {Item: "item3", Hint: "this is hint 3", HelpText: "This is help text 3."}, + }, nil + } + return nil, fmt.Errorf("%s not a dynamic argument", arg.Name) +}