Skip to content

Commit 4700a9f

Browse files
authored
[MI-2193]: Made changes on server to support filters and refactored all the commands (#79)
* [MI-2193]: Added flags on server in list subscription slash command to support filters. * [MI-2193]: Made changes on server to support filters and refactored all the commands * [MI-2193]: Fixed linting * [MI-2193]: Fixed linting * [MI-2193]: Review fixes: Minor refactoring * [MI-2193]: Review fixes: Changes slash command name and description Co-authored-by: Abhishek Verma <[email protected]>
1 parent 2c7f953 commit 4700a9f

File tree

6 files changed

+127
-68
lines changed

6 files changed

+127
-68
lines changed

server/constants/constants.go

+15-2
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,20 @@ const (
1818
"* `/azuredevops disconnect` - Disconnect your Mattermost account from your Azure DevOps account.\n" +
1919
"* `/azuredevops link [projectURL]` - Link your project to a current channel.\n" +
2020
"* `/azuredevops boards create [title] [description]` - Create a new task for your project.\n" +
21-
"* `/azuredevops subscribe` - Create subscriptions to track changes in tasks for your linked projects.\n"
22-
InvalidCommand = "Invalid command parameters. Please use `/azuredevops help` for more information."
21+
"* `/azuredevops boards subscription add` - Add a new Boards subscription for your linked projects.\n" +
22+
"* `/azuredevops boards subscription list [me or anyone] [all_channels]` - View Boards subscriptions.\n" +
23+
"* `/azuredevops boards subscription delete [subscription id]` - Unsubscribe a Boards subscription"
24+
InvalidCommand = "Invalid command parameters. Please use `/azuredevops help` for more information."
25+
CommandHelp = "help"
26+
CommandConnect = "connect"
27+
CommandDisconnect = "disconnect"
28+
CommandLink = "link"
29+
CommandBoards = "boards"
30+
CommandCreate = "create"
31+
CommandSubscription = "subscription"
32+
CommandAdd = "add"
33+
CommandList = "list"
34+
CommandDelete = "delete"
2335

2436
// Get task link preview constants
2537
HTTPS = "https:"
@@ -54,6 +66,7 @@ const (
5466
// Filters
5567
FilterCreatedByMe = "me"
5668
FilterCreatedByAnyone = "anyone"
69+
FilterAllChannels = "all_channels"
5770

5871
DefaultPage = 0
5972
DefaultPerPageLimit = 50

server/constants/messages.go

+1
Original file line numberDiff line numberDiff line change
@@ -61,4 +61,5 @@ const (
6161
GetChannelError = "Error in getting channels for team and user"
6262
GetUserError = "Error in getting Mattermost user details"
6363
InvalidPaginationQueryParam = "Invalid value for query param(s) page or per_page"
64+
ErrorAdminAccess = "Can not delete the subscription, looks like you do not have access to add/delete a subscription for this project. Please make sure you are a project or team administrator for this project"
6465
)

server/plugin/command.go

+82-37
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package plugin
22

33
import (
44
"fmt"
5+
"net/http"
56
"strings"
67

78
"github.com/pkg/errors"
@@ -22,11 +23,11 @@ type Handler struct {
2223

2324
var azureDevopsCommandHandler = Handler{
2425
handlers: map[string]HandlerFunc{
25-
"help": azureDevopsHelpCommand,
26-
"connect": azureDevopsConnectCommand,
27-
"disconnect": azureDevopsDisconnectCommand,
28-
"link": azureDevopsAccountConnectionCheck,
29-
"boards": azureDevopsAccountConnectionCheck,
26+
constants.CommandHelp: azureDevopsHelpCommand,
27+
constants.CommandConnect: azureDevopsConnectCommand,
28+
constants.CommandDisconnect: azureDevopsDisconnectCommand,
29+
constants.CommandLink: azureDevopsAccountConnectionCheck,
30+
constants.CommandBoards: azureDevopsBoardsCommand,
3031
},
3132
defaultHandler: executeDefault,
3233
}
@@ -45,31 +46,44 @@ func (ch *Handler) Handle(p *Plugin, c *plugin.Context, commandArgs *model.Comma
4546
}
4647

4748
func (p *Plugin) getAutoCompleteData() *model.AutocompleteData {
48-
azureDevops := model.NewAutocompleteData(constants.CommandTriggerName, "[command]", "Available commands: help, connect, disconnect, create, link, subscribe, subscriptions, unsubscribe")
49+
azureDevops := model.NewAutocompleteData(constants.CommandTriggerName, "[command]", fmt.Sprintf("Available commands: %s, %s, %s, %s, %s", constants.CommandHelp, constants.CommandConnect, constants.CommandDisconnect, constants.CommandLink, constants.CommandBoards))
4950

50-
help := model.NewAutocompleteData("help", "", fmt.Sprintf("Show %s slash command help", constants.CommandTriggerName))
51+
help := model.NewAutocompleteData(constants.CommandHelp, "", fmt.Sprintf("Show %s slash command help", constants.CommandTriggerName))
5152
azureDevops.AddCommand(help)
5253

53-
connect := model.NewAutocompleteData("connect", "", "Connect to your Azure DevOps account")
54+
connect := model.NewAutocompleteData(constants.CommandConnect, "", "Connect to your Azure DevOps account")
5455
azureDevops.AddCommand(connect)
5556

56-
disconnect := model.NewAutocompleteData("disconnect", "", "Disconnect your Azure DevOps account")
57+
disconnect := model.NewAutocompleteData(constants.CommandDisconnect, "", "Disconnect your Azure DevOps account")
5758
azureDevops.AddCommand(disconnect)
5859

59-
link := model.NewAutocompleteData("link", "[projectURL]", "Link a project")
60+
link := model.NewAutocompleteData(constants.CommandLink, "", "Link a project")
61+
link.AddTextArgument("URL of the project to be linked", "[projectURL]", "")
6062
azureDevops.AddCommand(link)
6163

62-
create := model.NewAutocompleteData("boards create [title] [description]", "", "Create a new task")
63-
azureDevops.AddCommand(create)
64-
65-
subscribe := model.NewAutocompleteData("boards subscribe", "", "Add a boards subscription")
66-
azureDevops.AddCommand(subscribe)
67-
68-
subscriptions := model.NewAutocompleteData("boards subscriptions", "", "View board's subscriptions in the current channel")
69-
azureDevops.AddCommand(subscriptions)
70-
71-
unsubscribe := model.NewAutocompleteData("boards unsubscribe [subscription id]", "", "Unsubscribe a board subscription")
72-
azureDevops.AddCommand(unsubscribe)
64+
subscription := model.NewAutocompleteData(constants.CommandSubscription, "", "Add/list/unsubscribe subscriptions")
65+
subscriptionAdd := model.NewAutocompleteData(constants.CommandAdd, "", "Add a new subscription")
66+
subscriptionView := model.NewAutocompleteData(constants.CommandList, "", "List subscriptions")
67+
subscriptionUnsubscribe := model.NewAutocompleteData(constants.CommandDelete, "", "Unsubscribe a subscription")
68+
subscriptionUnsubscribe.AddTextArgument("ID of the subscription to be deleted", "[subscription id]", "")
69+
subscriptionCreatedByMe := model.NewAutocompleteData(constants.FilterCreatedByMe, "", "Created By Me")
70+
subscriptionShowForAllChannels := model.NewAutocompleteData(constants.FilterAllChannels, "", "Show for all channels or You can leave this argument to show for the current channel only")
71+
subscriptionCreatedByMe.AddCommand(subscriptionShowForAllChannels)
72+
subscriptionView.AddCommand(subscriptionCreatedByMe)
73+
subscriptionCreatedByAnyone := model.NewAutocompleteData(constants.FilterCreatedByAnyone, "", "Created By Anyone")
74+
subscriptionCreatedByAnyone.AddCommand(subscriptionShowForAllChannels)
75+
subscriptionView.AddCommand(subscriptionCreatedByAnyone)
76+
subscription.AddCommand(subscriptionAdd)
77+
subscription.AddCommand(subscriptionView)
78+
subscription.AddCommand(subscriptionUnsubscribe)
79+
80+
boards := model.NewAutocompleteData(constants.CommandBoards, "", "Create a new work-item or add/list/delete subscriptions")
81+
create := model.NewAutocompleteData(constants.CommandCreate, "", "Create a new work-item")
82+
create.AddTextArgument("Title", "[title]", "")
83+
create.AddTextArgument("Description", "[description]", "")
84+
boards.AddCommand(create)
85+
boards.AddCommand(subscription)
86+
azureDevops.AddCommand(boards)
7387

7488
return azureDevops
7589
}
@@ -83,7 +97,7 @@ func (p *Plugin) getCommand() (*model.Command, error) {
8397
return &model.Command{
8498
Trigger: constants.CommandTriggerName,
8599
AutoComplete: true,
86-
AutoCompleteDesc: "Available commands: help",
100+
AutoCompleteDesc: fmt.Sprintf("Available commands: %s, %s, %s, %s, %s", constants.CommandHelp, constants.CommandConnect, constants.CommandDisconnect, constants.CommandLink, constants.CommandBoards),
87101
AutoCompleteHint: "[command]",
88102
AutocompleteData: p.getAutoCompleteData(),
89103
AutocompleteIconData: iconData,
@@ -94,37 +108,59 @@ func azureDevopsAccountConnectionCheck(p *Plugin, c *plugin.Context, commandArgs
94108
if isConnected := p.UserAlreadyConnected(commandArgs.UserId); !isConnected {
95109
return p.sendEphemeralPostForCommand(commandArgs, p.getConnectAccountFirstMessage())
96110
}
111+
return &model.CommandResponse{}, nil
112+
}
113+
114+
func azureDevopsBoardsCommand(p *Plugin, c *plugin.Context, commandArgs *model.CommandArgs, args ...string) (*model.CommandResponse, *model.AppError) {
115+
// Check if user's Azure DevOps account is connected
116+
if isConnected := p.UserAlreadyConnected(commandArgs.UserId); !isConnected {
117+
return p.sendEphemeralPostForCommand(commandArgs, p.getConnectAccountFirstMessage())
118+
}
97119

98-
if len(args) > 0 {
99-
switch {
100-
case args[0] == "subscriptions":
101-
return azureDevopsListSubscriptionsCommand(p, c, commandArgs, args...)
102-
case args[0] == "unsubscribe":
120+
// Validate commands and their arguments
121+
switch {
122+
case len(args) >= 1 && args[0] == constants.CommandCreate:
123+
return &model.CommandResponse{}, nil
124+
// For "subscription" command there must be at least 2 arguments
125+
case len(args) >= 2 && args[0] == constants.CommandSubscription:
126+
switch args[1] {
127+
case constants.CommandList:
128+
// For "list" command there must be at least 3 arguments
129+
if len(args) >= 3 && (args[2] == constants.FilterCreatedByMe || args[2] == constants.FilterCreatedByAnyone) {
130+
return azureDevopsListSubscriptionsCommand(p, c, commandArgs, args...)
131+
}
132+
case constants.CommandDelete:
103133
return azureDevopsUnsubscribeCommand(p, c, commandArgs, args...)
134+
case constants.CommandAdd:
135+
return &model.CommandResponse{}, nil
104136
}
105137
}
106138

107-
return &model.CommandResponse{}, nil
139+
return executeDefault(p, c, commandArgs, args...)
108140
}
109141

110142
func azureDevopsUnsubscribeCommand(p *Plugin, c *plugin.Context, commandArgs *model.CommandArgs, args ...string) (*model.CommandResponse, *model.AppError) {
111-
if len(args) < 2 {
143+
if len(args) < 3 {
112144
return p.sendEphemeralPostForCommand(commandArgs, "Subscription ID is not provided")
113145
}
114146

115-
subscriptionList, err := p.Store.GetAllSubscriptions(commandArgs.UserId)
147+
subscriptionList, err := p.Store.GetAllSubscriptions("")
116148
if err != nil {
117149
p.API.LogError(constants.FetchSubscriptionListError, "Error", err.Error())
118150
return p.sendEphemeralPostForCommand(commandArgs, constants.GenericErrorMessage)
119151
}
120152

153+
subscriptionIDToBeDeleted := args[2]
121154
for _, subscription := range subscriptionList {
122-
if subscription.SubscriptionID == args[1] {
123-
if _, err := p.sendEphemeralPostForCommand(commandArgs, fmt.Sprintf("Boards subscription with ID: %q is being deleted", args[1])); err != nil {
155+
if subscription.SubscriptionID == subscriptionIDToBeDeleted {
156+
if _, err := p.sendEphemeralPostForCommand(commandArgs, fmt.Sprintf("Boards subscription with ID: %q is being deleted", subscriptionIDToBeDeleted)); err != nil {
124157
p.API.LogError("Error in sending ephemeral post", "Error", err.Error())
125158
}
126159

127-
if _, err := p.Client.DeleteSubscription(subscription.OrganizationName, subscription.SubscriptionID, commandArgs.UserId); err != nil {
160+
if statusCode, err := p.Client.DeleteSubscription(subscription.OrganizationName, subscription.SubscriptionID, commandArgs.UserId); err != nil {
161+
if statusCode == http.StatusForbidden {
162+
return p.sendEphemeralPostForCommand(commandArgs, constants.ErrorAdminAccess)
163+
}
128164
p.API.LogError("Error in deleting subscription", "Error", err.Error())
129165
return p.sendEphemeralPostForCommand(commandArgs, constants.GenericErrorMessage)
130166
}
@@ -140,21 +176,30 @@ func azureDevopsUnsubscribeCommand(p *Plugin, c *plugin.Context, commandArgs *mo
140176
&model.WebsocketBroadcast{UserId: commandArgs.UserId},
141177
)
142178

143-
return p.sendEphemeralPostForCommand(commandArgs, fmt.Sprintf("Boards subscription with ID: %q is successfully deleted", args[1]))
179+
return p.sendEphemeralPostForCommand(commandArgs, fmt.Sprintf("Boards subscription with ID: %q is successfully deleted", subscriptionIDToBeDeleted))
144180
}
145181
}
146182

147-
return p.sendEphemeralPostForCommand(commandArgs, fmt.Sprintf("Boards subscription with ID: %q does not exist", args[1]))
183+
return p.sendEphemeralPostForCommand(commandArgs, fmt.Sprintf("Boards subscription with ID: %q does not exist", subscriptionIDToBeDeleted))
148184
}
149185

150186
func azureDevopsListSubscriptionsCommand(p *Plugin, c *plugin.Context, commandArgs *model.CommandArgs, args ...string) (*model.CommandResponse, *model.AppError) {
151-
subscriptionList, err := p.Store.GetAllSubscriptions(commandArgs.UserId)
187+
// If 4th argument is present then it must be "all_channels"
188+
if len(args) >= 4 && args[3] != constants.FilterAllChannels {
189+
return executeDefault(p, c, commandArgs, args...)
190+
}
191+
192+
subscriptionList, err := p.Store.GetAllSubscriptions("")
152193
if err != nil {
153194
p.API.LogError(constants.FetchSubscriptionListError, "Error", err.Error())
154195
return p.sendEphemeralPostForCommand(commandArgs, constants.GenericErrorMessage)
155196
}
156197

157-
return p.sendEphemeralPostForCommand(commandArgs, p.ParseSubscriptionsToCommandResponse(subscriptionList, commandArgs.ChannelId))
198+
showForChannelID := commandArgs.ChannelId
199+
if len(args) >= 4 && args[3] == constants.FilterAllChannels {
200+
showForChannelID = ""
201+
}
202+
return p.sendEphemeralPostForCommand(commandArgs, p.ParseSubscriptionsToCommandResponse(subscriptionList, showForChannelID, args[2], commandArgs.UserId))
158203
}
159204

160205
func azureDevopsHelpCommand(p *Plugin, c *plugin.Context, commandArgs *model.CommandArgs, args ...string) (*model.CommandResponse, *model.AppError) {

server/plugin/utils.go

+24-15
Original file line numberDiff line numberDiff line change
@@ -208,32 +208,41 @@ func (p *Plugin) getConnectAccountFirstMessage() string {
208208
return fmt.Sprintf(constants.ConnectAccountFirst, fmt.Sprintf(constants.ConnectAccount, p.GetPluginURLPath(), constants.PathOAuthConnect))
209209
}
210210

211-
func (p *Plugin) ParseSubscriptionsToCommandResponse(subscriptionsList []*serializers.SubscriptionDetails, channelID string) string {
211+
func (p *Plugin) ParseSubscriptionsToCommandResponse(subscriptionsList []*serializers.SubscriptionDetails, channelID, createdBy, userID string) string {
212212
var sb strings.Builder
213213

214214
if len(subscriptionsList) == 0 {
215215
sb.WriteString(constants.NoSubscriptionFound)
216216
return sb.String()
217217
}
218218

219-
sb.WriteString("###### Board subscription(s) for this channel\n")
220-
sb.WriteString("| Subscription ID | Organization | Project | Event Type |\n")
221-
sb.WriteString("| :-------------- | :----------- | :------ | :--------- |\n")
219+
sb.WriteString("###### Boards subscription(s)\n")
220+
sb.WriteString("| Subscription ID | Organization | Project | Event Type | Created By | Channel |\n")
221+
sb.WriteString("| :-------------- | :----------- | :------ | :--------- | :--------- | :------ |\n")
222222

223223
noSubscriptionFound := true
224224
for _, subscription := range subscriptionsList {
225-
if subscription.ChannelID == channelID {
226-
noSubscriptionFound = false
227-
displayEventType := ""
228-
switch {
229-
case subscription.EventType == "create":
230-
displayEventType = "Work Item Created"
231-
case subscription.EventType == "update":
232-
displayEventType = "Work Item Updated"
233-
case subscription.EventType == "delete":
234-
displayEventType = "Work Item Deleted"
225+
displayEventType := ""
226+
switch subscription.EventType {
227+
case "create":
228+
displayEventType = "Work Item Created"
229+
case "update":
230+
displayEventType = "Work Item Updated"
231+
case "delete":
232+
displayEventType = "Work Item Deleted"
233+
}
234+
235+
if channelID == "" || subscription.ChannelID == channelID {
236+
switch createdBy {
237+
case constants.FilterCreatedByMe:
238+
if subscription.MattermostUserID == userID {
239+
noSubscriptionFound = false
240+
sb.WriteString(fmt.Sprintf("| %s | %s | %s | %s | %s | %s |\n", subscription.SubscriptionID, subscription.OrganizationName, subscription.ProjectName, displayEventType, subscription.CreatedBy, subscription.ChannelName))
241+
}
242+
case constants.FilterCreatedByAnyone:
243+
noSubscriptionFound = false
244+
sb.WriteString(fmt.Sprintf("| %s | %s | %s | %s | %s | %s |\n", subscription.SubscriptionID, subscription.OrganizationName, subscription.ProjectName, displayEventType, subscription.CreatedBy, subscription.ChannelName))
235245
}
236-
sb.WriteString(fmt.Sprintf("| %s | %s | %s | %s |\n", subscription.SubscriptionID, subscription.OrganizationName, subscription.ProjectName, displayEventType))
237246
}
238247
}
239248

webapp/src/hooks/index.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ export default class Hooks {
3030
});
3131
}
3232

33-
if (commandTrimmed && commandTrimmed.startsWith('/azuredevops boards subscribe')) {
33+
if (commandTrimmed && commandTrimmed.startsWith('/azuredevops boards subscription add')) {
3434
const commandArgs = getCommandArgs(commandTrimmed);
3535
this.store.dispatch(setGlobalModalState({modalId: 'subscribeProject', commandArgs}));
3636
return {

webapp/src/utils/index.ts

+4-13
Original file line numberDiff line numberDiff line change
@@ -44,19 +44,10 @@ export const getProjectLinkModalArgs = (str: string): LinkPayload => {
4444
};
4545
};
4646

47-
export const getCreateTaskModalCommandArgs = (arr: Array<string>): CreateTaskCommandArgs => {
48-
if (arr.length < 3) {
49-
return {
50-
title: '',
51-
description: '',
52-
};
53-
}
54-
55-
return {
56-
title: arr[1] ?? '',
57-
description: arr[2] ?? '',
58-
};
59-
};
47+
export const getCreateTaskModalCommandArgs = (arr: Array<string>): CreateTaskCommandArgs => ({
48+
title: arr[1] ?? '',
49+
description: arr[2] ?? '',
50+
});
6051

6152
export const onPressingEnterKey = (event: React.KeyboardEvent<HTMLSpanElement> | React.KeyboardEvent<SVGSVGElement>, func: () => void) => {
6253
if (event.key !== 'Enter' && event.key !== ' ') {

0 commit comments

Comments
 (0)