Skip to content

Commit

Permalink
[MI-2009] API to get list of subscriptions (#35)
Browse files Browse the repository at this point in the history
* [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 <[email protected]>
  • Loading branch information
ayusht2810 and avas27JTG authored Sep 6, 2022
1 parent 03423a7 commit bffcb47
Show file tree
Hide file tree
Showing 8 changed files with 102 additions and 128 deletions.
4 changes: 3 additions & 1 deletion server/constants/messages.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
172 changes: 90 additions & 82 deletions server/plugin/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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 {
Expand All @@ -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())
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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
}
}

Expand Down
1 change: 0 additions & 1 deletion server/plugin/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
3 changes: 1 addition & 2 deletions webapp/src/components/emptyState/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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) => (
<div className={`no-data d-flex ${wrapperExtraClass}`}>
<div className='d-flex flex-column align-items-center'>
<div className='no-data__icon d-flex justify-content-center align-items-center'>
Expand Down Expand Up @@ -110,5 +110,4 @@ const EmptyState = ({ title, subTitle, buttonText, buttonAction, icon = 'folder'
</div>
);


export default EmptyState;
31 changes: 0 additions & 31 deletions webapp/src/containers/Rhs/projectList/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -113,37 +113,6 @@ const ProjectList = () => {
wrapperExtraClass='margin-top-80'
/>)
}
{
getApiState(plugin_constants.pluginApiServiceConfigs.getAllLinkedProjectsList.apiServiceName).isSuccess && (
data?.length > 0 ?
<>
{
data.map((item) => (
<ProjectCard
onProjectTitleClick={handleProjectTitleClick}
projectDetails={item}
key={item.projectID}
handleUnlinkProject={handleUnlinkProject}
/>
),
)
}
<div className='rhs-project-list-wrapper'>
<button
onClick={handleOpenLinkProjectModal}
className='plugin-btn no-data__btn btn btn-primary project-list-btn'
>
{'Link new project'}
</button>
</div>
</> :
<EmptyState
title='No Project Linked'
subTitle={{text: 'Link a project by clicking the button below'}}
buttonText='Link new project'
buttonAction={handleOpenLinkProjectModal}
/>)
}
</>
);
};
Expand Down
6 changes: 3 additions & 3 deletions webapp/src/containers/SubscribeModal/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ const SubscribeModal = () => {
const {
formFields,
errorState,
onChangeOfFormField,
onChangeFormField,
setSpecificFieldValue,
resetFormFields,
isErrorInFormValidation,
Expand Down Expand Up @@ -141,7 +141,7 @@ const SubscribeModal = () => {

// Pre-select the dropdown value in case of single option
useEffect(() => {
const autoSelectedValues: Pick<Record<FormFields, string>, 'organization' | 'project' | 'channelID'> = {
const autoSelectedValues: Pick<Record<FormFieldNames, string>, 'organization' | 'project' | 'channelID'> = {
organization: '',
project: '',
channelID: '',
Expand Down Expand Up @@ -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}
/>
Expand Down
6 changes: 3 additions & 3 deletions webapp/src/containers/TaskModal/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ const TaskModal = () => {
const {
formFields,
errorState,
onChangeOfFormField,
onChangeFormField,
setSpecificFieldValue,
resetFormFields,
isErrorInFormValidation,
Expand Down Expand Up @@ -119,7 +119,7 @@ const TaskModal = () => {

// Pre-select the dropdown value in case of single option
useEffect(() => {
const autoSelectedValues: Pick<Record<FormFields, string>, 'organization' | 'project'> = {
const autoSelectedValues: Pick<Record<FormFieldNames, string>, 'organization' | 'project'> = {
organization: '',
project: '',
};
Expand Down Expand Up @@ -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}
/>
Expand Down
7 changes: 2 additions & 5 deletions webapp/src/hooks/useForm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,11 +108,8 @@ function useForm(initialFormFields: Record<FormFieldNames, ModalFormFieldConfig>
};

// Set value for a specific form field
const setSpecificFieldValue = (fieldName: FormFieldNames, value: string) => {
setFormFields({
...formFields,
[fieldName]: value,
});
const setSpecificFieldValue = (modifiedFormFields: Partial<Record<FormFieldNames, string>>) => {
setFormFields(modifiedFormFields);
};

return {formFields, errorState, setSpecificFieldValue, onChangeFormField, isErrorInFormValidation, resetFormFields};
Expand Down

0 comments on commit bffcb47

Please sign in to comment.