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-2085]: No projects linked handling on modals and refactor create-… #54

Merged
merged 39 commits into from
Sep 5, 2022
Merged
Changes from 1 commit
Commits
Show all changes
39 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
9c341a4
[MI-1939]: Review fixes
avas27JTG Aug 17, 2022
78d8173
[MI-1939]: Added check for user's oAuth
avas27JTG Aug 17, 2022
111fe53
Release-2
ayusht2810 Aug 17, 2022
4414572
[MI-1939]: Added buffer
avas27JTG 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
cb4c65a
[MI-1939]: Used epoch time
avas27JTG Aug 18, 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
09c25ba
[MI-2071]: Auto select drop down values on clicking subscription button
avas27JTG Aug 23, 2022
0ac9c07
[MI-2085]: No projects linked handling on modals and refactor create-…
avas27JTG Aug 24, 2022
456779f
Merge branch 'MI-1939_1' of github.com:Brightscout/mattermost-plugin-…
avas27JTG Aug 24, 2022
ae2191f
[MI-2090]: Updated UI from Figma
avas27JTG Aug 24, 2022
ec4cf6a
Merge branch 'master' of github.com:Brightscout/mattermost-plugin-azu…
avas27JTG Sep 5, 2022
6759ea0
[MI-2085]: Review 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-2001]: [MI-2001]: Create plugin API to unlink project and integra…
…te the UI
avas27JTG committed Aug 12, 2022
commit 8dc6f80bb6b2f503994a2415927de212a514ca13
3 changes: 3 additions & 0 deletions server/constants/messages.go
Original file line number Diff line number Diff line change
@@ -25,4 +25,7 @@ const (
AlreadyLinkedProject = "This project is already linked."
GetProjectListError = "Error getting Project List"
ErrorFetchProjectList = "Error in fetching project list"
ErrorDecodingBody = "Error in decoding body"
ProjectNotFound = "Requested project does not exists"
ErrorUnlinkProject = "Error in unlinking the project"
)
1 change: 1 addition & 0 deletions server/constants/routes.go
Original file line number Diff line number Diff line change
@@ -7,4 +7,5 @@ const (
PathOAuthConnect = "/oauth/connect"
PathOAuthCallback = "/oauth/complete"
PathGetAllLinkedProjects = "/link/project"
PathUnlinkProject = "/unlink/project"
)
63 changes: 58 additions & 5 deletions server/plugin/api.go
Original file line number Diff line number Diff line change
@@ -37,6 +37,7 @@ func (p *Plugin) InitRoutes() {
s.HandleFunc("/tasks", p.handleAuthRequired(p.handleCreateTask)).Methods(http.MethodPost)
s.HandleFunc("/link", p.handleAuthRequired(p.handleLink)).Methods(http.MethodPost)
s.HandleFunc(constants.PathGetAllLinkedProjects, p.handleAuthRequired(p.handleGetAllLinkedProjects)).Methods(http.MethodGet)
s.HandleFunc(constants.PathUnlinkProject, p.handleAuthRequired(p.handleUnlinkProject)).Methods(http.MethodPost)
}

// API to create task of a project in an organization.
@@ -83,7 +84,7 @@ func (p *Plugin) handleLink(w http.ResponseWriter, r *http.Request) {
var body *serializers.LinkRequestPayload
decoder := json.NewDecoder(r.Body)
if err := decoder.Decode(&body); err != nil {
p.API.LogError("Error in decoding body", "Error", err.Error())
p.API.LogError(constants.ErrorDecodingBody, "Error", err.Error())
p.handleError(w, r, &serializers.Error{Code: http.StatusInternalServerError, Message: err.Error()})
return
}
@@ -136,16 +137,21 @@ func (p *Plugin) handleGetAllLinkedProjects(w http.ResponseWriter, r *http.Reque
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())
error := serializers.Error{Code: http.StatusInternalServerError, Message: err.Error()}
p.handleError(w, r, &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 {
p.handleError(w, r, &serializers.Error{Code: http.StatusInternalServerError, Message: err.Error()})
}
@@ -178,6 +184,53 @@ func (p *Plugin) handleError(w http.ResponseWriter, r *http.Request, error *seri
}
}

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

var project *serializers.ProjectDetails
decoder := json.NewDecoder(r.Body)
if err := decoder.Decode(&project); err != nil {
p.API.LogError(constants.ErrorDecodingBody, "Error", err.Error())
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
}

if !p.IsProjectLinked(projectList, *project) {
p.API.LogError(constants.ProjectNotFound, "Error")
p.handleError(w, r, &serializers.Error{Code: http.StatusNotFound, Message: constants.ProjectNotFound})
return
}

if err := p.Store.DeleteProject(project); err != nil {
p.API.LogError(constants.ErrorUnlinkProject, "Error", err.Error())
p.handleError(w, r, &serializers.Error{Code: http.StatusInternalServerError, Message: err.Error()})
}

successResponse := &serializers.SuccessResponse{
Message: "success",
}
response, err := json.Marshal(&successResponse)
if err != nil {
p.API.LogError("Error marhsalling 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)
}
}

func (p *Plugin) WithRecovery(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
4 changes: 4 additions & 0 deletions server/serializers/error.go → server/serializers/common.go
Original file line number Diff line number Diff line change
@@ -5,3 +5,7 @@ type Error struct {
Code int
Message string
}

type SuccessResponse struct {
Message string `json:"message"`
}
4 changes: 2 additions & 2 deletions webapp/src/components/card/project/index.tsx
Original file line number Diff line number Diff line change
@@ -7,7 +7,7 @@ import {onPressingEnterKey} from 'utils';

type ProjectCardProps = {
onProjectTitleClick: (projectDetails: ProjectDetails) => void
handleUnlinkProject: () => void
handleUnlinkProject: (projectDetails: ProjectDetails) => void
projectDetails: ProjectDetails
}

@@ -35,7 +35,7 @@ const ProjectCard = ({onProjectTitleClick, projectDetails: {organizationName, pr
tooltipText='Unlink project'
iconClassName='fa fa-chain-broken'
extraClass='unlink-button'
onClick={handleUnlinkProject}
onClick={() => handleUnlinkProject(projectDetails)}
/>
</div>
</div>
4 changes: 3 additions & 1 deletion webapp/src/components/modal/confirmationModal/index.tsx
Original file line number Diff line number Diff line change
@@ -9,9 +9,10 @@ type ConfirmationModalProps = {
confirmBtnText: string
onHide?: () => void
onConfirm?: () => void
isLoading?: boolean
}

const ConfirmationModal = ({isOpen, title, confirmBtnText, description, onHide, onConfirm}: ConfirmationModalProps) => {
const ConfirmationModal = ({isOpen, title, confirmBtnText, description, onHide, onConfirm, isLoading}: ConfirmationModalProps) => {
return (
<Modal
show={isOpen}
@@ -20,6 +21,7 @@ const ConfirmationModal = ({isOpen, title, confirmBtnText, description, onHide,
onConfirm={onConfirm}
confirmAction={true}
confirmBtnText={confirmBtnText}
loading={isLoading}
>
<p>{description}</p>
</Modal>
200 changes: 94 additions & 106 deletions webapp/src/containers/LinkModal/index.tsx
Original file line number Diff line number Diff line change
@@ -1,138 +1,126 @@
import React, {useCallback, useEffect, useState} from 'react';
import React, {useEffect, useState} from 'react';
import {useDispatch} from 'react-redux';

import Input from 'components/inputField';
import Modal from 'components/modal';

import Constants from 'plugin_constants';
import usePluginApi from 'hooks/usePluginApi';
import {hideLinkModal} from 'reducers/linkModal';
import {hideLinkModal, toggleIsLinked} from 'reducers/linkModal';
import {getLinkModalState} from 'selectors';
import plugin_constants from 'plugin_constants';

const LinkModal = () => {
const [state, setState] = useState({
linkOrganization: '',
linkProject: '',
// State variables
const [projectDetails, setProjectDetails] = useState<LinkPayload>({
organization: '',
project: '',
});
const [error, setError] = useState({
linkOrganizationError: '',
linkProjectError: '',

const [errorState, setErrorState] = useState<LinkPayload>({
organization: '',
project: '',
});
const [linkPayload, setLinkPayload] = useState<LinkPayload | null>();

// Hooks
const usePlugin = usePluginApi();
const {visibility, organization, project} = usePlugin.state['plugins-mattermost-plugin-azure-devops'].openLinkModalReducer;
const [loading, setLoading] = useState(false);
const [APIError, setAPIError] = useState('');
const dispatch = useDispatch();

useEffect(() => {
if (organization && project) {
setState({
linkOrganization: organization,
linkProject: project,
});
}
}, [visibility]);

// Function to hide the modal and reset all the states.
const onHide = useCallback(() => {
setState({
linkOrganization: '',
linkProject: '',
const resetModalState = () => {
setProjectDetails({
organization: '',
project: '',
});
setError({
linkOrganizationError: '',
linkProjectError: '',
setErrorState({
organization: '',
project: '',
});
setLinkPayload(null);
setLoading(false);
setAPIError('');
dispatch(hideLinkModal());
}, []);
};

// Set organization name
const onOrganizationChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setErrorState({...errorState, organization: ''});
setProjectDetails({...projectDetails, organization: (e.target as HTMLInputElement).value});
};

const onOrganizationChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
setError({...error, linkOrganizationError: ''});
setState({...state, linkOrganization: (e.target as HTMLInputElement).value});
}, [state, error]);
// Set project name
const onProjectChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setErrorState({...errorState, project: ''});
setProjectDetails({...projectDetails, project: (e.target as HTMLInputElement).value});
};

const onProjectChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
setError({...error, linkProjectError: ''});
setState({...state, linkProject: (e.target as HTMLInputElement).value});
}, [state, error]);
// Handles on confirming link project
const onConfirm = () => {
const errorStateChanges: LinkPayload = {
organization: '',
project: '',
};

const onConfirm = useCallback(() => {
if (state.linkOrganization === '') {
setError((value) => ({...value, linkOrganizationError: 'Organization is required'}));
if (projectDetails.organization === '') {
errorStateChanges.organization = 'Organization is required';
}
if (state.linkProject === '') {
setError((value) => ({...value, linkProjectError: 'Project is required'}));

if (projectDetails.project === '') {
errorStateChanges.project = 'Project is required';
}

if (!state.linkOrganization || !state.linkProject) {
if (errorStateChanges.organization || errorStateChanges.project) {
setErrorState(errorStateChanges);
return;
}

// Create payload to send in the POST request.
const payload = {
organization: state.linkOrganization,
project: state.linkProject,
};

// TODO: save the payload in a state variable to use it while reading the state
// we can see later if there exists a better way for this
setLinkPayload(payload);

// Make POST api request
usePlugin.makeApiRequest(Constants.pluginApiServiceConfigs.createLink.apiServiceName, payload);
}, [state]);
linkTask(projectDetails);
};

useEffect(() => {
if (linkPayload) {
const {isLoading, isSuccess, isError} = usePlugin.getApiState(Constants.pluginApiServiceConfigs.createLink.apiServiceName, linkPayload);
setLoading(isLoading);
if (isSuccess) {
onHide();
}
if (isError) {
setAPIError('Organization or project name is wrong');
}
// Make POST api request to link a project
const linkTask = async (payload: LinkPayload) => {
const createTaskRequest = await usePlugin.makeApiRequest(plugin_constants.pluginApiServiceConfigs.createLink.apiServiceName, payload);
if (createTaskRequest) {
dispatch(toggleIsLinked(true));
resetModalState();
}
}, [usePlugin.state]);

if (visibility) {
return (
<Modal
show={visibility}
title='Link new project'
onHide={onHide}
onConfirm={onConfirm}
confirmBtnText='Link new project'
cancelDisabled={loading}
confirmDisabled={loading}
loading={loading}
error={APIError}
>
<>
<Input
type='text'
placeholder='Organization name'
value={state.linkOrganization}
onChange={onOrganizationChange}
error={error.linkOrganizationError}
required={true}
/>
<Input
type='text'
placeholder='Project name'
value={state.linkProject}
onChange={onProjectChange}
required={true}
error={error.linkProjectError}
/>
</>
</Modal>
);
}
return null;
};

useEffect(() => {
setProjectDetails({
organization: getLinkModalState(usePlugin.state).organization,
project: getLinkModalState(usePlugin.state).project,
});
}, [getLinkModalState(usePlugin.state)]);

return (
<Modal
show={getLinkModalState(usePlugin.state).visibility}
title='Link new project'
onHide={resetModalState}
onConfirm={onConfirm}
confirmBtnText='Link new project'
cancelDisabled={usePlugin.getApiState(plugin_constants.pluginApiServiceConfigs.createLink.apiServiceName, projectDetails).isLoading}
confirmDisabled={usePlugin.getApiState(plugin_constants.pluginApiServiceConfigs.createLink.apiServiceName, projectDetails).isLoading}
loading={usePlugin.getApiState(plugin_constants.pluginApiServiceConfigs.createLink.apiServiceName, projectDetails).isLoading}
>
<>
<Input
type='text'
placeholder='Organization name'
value={projectDetails.project}
onChange={onOrganizationChange}
error={errorState.organization}
required={true}
/>
<Input
type='text'
placeholder='Project name'
value={projectDetails.project}
onChange={onProjectChange}
required={true}
error={errorState.project}
/>
</>
</Modal>
);
};

export default LinkModal;
Loading