From bffcb47e96e466c9a126d44fea909eb892cb0fe9 Mon Sep 17 00:00:00 2001 From: Ayush Thakur <100013900+ayusht2810@users.noreply.github.com> Date: Tue, 6 Sep 2022 12:37:43 +0530 Subject: [PATCH] [MI-2009] API to get list of subscriptions (#35) * [MI-1986]: Create plugin API to fetch linked projects list * [MI-1987]: Integrated project list UI * [MI-1987]: Review fixes * [MI-2001]: [MI-2001]: Create plugin API to unlink project and integrate the UI * [MI-2001]: Review fixes * [MI-2002]: Created plugin API to fetch user details and UI integration with other changes * [MI-2002]: Review fixes * [MI-2002]: Updated API paths * [MI-2049]: Added websocket support to detect user connection details and a centralised check for root modals * [MI-2010]: API to create subscriptions * [MI-2010] Fix lint errors * [MI-2009] API to get list of subscriptions * [MI-1987]: Review fix * [MI-1987]: Review fixes * [MI-2001]: Review fixes * [MI-2001]: Review fixes * [MI-2002]: Review fixes * [MI-2049]: Review fixes * [MI-2049]: Fixed merge change * [MI-2049]: Refactored code * [MI-2010] Review fixes * [MI-2010] Correct messages Co-authored-by: Abhishek Verma --- server/constants/messages.go | 4 +- server/plugin/api.go | 172 +++++++++--------- server/plugin/client.go | 1 - webapp/src/components/emptyState/index.tsx | 3 +- .../src/containers/Rhs/projectList/index.tsx | 31 ---- .../src/containers/SubscribeModal/index.tsx | 6 +- webapp/src/containers/TaskModal/index.tsx | 6 +- webapp/src/hooks/useForm.ts | 7 +- 8 files changed, 102 insertions(+), 128 deletions(-) diff --git a/server/constants/messages.go b/server/constants/messages.go index 205d8b6e..8b695a11 100644 --- a/server/constants/messages.go +++ b/server/constants/messages.go @@ -34,9 +34,11 @@ const ( UnableToStoreOauthState = "Unable to store oAuth state for the userID %s" AuthAttemptExpired = "Authentication attempt expired, please try again" InvalidAuthState = "Invalid oauth state, please try again" - GetProjectListError = "Error getting Project List" + GetProjectListError = "Error in getting project list" ErrorFetchProjectList = "Error in fetching project list" ErrorDecodingBody = "Error in decoding body" + ErrorCreateTask = "Error in creating task" + ErrorLinkProject = "Error in linking the project" FetchSubscriptionListError = "Error in fetching subscription list" CreateSubscriptionError = "Error in creating subscription" ProjectNotLinked = "Requested project is not linked" diff --git a/server/plugin/api.go b/server/plugin/api.go index 6d67de9e..69506c87 100644 --- a/server/plugin/api.go +++ b/server/plugin/api.go @@ -66,20 +66,12 @@ func (p *Plugin) handleCreateTask(w http.ResponseWriter, r *http.Request) { task, statusCode, err := p.Client.CreateTask(body, mattermostUserID) if err != nil { + p.API.LogError(constants.ErrorCreateTask) p.handleError(w, r, &serializers.Error{Code: statusCode, Message: err.Error()}) return } - response, err := json.Marshal(task) - if err != nil { - p.handleError(w, r, &serializers.Error{Code: http.StatusInternalServerError, Message: err.Error()}) - return - } - - w.Header().Add("Content-Type", "application/json") - if _, err := w.Write(response); err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - } + p.writeJSON(w, task) message := fmt.Sprintf(constants.CreatedTask, task.Link.HTML.Href) // Send message to DM. @@ -152,7 +144,6 @@ func (p *Plugin) handleGetAllLinkedProjects(w http.ResponseWriter, r *http.Reque } w.Header().Add("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) if projectList == nil { if _, err = w.Write([]byte("[]")); err != nil { @@ -162,53 +153,6 @@ func (p *Plugin) handleGetAllLinkedProjects(w http.ResponseWriter, r *http.Reque return } - w.Header().Add("Content-Type", "application/json") - - if projectList == nil { - _, _ = w.Write([]byte("[]")) - return - } - - w.Header().Add("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - - if projectList == nil { - _, _ = w.Write([]byte("[]")) - return - } - - w.Header().Add("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - - if projectList == nil { - _, _ = w.Write([]byte("[]")) - return - } - - w.Header().Add("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - - if projectList == nil { - _, _ = w.Write([]byte("[]")) - return - } - - w.Header().Add("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - - if projectList == nil { - _, _ = w.Write([]byte("[]")) - return - } - - w.Header().Add("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - - if projectList == nil { - _, _ = w.Write([]byte("[]")) - return - } - response, err := json.Marshal(projectList) if err != nil { p.API.LogError(constants.ErrorFetchProjectList, "Error", err.Error()) @@ -260,18 +204,7 @@ func (p *Plugin) handleUnlinkProject(w http.ResponseWriter, r *http.Request) { Message: "success", } - response, err := json.Marshal(&successResponse) - if err != nil { - p.API.LogError("Error marshaling the response", "Error", err.Error()) - p.handleError(w, r, &serializers.Error{Code: http.StatusInternalServerError, Message: err.Error()}) - return - } - - w.Header().Add("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - if _, err := w.Write(response); err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - } + p.writeJSON(w, &successResponse) } func (p *Plugin) handleCreateSubscriptions(w http.ResponseWriter, r *http.Request) { @@ -561,28 +494,103 @@ func (p *Plugin) handleGetUserAccountDetails(w http.ResponseWriter, r *http.Requ } if userDetails.MattermostUserID == "" { - p.API.LogError(constants.ConnectAccountFirst, "Error") + p.API.LogError(constants.ConnectAccountFirst) p.handleError(w, r, &serializers.Error{Code: http.StatusUnauthorized, Message: constants.ConnectAccountFirst}) return } - response, err := json.Marshal(&userDetails) - if err != nil { - p.API.LogError("Error marshaling the response", "Error", err.Error()) - p.handleError(w, r, &serializers.Error{Code: http.StatusInternalServerError, Message: err.Error()}) - return - } - p.API.PublishWebSocketEvent( constants.WSEventConnect, nil, &model.WebsocketBroadcast{UserId: mattermostUserID}, ) - w.Header().Add("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - if _, err := w.Write(response); err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) + p.writeJSON(w, &userDetails) +} + +func (p *Plugin) handleCreateSubscription(w http.ResponseWriter, r *http.Request) { + 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()) + p.handleError(w, r, &serializers.Error{Code: http.StatusBadRequest, Message: err.Error()}) + return + } + + if err := body.IsSubscriptionRequestPayloadValid(); err != nil { + p.handleError(w, r, &serializers.Error{Code: http.StatusBadRequest, Message: err.Error()}) + return + } + + projectList, err := p.Store.GetAllProjects(mattermostUserID) + if err != nil { + p.API.LogError(constants.ErrorFetchProjectList, "Error", err.Error()) + p.handleError(w, r, &serializers.Error{Code: http.StatusInternalServerError, Message: err.Error()}) + return + } + + project, isProjectLinked := p.IsProjectLinked(projectList, serializers.ProjectDetails{OrganizationName: body.Organization, ProjectName: body.Project}) + if !isProjectLinked { + p.API.LogError(constants.ProjectNotFound) + p.handleError(w, r, &serializers.Error{Code: http.StatusNotFound, Message: constants.ProjectNotLinked}) + return + } + + // TODO: remove later + teamID := "qteks46as3befxj4ec1mip5ume" + channel, channelErr := p.API.GetChannelByName(teamID, body.ChannelID, false) + if channelErr != nil { + p.API.LogError("Error in getting channel name", "Error", channelErr.DetailedError) + 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 { + p.API.LogError(constants.SubscriptionAlreadyPresent) + 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) + if err != nil { + p.API.LogError(constants.CreateSubscriptionError, "Error", err.Error()) + p.handleError(w, r, &serializers.Error{Code: statusCode, Message: err.Error()}) + return + } + + p.Store.StoreSubscription(&serializers.SubscriptionDetails{ + MattermostUserID: mattermostUserID, + ProjectName: body.Project, + ProjectID: subscription.PublisherInputs.ProjectID, + OrganizationName: body.Organization, + EventType: body.EventType, + ChannelID: channel.Id, + SubscriptionID: subscription.ID, + }) + + p.writeJSON(w, subscription) +} + +func (p *Plugin) writeJSON(w http.ResponseWriter, v interface{}) { + w.Header().Set("Content-Type", "application/json") + b, err := json.Marshal(v) + if err != nil { + p.API.LogError("Failed to marshal JSON response", "error", err.Error()) + w.WriteHeader(http.StatusInternalServerError) + return + } + + if _, err = w.Write(b); err != nil { + p.API.LogError("Failed to write JSON response", "error", err.Error()) + w.WriteHeader(http.StatusInternalServerError) + return } } diff --git a/server/plugin/client.go b/server/plugin/client.go index 4244c050..e154635f 100644 --- a/server/plugin/client.go +++ b/server/plugin/client.go @@ -94,7 +94,6 @@ func (c *client) GetTask(organization, taskID, mattermostUserID string) (*serial func (c *client) Link(body *serializers.LinkRequestPayload, mattermostUserID string) (*serializers.Project, int, error) { projectURL := fmt.Sprintf(constants.GetProject, body.Organization, body.Project) var project *serializers.Project - _, statusCode, err := c.callJSON(c.plugin.getConfiguration().AzureDevopsAPIBaseURL, projectURL, http.MethodGet, mattermostUserID, nil, &project, nil) if err != nil { return nil, statusCode, errors.Wrap(err, "failed to link Project") diff --git a/webapp/src/components/emptyState/index.tsx b/webapp/src/components/emptyState/index.tsx index bc13807f..bd335b79 100644 --- a/webapp/src/components/emptyState/index.tsx +++ b/webapp/src/components/emptyState/index.tsx @@ -17,7 +17,7 @@ type EmptyStatePropTypes = { } // TODO: UI to be changed -const EmptyState = ({ title, subTitle, buttonText, buttonAction, icon = 'folder', wrapperExtraClass }: EmptyStatePropTypes) => ( +const EmptyState = ({title, subTitle, buttonText, buttonAction, icon = 'folder', wrapperExtraClass}: EmptyStatePropTypes) => (
@@ -110,5 +110,4 @@ const EmptyState = ({ title, subTitle, buttonText, buttonAction, icon = 'folder'
); - export default EmptyState; diff --git a/webapp/src/containers/Rhs/projectList/index.tsx b/webapp/src/containers/Rhs/projectList/index.tsx index 8883690b..4d6ac864 100644 --- a/webapp/src/containers/Rhs/projectList/index.tsx +++ b/webapp/src/containers/Rhs/projectList/index.tsx @@ -113,37 +113,6 @@ const ProjectList = () => { wrapperExtraClass='margin-top-80' />) } - { - getApiState(plugin_constants.pluginApiServiceConfigs.getAllLinkedProjectsList.apiServiceName).isSuccess && ( - data?.length > 0 ? - <> - { - data.map((item) => ( - - ), - ) - } -
- -
- : - ) - } ); }; diff --git a/webapp/src/containers/SubscribeModal/index.tsx b/webapp/src/containers/SubscribeModal/index.tsx index 29fb5346..1ede5e5a 100644 --- a/webapp/src/containers/SubscribeModal/index.tsx +++ b/webapp/src/containers/SubscribeModal/index.tsx @@ -27,7 +27,7 @@ const SubscribeModal = () => { const { formFields, errorState, - onChangeOfFormField, + onChangeFormField, setSpecificFieldValue, resetFormFields, isErrorInFormValidation, @@ -141,7 +141,7 @@ const SubscribeModal = () => { // Pre-select the dropdown value in case of single option useEffect(() => { - const autoSelectedValues: Pick, 'organization' | 'project' | 'channelID'> = { + const autoSelectedValues: Pick, 'organization' | 'project' | 'channelID'> = { organization: '', project: '', channelID: '', @@ -220,7 +220,7 @@ const SubscribeModal = () => { fieldConfig={plugin_constants.form.subscriptionModal[field as SubscriptionModalFields]} value={formFields[field as SubscriptionModalFields]} optionsList={getDropDownOptions(field as SubscriptionModalFields)} - onChange={(newValue) => onChangeOfFormField(field as SubscriptionModalFields, newValue)} + onChange={(newValue) => onChangeFormField(field as SubscriptionModalFields, newValue)} error={errorState[field as SubscriptionModalFields]} isDisabled={isLoading} /> diff --git a/webapp/src/containers/TaskModal/index.tsx b/webapp/src/containers/TaskModal/index.tsx index 48a3f3ba..89a7479a 100644 --- a/webapp/src/containers/TaskModal/index.tsx +++ b/webapp/src/containers/TaskModal/index.tsx @@ -23,7 +23,7 @@ const TaskModal = () => { const { formFields, errorState, - onChangeOfFormField, + onChangeFormField, setSpecificFieldValue, resetFormFields, isErrorInFormValidation, @@ -119,7 +119,7 @@ const TaskModal = () => { // Pre-select the dropdown value in case of single option useEffect(() => { - const autoSelectedValues: Pick, 'organization' | 'project'> = { + const autoSelectedValues: Pick, 'organization' | 'project'> = { organization: '', project: '', }; @@ -186,7 +186,7 @@ const TaskModal = () => { fieldConfig={plugin_constants.form.createTaskModal[field as CreateTaskModalFields]} value={formFields[field as CreateTaskModalFields]} optionsList={getDropDownOptions(field as CreateTaskModalFields)} - onChange={(newValue) => onChangeOfFormField(field as CreateTaskModalFields, newValue)} + onChange={(newValue) => onChangeFormField(field as CreateTaskModalFields, newValue)} error={errorState[field as CreateTaskModalFields]} isDisabled={isLoading} /> diff --git a/webapp/src/hooks/useForm.ts b/webapp/src/hooks/useForm.ts index cf9078a4..4a0f6514 100644 --- a/webapp/src/hooks/useForm.ts +++ b/webapp/src/hooks/useForm.ts @@ -108,11 +108,8 @@ function useForm(initialFormFields: Record }; // Set value for a specific form field - const setSpecificFieldValue = (fieldName: FormFieldNames, value: string) => { - setFormFields({ - ...formFields, - [fieldName]: value, - }); + const setSpecificFieldValue = (modifiedFormFields: Partial>) => { + setFormFields(modifiedFormFields); }; return {formFields, errorState, setSpecificFieldValue, onChangeFormField, isErrorInFormValidation, resetFormFields};