Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[MI-2061]: Refactoring and optimisations around Forms, RTK query, and types and constants management #45

Merged
merged 32 commits into from
Sep 5, 2022
Merged
Changes from 1 commit
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
9f65a10
[MI-1986]: Create plugin API to fetch linked projects list
avas27JTG Aug 11, 2022
e1793e4
[MI-1987]: Integrated project list UI
avas27JTG Aug 11, 2022
0110bdc
[MI-1987]: Review fixes
avas27JTG Aug 11, 2022
ff267f5
Merge branch 'master' of github.com:Brightscout/mattermost-plugin-azu…
avas27JTG Aug 12, 2022
8dc6f80
[MI-2001]: [MI-2001]: Create plugin API to unlink project and integra…
avas27JTG Aug 12, 2022
9637e82
[MI-2001]: Review fixes
avas27JTG Aug 12, 2022
15cee42
[MI-2002]: Created plugin API to fetch user details and UI integratio…
avas27JTG Aug 12, 2022
84397d0
[MI-2002]: Updated API paths
avas27JTG Aug 12, 2022
5fb9155
[MI-2049]: Added websocket support to detect user connection details …
avas27JTG Aug 12, 2022
268bc79
[MI-2010]: API to create subscriptions
ayusht2810 Aug 16, 2022
bb12691
[MI-2010] Fix lint errors
ayusht2810 Aug 16, 2022
283e1c2
[MI-2035]: Integrated unlinking project from details page
avas27JTG Aug 16, 2022
f7ff1b6
[MI-1939]: Added refresh token logic
avas27JTG Aug 16, 2022
8bc372b
[MI-2009] API to get list of subscriptions
ayusht2810 Aug 16, 2022
37bc024
[MI-2011] Add API to listen notifications
ayusht2810 Aug 16, 2022
4af24be
[MI-2023] API to delete subscriptions
ayusht2810 Aug 16, 2022
f516a00
[MI-2023] Remove print statement
ayusht2810 Aug 16, 2022
63a3628
[MI-1939]: Fixed statusCode
avas27JTG Aug 16, 2022
a61dd77
[MI-2029] Add feature to create subscription from modal
ayusht2810 Aug 16, 2022
df6ce21
[MI-2030] Add filter to fetch subscriptions related to project
ayusht2810 Aug 16, 2022
4ecb8e7
[MI-2056]: Fixed create task flow and added user connection check on …
avas27JTG Aug 16, 2022
ca65519
[MI-2029_1] Update subscription modal
ayusht2810 Aug 16, 2022
50b04db
Pull from 'MI-2029_1'
ayusht2810 Aug 17, 2022
ee631b6
[MI-2057] Integrate subscription list page
ayusht2810 Aug 17, 2022
111fe53
Release-2
ayusht2810 Aug 17, 2022
c9b6257
Remove nolint comment
ayusht2810 Aug 17, 2022
7d35921
[MI-2060]: Refactor code for release and make required changes
avas27JTG Aug 17, 2022
c8e94b6
[MI-2061]: Refactor code base and Explore and implement RTK error han…
avas27JTG Aug 22, 2022
86d00f8
[MI-2061]: Added logic to handle error/success for multiple API calls…
avas27JTG Aug 22, 2022
99ce634
[MI-2061]: Review fix
avas27JTG Sep 5, 2022
519dca2
Merge branch 'master' of github.com:Brightscout/mattermost-plugin-azu…
avas27JTG Sep 5, 2022
387b039
[MI-2061]: lint fixes
avas27JTG Sep 5, 2022
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
Prev Previous commit
Next Next commit
[MI-2029] Add feature to create subscription from modal
  • Loading branch information
ayusht2810 committed Aug 16, 2022

Verified

This commit was signed with the committer’s verified signature.
frapell Franco Pellegrini
commit a61dd77da3bb5d0c27f74ee2a7e16dd8531ef691
5 changes: 3 additions & 2 deletions server/constants/constants.go
Original file line number Diff line number Diff line change
@@ -10,8 +10,6 @@ const (
PluginID = "mattermost-plugin-azure-devops"
ChannelID = "channel_id"
HeaderMattermostUserID = "Mattermost-User-ID"
// TODO: Change later according to the needs.
HeaderMattermostUserIDAPI = "User-ID"

// Command configs
CommandTriggerName = "azuredevops"
@@ -38,6 +36,9 @@ const (
Update = "update"
Delete = "delete"

// Path params
PathParamTeamID = "team_id"

// Authorization constants
Bearer = "Bearer"
Authorization = "Authorization"
1 change: 0 additions & 1 deletion server/constants/messages.go
Original file line number Diff line number Diff line change
@@ -21,7 +21,6 @@ const (
TaskTypeRequired = "task type is required"
TaskTitleRequired = "task title is required"
EventTypeRequired = "event type is required"
ChannelNameRequired = "channel name is required"
ChannelIDRequired = "channel ID is required"
)

1 change: 1 addition & 0 deletions server/constants/routes.go
Original file line number Diff line number Diff line change
@@ -14,6 +14,7 @@ const (
PathLinkProject = "/link"
PathSubscriptions = "/subscriptions"
PathSubscriptionNotifications = "/notification"
PathGetUserChannelsForTeam = "/channels/{team_id:[A-Za-z0-9]+}"

// Azure API paths
CreateTask = "/%s/%s/_apis/wit/workitems/$%s?api-version=7.1-preview.3"
75 changes: 54 additions & 21 deletions server/plugin/api.go
Original file line number Diff line number Diff line change
@@ -45,11 +45,12 @@ func (p *Plugin) InitRoutes() {
s.HandleFunc(constants.PathSubscriptions, p.handleAuthRequired(p.checkOAuth(p.handleGetSubscriptions))).Methods(http.MethodGet)
s.HandleFunc(constants.PathSubscriptionNotifications, p.handleSubscriptionNotifications).Methods(http.MethodPost)
s.HandleFunc(constants.PathSubscriptions, p.handleAuthRequired(p.checkOAuth(p.handleDeleteSubscriptions))).Methods(http.MethodDelete)
s.HandleFunc(constants.PathGetUserChannelsForTeam, p.handleAuthRequired(p.getUserChannelsForTeam)).Methods(http.MethodGet)
}

// API to create task of a project in an organization.
func (p *Plugin) handleCreateTask(w http.ResponseWriter, r *http.Request) {
mattermostUserID := r.Header.Get(constants.HeaderMattermostUserIDAPI)
mattermostUserID := r.Header.Get(constants.HeaderMattermostUserID)

body, err := serializers.CreateTaskRequestPayloadFromJSON(r.Body)
if err != nil {
@@ -87,7 +88,7 @@ func (p *Plugin) handleCreateTask(w http.ResponseWriter, r *http.Request) {

// API to link a project and an organization to a user.
func (p *Plugin) handleLink(w http.ResponseWriter, r *http.Request) {
mattermostUserID := r.Header.Get(constants.HeaderMattermostUserIDAPI)
mattermostUserID := r.Header.Get(constants.HeaderMattermostUserID)
var body *serializers.LinkRequestPayload
decoder := json.NewDecoder(r.Body)
if err := decoder.Decode(&body); err != nil {
@@ -135,7 +136,7 @@ func (p *Plugin) handleLink(w http.ResponseWriter, r *http.Request) {

// handleGetAllLinkedProjects returns all linked projects list
func (p *Plugin) handleGetAllLinkedProjects(w http.ResponseWriter, r *http.Request) {
mattermostUserID := r.Header.Get(constants.HeaderMattermostUserIDAPI)
mattermostUserID := r.Header.Get(constants.HeaderMattermostUserID)
projectList, err := p.Store.GetAllProjects(mattermostUserID)
if err != nil {
p.API.LogError(constants.ErrorFetchProjectList, "Error", err.Error())
@@ -166,7 +167,7 @@ func (p *Plugin) handleGetAllLinkedProjects(w http.ResponseWriter, r *http.Reque

// handleUnlinkProject unlinks a project
func (p *Plugin) handleUnlinkProject(w http.ResponseWriter, r *http.Request) {
mattermostUserID := r.Header.Get(constants.HeaderMattermostUserIDAPI)
mattermostUserID := r.Header.Get(constants.HeaderMattermostUserID)

var project *serializers.ProjectDetails
decoder := json.NewDecoder(r.Body)
@@ -213,7 +214,7 @@ func (p *Plugin) handleUnlinkProject(w http.ResponseWriter, r *http.Request) {

// handleUnlinkProject unlinks a project
func (p *Plugin) handleGetUserAccountDetails(w http.ResponseWriter, r *http.Request) {
mattermostUserID := r.Header.Get(constants.HeaderMattermostUserIDAPI)
mattermostUserID := r.Header.Get(constants.HeaderMattermostUserID)

userDetails, err := p.Store.LoadUser(mattermostUserID)
if err != nil {
@@ -243,7 +244,7 @@ func (p *Plugin) handleGetUserAccountDetails(w http.ResponseWriter, r *http.Requ
}

func (p *Plugin) handleCreateSubscriptions(w http.ResponseWriter, r *http.Request) {
mattermostUserID := r.Header.Get(constants.HeaderMattermostUserIDAPI)
mattermostUserID := r.Header.Get(constants.HeaderMattermostUserID)
body, err := serializers.CreateSubscriptionRequestPayloadFromJSON(r.Body)
if err != nil {
p.API.LogError("Error in decoding the body for creating subscriptions", "Error", err.Error())
@@ -270,28 +271,20 @@ func (p *Plugin) handleCreateSubscriptions(w http.ResponseWriter, r *http.Reques
return
}

// TODO: remove later
teamID := "qteks46as3befxj4ec1mip5ume"
channel, channelErr := p.API.GetChannelByName(teamID, body.ChannelName, false)
if channelErr != nil {
p.handleError(w, r, &serializers.Error{Code: http.StatusBadRequest, Message: channelErr.DetailedError})
return
}

subscriptionList, err := p.Store.GetAllSubscriptions(mattermostUserID)
if err != nil {
p.API.LogError(constants.FetchSubscriptionListError, "Error", err.Error())
p.handleError(w, r, &serializers.Error{Code: http.StatusInternalServerError, Message: err.Error()})
return
}

if _, isSubscriptionPresent := p.IsSubscriptionPresent(subscriptionList, serializers.SubscriptionDetails{OrganizationName: body.Organization, ProjectName: body.Project, ChannelID: channel.Id, EventType: body.EventType}); isSubscriptionPresent {
if _, isSubscriptionPresent := p.IsSubscriptionPresent(subscriptionList, serializers.SubscriptionDetails{OrganizationName: body.Organization, ProjectName: body.Project, ChannelID: body.ChannelID, EventType: body.EventType}); isSubscriptionPresent {
p.API.LogError(constants.SubscriptionAlreadyPresent, "Error")
p.handleError(w, r, &serializers.Error{Code: http.StatusBadRequest, Message: constants.SubscriptionAlreadyPresent})
return
}

subscription, statusCode, err := p.Client.CreateSubscription(body, project, channel.Id, p.GetPluginURL(), mattermostUserID)
subscription, statusCode, err := p.Client.CreateSubscription(body, project, body.ChannelID, p.GetPluginURL(), mattermostUserID)
if err != nil {
p.API.LogError(constants.CreateSubscriptionError, "Error", err.Error())
p.handleError(w, r, &serializers.Error{Code: statusCode, Message: err.Error()})
@@ -304,7 +297,7 @@ func (p *Plugin) handleCreateSubscriptions(w http.ResponseWriter, r *http.Reques
ProjectID: subscription.PublisherInputs.ProjectID,
OrganizationName: body.Organization,
EventType: body.EventType,
ChannelID: channel.Id,
ChannelID: body.ChannelID,
SubscriptionID: subscription.ID,
})

@@ -321,7 +314,7 @@ func (p *Plugin) handleCreateSubscriptions(w http.ResponseWriter, r *http.Reques
}

func (p *Plugin) handleGetSubscriptions(w http.ResponseWriter, r *http.Request) {
mattermostUserID := r.Header.Get(constants.HeaderMattermostUserIDAPI)
mattermostUserID := r.Header.Get(constants.HeaderMattermostUserID)
subscriptionList, err := p.Store.GetAllSubscriptions(mattermostUserID)
if err != nil {
p.API.LogError(constants.FetchSubscriptionListError, "Error", err.Error())
@@ -375,7 +368,7 @@ func (p *Plugin) handleSubscriptionNotifications(w http.ResponseWriter, r *http.
}

func (p *Plugin) handleDeleteSubscriptions(w http.ResponseWriter, r *http.Request) {
mattermostUserID := r.Header.Get(constants.HeaderMattermostUserIDAPI)
mattermostUserID := r.Header.Get(constants.HeaderMattermostUserID)
body, err := serializers.DeleteSubscriptionRequestPayloadFromJSON(r.Body)
if err != nil {
p.API.LogError("Error in decoding the body for deleting subscriptions", "Error", err.Error())
@@ -419,9 +412,49 @@ func (p *Plugin) handleDeleteSubscriptions(w http.ResponseWriter, r *http.Reques
w.WriteHeader(http.StatusNoContent)
}

func (p *Plugin) getUserChannelsForTeam(w http.ResponseWriter, r *http.Request) {
mattermostUserID := r.Header.Get(constants.HeaderMattermostUserID)
pathParams := mux.Vars(r)
teamID := pathParams[constants.PathParamTeamID]
if !model.IsValidId(teamID) {
p.API.LogError("Invalid team id")
http.Error(w, "Invalid team id", http.StatusBadRequest)
return
}

channels, channelErr := p.API.GetChannelsForTeamForUser(teamID, mattermostUserID, false)
if channelErr != nil {
p.API.LogError("Error in getting channels for team and user", "Error", channelErr.Error())
http.Error(w, fmt.Sprintf("Error in getting channels for team and user. Error: %s", channelErr.Error()), channelErr.StatusCode)
return
}

w.Header().Set("Content-Type", "application/json")
if channels == nil {
_, _ = w.Write([]byte("[]"))
return
}

var requiredChannels []*model.Channel
for _, channel := range channels {
if channel.Type == model.CHANNEL_PRIVATE || channel.Type == model.CHANNEL_OPEN {
requiredChannels = append(requiredChannels, channel)
}
}
if requiredChannels == nil {
_, _ = w.Write([]byte("[]"))
return
}

if err := json.NewEncoder(w).Encode(requiredChannels); err != nil {
p.API.LogError("Error while writing response", "Error", err.Error())
w.WriteHeader(http.StatusInternalServerError)
}
}

func (p *Plugin) checkOAuth(handler http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
mattermostUserID := r.Header.Get(constants.HeaderMattermostUserIDAPI)
mattermostUserID := r.Header.Get(constants.HeaderMattermostUserID)
user, err := p.Store.LoadUser(mattermostUserID)
if err != nil || user.AccessToken == "" {
if errors.Is(err, ErrNotFound) || user.AccessToken == "" {
@@ -439,7 +472,7 @@ func (p *Plugin) checkOAuth(handler http.HandlerFunc) http.HandlerFunc {
// handleAuthRequired verifies if the provided request is performed by an authorized source.
func (p *Plugin) handleAuthRequired(handleFunc http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
mattermostUserID := r.Header.Get(constants.HeaderMattermostUserIDAPI)
mattermostUserID := r.Header.Get(constants.HeaderMattermostUserID)
if mattermostUserID == "" {
error := serializers.Error{Code: http.StatusUnauthorized, Message: constants.NotAuthorized}
p.handleError(w, r, &error)
6 changes: 5 additions & 1 deletion server/plugin/command.go
Original file line number Diff line number Diff line change
@@ -26,6 +26,7 @@ var azureDevopsCommandHandler = Handler{
"connect": azureDevopsConnectCommand,
"disconnect": azureDevopsDisconnectCommand,
"link": azureDevopsAccountConnectionCheck,
"subscribe": azureDevopsAccountConnectionCheck,
},
defaultHandler: executeDefault,
}
@@ -42,7 +43,7 @@ func (ch *Handler) Handle(p *Plugin, c *plugin.Context, commandArgs *model.Comma
}

func (p *Plugin) getAutoCompleteData() *model.AutocompleteData {
azureDevops := model.NewAutocompleteData(constants.CommandTriggerName, "[command]", "Available commands: help, connect, disconnect, create, link")
azureDevops := model.NewAutocompleteData(constants.CommandTriggerName, "[command]", "Available commands: help, connect, disconnect, create, link, subscribe")

help := model.NewAutocompleteData("help", "", fmt.Sprintf("Show %s slash command help", constants.CommandTriggerName))
azureDevops.AddCommand(help)
@@ -59,6 +60,9 @@ func (p *Plugin) getAutoCompleteData() *model.AutocompleteData {
link := model.NewAutocompleteData("link", "[link]", "link a project")
azureDevops.AddCommand(link)

subscribe := model.NewAutocompleteData("subscribe", "", "Add a subscription")
azureDevops.AddCommand(subscribe)

return azureDevops
}

8 changes: 4 additions & 4 deletions server/serializers/subscriptions.go
Original file line number Diff line number Diff line change
@@ -43,7 +43,7 @@ type CreateSubscriptionRequestPayload struct {
Organization string `json:"organization"`
Project string `json:"project"`
EventType string `json:"eventType"`
ChannelName string `json:"channelName"`
ChannelID string `json:"channelID"`
}

type CreateSubscriptionBodyPayload struct {
@@ -114,8 +114,8 @@ func (t *CreateSubscriptionRequestPayload) IsSubscriptionRequestPayloadValid() e
if t.EventType == "" {
return errors.New(constants.EventTypeRequired)
}
if t.ChannelName == "" {
return errors.New(constants.ChannelNameRequired)
if t.ChannelID == "" {
return errors.New(constants.ChannelIDRequired)
}
return nil
}
@@ -131,7 +131,7 @@ func (t *DeleteSubscriptionRequestPayload) IsSubscriptionRequestPayloadValid() e
return errors.New(constants.EventTypeRequired)
}
if t.ChannelID == "" {
return errors.New(constants.ChannelNameRequired)
return errors.New(constants.ChannelIDRequired)
}
return nil
}
8 changes: 6 additions & 2 deletions webapp/src/app.tsx
Original file line number Diff line number Diff line change
@@ -3,9 +3,10 @@ import {useDispatch} from 'react-redux';

import usePluginApi from 'hooks/usePluginApi';

import {getGlobalModalState, getLinkModalState} from 'selectors';
import {getGlobalModalState, getLinkModalState, getSubscribeModalState} from 'selectors';

import {toggleShowLinkModal} from 'reducers/linkModal';
import {toggleShowSubscribeModal} from 'reducers/subscribeModal';
import {resetGlobalModalState} from 'reducers/globalModal';

// Global styles
@@ -32,6 +33,9 @@ const App = (): JSX.Element => {
case 'linkProject':
dispatch(toggleShowLinkModal({isVisible: true, commandArgs}));
break;
case 'subscribeProject':
dispatch(toggleShowSubscribeModal({isVisible: true, commandArgs}));
break;
}
} else {
dispatch(resetGlobalModalState());
@@ -40,7 +44,7 @@ const App = (): JSX.Element => {

useEffect(() => {
dispatch(resetGlobalModalState());
}, [getLinkModalState(usePlugin.state).visibility]);
}, [getLinkModalState(usePlugin.state).visibility, getSubscribeModalState(usePlugin.state).visibility]);

return <></>;
};
Loading