Skip to content

Commit

Permalink
[GH-774] Add tooltip to Jira ticket links (#887)
Browse files Browse the repository at this point in the history
* [GH-774] Add Jira Ticket Tolltip

* [GH-774] Add Jira Ticket Tooltip

* [GH-774] Add Jira Ticket Tooltip

* [GH-774] Add Jira Ticket Tolltip

* [GH-774] Draft Update Styling With Fake Data Link PopOver

* tooltip for jira link comment

* tooltip for jira link comment

* fix lint issue

* fix ci issue

* fix ci issue

* fix ci issue

* fix ci issue

* fix ci issue

* fix ci issue

* fix ci issue

* fix ci issue

* fix ci issue

* fix lint isssue

* lint issue fix

* fix lint issue

* fix lint issue

* fix lint issue

* fix lint issue

* fix lint issue

* fix lint issue

* fix lint issue

* fix lint issue

* added some debug statement

* added some debug statement

* fix overlap of state

* fix overlap of state

* fix overlap of state

* fix lint issue

* lint fix

* fixed lint issue

* resolved comments

* resolved comments

* resloved lint

* temp push

* temp push

* fix logic

* solved lint issue

* solved lint issue

* resolved comments

* removed reducer flow to fetch jira ticket details

* removed reducer flow to fetch jira ticket details

* temp push

* added props redux logic to handle state

* fixed lint issue

* [MI-2260] Add tooltip on jira ticket links

* [MI-2260] Self review fixes

* [MI-2260] Review fixes
1. Removed any types whereever possible.
2. Added condition to prevent unnecessary API call.

* [MI-2260] Review fixes

* [MI-2260] Review fixes

* [MI-2260] Review fixes

* [MI-2260] Updated babel config to use optional chaining

* [MI-2260] Review fixes
1. Removed optional chaining
2. Updated className according to BEM

* [MI-2260] Review fixes

* [MI-2385] Fix review fixes for PR GH-774

* [MI-2385] Fix declaration of variables

* [MI-2385] Fix lint error

* [GH-774] Revert package-lock.json file changes.

* [MI-2471] Review fixes on PR #887 by javaguirre

* [MI-2471] Review fix on jira PR #887 by mickmister

* [MI-2471] Review fix on PR #887 by mickmister
1. Wrapped the CSS into a parent ID so that it does not override with any other css

* [MI-2471] Fix lint errors

* [MI-2471] Replaced id with class

* [MI-2471] Review fixes

* [MI-2471] Review fixes

* [MI-2543] Review fixes on Jira PR #887 (Jira ticket link tooltip)

* [MI-2543] Changed reducer name

* [GH-774] Review fixes

* [MI-2612] Review fixes on Jira PR #887

* [MI-2612] Review fix

* Done the review fixes of PR #887 (#37)

* [MI-2700] Review fixes

* [MI-2700]: Done the review fixes of PR #887

* [MI-2700]: Added EOF

* [MI-2700]: Review fixes done
1. Improved code readability

* [MI-2700]: Review fixes done
1. Added support for multiple themes.
2. Improved code quality

* [MI-2700]: Review fixes done
1. Improved code quality

* [MI-2700]: Removed white space from around the image loader

* [MI-2700]: Review fixes done
1. Improved code quality

* [MI-2700]: Review fixes done
1. Improved code readability
2. Improved comments

---------

Co-authored-by: raghavaggarwal2308 <raghav.aggarwal@brightscout.com>

* [MI-2944] Review fixes on Jira PR #887 (Add link tooltip)

* [MI-2944] Fixed few testcases related comments

* [MI-2944] Updated logic to store the actual ticket data in redux

* [MI-2944] Review fixes

* [MI-2988] Review fixes on Jira PR #887(Add link tooltip)
1. Created a separate file to get mock data for tickt details.
2. Separated out the logic to render skeleton loader.

* [MI-2988] Review fix

* [MI-3053] Review fixes on Jira PR #887 (Add link tooltip)
1. Replaced skeleton loader with spinner loader

* [MI-3053] Removed unused css variable

* [MI-3053] Fix spinner opacity and size

* [MI-3064] Review fixes on jira PR #887(Add link tooltip)

* [MI-3077] Review fixes on Jira PR #887 (Add link tooltip)

* [MI-3077] Review fixes

* [MI-3103] Review fixes on Jira PR #887 (Add link tooltip)

---------

Co-authored-by: JuprianoAbelioGinting <juprianoabelioginting@gmail.com>
Co-authored-by: Kitty <xtabs12@gmail.com>
Co-authored-by: sibasankarnayak <sibasankar@demansoltech.com>
Co-authored-by: ayusht2810 <ayush.thakur@brightscout.com>
Co-authored-by: Ayush Thakur <100013900+ayusht2810@users.noreply.github.com>
Co-authored-by: Nityanand Rai <107465508+Nityanand13@users.noreply.github.com>
  • Loading branch information
7 people authored Dec 5, 2023
1 parent 8372a0c commit acd959e
Show file tree
Hide file tree
Showing 18 changed files with 938 additions and 2 deletions.
8 changes: 8 additions & 0 deletions server/constants.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package main

const (
HeaderMattermostUserID = "Mattermost-User-Id"

ParamInstanceID = "instance_id"
ParamIssueKey = "issue_key"
)
3 changes: 3 additions & 0 deletions server/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ const (
routeUserStart = "/user/start"
routeUserConnect = "/user/connect"
routeUserDisconnect = "/user/disconnect"
routeGetIssueByKey = "/get-issue-by-key"
routeSharePublicly = "/share-issue-publicly"
routeOAuth2Complete = "/oauth2/complete.html"
)
Expand Down Expand Up @@ -98,6 +99,7 @@ func (p *Plugin) initializeRouter() {

apiRouter := p.router.PathPrefix(routeAPI).Subrouter()

// Issue APIs
apiRouter.HandleFunc(routeAPIGetAutoCompleteFields, p.checkAuth(p.handleResponse(p.httpGetAutoCompleteFields))).Methods(http.MethodGet)
apiRouter.HandleFunc(routeAPICreateIssue, p.checkAuth(p.handleResponse(p.httpCreateIssue))).Methods(http.MethodPost)
apiRouter.HandleFunc(routeAPIGetCreateIssueMetadata, p.checkAuth(p.handleResponse(p.httpGetCreateIssueMetadataForProjects))).Methods(http.MethodGet)
Expand All @@ -107,6 +109,7 @@ func (p *Plugin) initializeRouter() {
apiRouter.HandleFunc(routeAPIAttachCommentToIssue, p.checkAuth(p.handleResponse(p.httpAttachCommentToIssue))).Methods(http.MethodPost)
apiRouter.HandleFunc(routeIssueTransition, p.handleResponse(p.httpTransitionIssuePostAction)).Methods(http.MethodPost)
apiRouter.HandleFunc(routeSharePublicly, p.handleResponse(p.httpShareIssuePublicly)).Methods(http.MethodPost)
apiRouter.HandleFunc(routeGetIssueByKey, p.handleResponse(p.httpGetIssueByKey)).Methods(http.MethodGet)

// User APIs
apiRouter.HandleFunc(routeAPIUserInfo, p.checkAuth(p.handleResponse(p.httpGetUserInfo))).Methods(http.MethodGet)
Expand Down
38 changes: 38 additions & 0 deletions server/issue.go
Original file line number Diff line number Diff line change
Expand Up @@ -1033,3 +1033,41 @@ func (p *Plugin) getClient(instanceID, mattermostUserID types.ID) (Client, Insta
}
return client, instance, connection, nil
}

func (p *Plugin) httpGetIssueByKey(w http.ResponseWriter, r *http.Request) (int, error) {
if r.Method != http.MethodGet {
return respondErr(w, http.StatusMethodNotAllowed, fmt.Errorf("request: %s is not allowed, must be GET", r.Method))
}

mattermostUserID := r.Header.Get(HeaderMattermostUserID)
if mattermostUserID == "" {
return respondErr(w, http.StatusUnauthorized, errors.New("not authorized"))
}

instanceID := r.FormValue(ParamInstanceID)
issueKey := r.FormValue(ParamIssueKey)
issue, err := p.GetIssueByKey(types.ID(instanceID), types.ID(mattermostUserID), issueKey)
if err != nil {
return respondErr(w, http.StatusInternalServerError, err)
}

return respondJSON(w, issue)
}

func (p *Plugin) GetIssueByKey(instanceID, mattermostUserID types.ID, issueKey string) (*jira.Issue, error) {
client, _, _, err := p.getClient(instanceID, mattermostUserID)
if err != nil {
return nil, err
}

issue, err := client.GetIssue(issueKey, nil)
if err != nil {
switch StatusCode(err) {
case http.StatusNotFound:
return nil, errors.New("we couldn't find the issue key, or you do not have the appropriate permissions to view the issue. Please try again or contact your Jira administrator")
default:
return nil, errors.WithMessage(err, "request to Jira failed")
}
}
return issue, nil
}
2 changes: 2 additions & 0 deletions webapp/src/action_types/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,6 @@ export default {

RECEIVED_CHANNEL_SUBSCRIPTIONS: `${PluginId}_recevied_channel_subscriptions`,
DELETED_CHANNEL_SUBSCRIPTION: `${PluginId}_deleted_channel_subscription`,

RECEIVED_JIRA_TICKET: `${PluginId}_received_jira_ticket`,
};
30 changes: 29 additions & 1 deletion webapp/src/actions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,14 @@ import ActionTypes from 'action_types';
import {doFetch, doFetchWithResponse, buildQueryString} from 'client';
import {getPluginServerRoute, getInstalledInstances, getUserConnectedInstances} from 'selectors';
import {isDesktopApp, isMinimumDesktopAppVersion} from 'utils/user_agent';
import {ChannelSubscription, CreateIssueRequest, SearchIssueParams, InstanceType, ProjectMetadata, APIResponse} from 'types/model';
import {
APIResponse,
ChannelSubscription,
CreateIssueRequest,
InstanceType,
ProjectMetadata,
SearchIssueParams,
} from 'types/model';

export const openConnectModal = () => {
return {
Expand Down Expand Up @@ -509,3 +516,24 @@ export function sendEphemeralPost(message: string, channelId?: string) {
});
};
}

export const fetchIssueByKey = (issueKey: string, instanceID: string) => {
return async (dispatch, getState) => {
const baseUrl = getPluginServerRoute(getState());
let data = null;
const params = `issue_key=${issueKey}&instance_id=${instanceID}`;
try {
data = await doFetch(`${baseUrl}/api/v2/get-issue-by-key?${params}`, {
method: 'get',
});

dispatch({
type: ActionTypes.RECEIVED_JIRA_TICKET,
data,
});
return {data};
} catch (error) {
return {error};
}
};
};
12 changes: 12 additions & 0 deletions webapp/src/components/default_avatar/defaultAvatar.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
.jira-issue-tooltip {
.default-avatar {
background-color: #708090;
border-radius: 50%;
margin-right: 5px;
display: flex;
justify-content: center;
align-items: center;
width: 22px;
height: 22px;
}
}
31 changes: 31 additions & 0 deletions webapp/src/components/default_avatar/default_avatar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import React from 'react';
import './defaultAvatar.scss';

function DefaultAvatar() {
return (
<span className='default-avatar'>
<svg
width='18'
height='18'
viewBox='0 0 18 18'
role='presentation'
>
<g
fill='white'
fillRule='evenodd'
>
<path
d='M3.5 14c0-1.105.902-2 2.009-2h7.982c1.11 0 2.009.894 2.009 2.006v4.44c0 3.405-12 3.405-12 0V14z'
/>
<circle
cx='9'
cy='6'
r='3.5'
/>
</g>
</svg>
</span>
);
}

export default DefaultAvatar;
24 changes: 24 additions & 0 deletions webapp/src/components/jira_ticket_tooltip/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import {connect} from 'react-redux';
import {bindActionCreators, Dispatch} from 'redux';
import {GlobalState} from 'mattermost-redux/types/store';

import {jiraIssueToReducer} from 'utils/jira_issue_metadata';

import {isUserConnected, getStoredLinkTooltipIssue, getUserConnectedInstances, getDefaultUserInstanceID} from 'selectors';
import {fetchIssueByKey} from 'actions';

import TicketPopover from './jira_ticket_tooltip';

const mapStateToProps = (state: GlobalState) => {
return {
connected: isUserConnected(state),
ticketDetails: jiraIssueToReducer(getStoredLinkTooltipIssue(state).ticket),
connectedInstances: getUserConnectedInstances(state),
};
};

const mapDispatchToProps = (dispatch: Dispatch) => bindActionCreators({
fetchIssueByKey,
}, dispatch);

export default connect(mapStateToProps, mapDispatchToProps)(TicketPopover);
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import React from 'react';
import {shallow} from 'enzyme';

import {Instance, InstanceType} from 'types/model';

import TicketPopover, {Props} from './jira_ticket_tooltip';

describe('components/jira_ticket_tooltip', () => {
describe('getIssueKey', () => {
const mockConnectedInstances: Instance[] = [
{
instance_id: 'https://something-1.atlassian.net',
type: InstanceType.CLOUD,
},
{
instance_id: 'https://something-2.atlassian.net',
type: InstanceType.SERVER,
},
];

const mockProps1: Props = {
href: '',
show: false,
connected: false,
connectedInstances: mockConnectedInstances,
fetchIssueByKey: jest.fn(),
};

const mockProps2: Props = {
href: '',
show: false,
connected: false,
connectedInstances: [],
fetchIssueByKey: jest.fn(),
};

test('should return the expected output when URL matches the first regex pattern', () => {
const wrapper = shallow(
<TicketPopover
{...mockProps1}
href='https://something-1.atlassian.net/browse/TICKET-1234'
/>
);
const instance = wrapper.instance() as TicketPopover;
const expectedOutput = {ticketID: 'TICKET-1234', instanceID: 'https://something-1.atlassian.net'};
expect(instance.getIssueKey()).toEqual(expectedOutput);
});

test('should return the expected output when URL matches the second regex pattern', () => {
const wrapper = shallow(
<TicketPopover
{...mockProps1}
href='https://something-2.atlassian.net/jira/issues/?selectedIssue=TICKET-1234'
/>
);
const instance = wrapper.instance() as TicketPopover;
const expectedOutput = {ticketID: 'TICKET-1234', instanceID: 'https://something-2.atlassian.net'};
expect(instance.getIssueKey()).toEqual(expectedOutput);
});

test('should return null when URL does not match any pattern', () => {
const wrapper = shallow(
<TicketPopover
{...mockProps1}
href='https://something-invalid.atlassian.net/not-a-ticket'
/>
);
const instance = wrapper.instance() as TicketPopover;
expect(instance.getIssueKey()).toEqual(null);
});

test('should return null when the URL does not contain the ticket ID', () => {
const wrapper = shallow(
<TicketPopover
{...mockProps1}
href='https://something-2.atlassian.net/jira/issues/?selectedIssue='
/>
);
const instance = wrapper.instance() as TicketPopover;
expect(instance.getIssueKey()).toEqual(null);
});

test('should return null when no instance is connected', () => {
const wrapper = shallow(
<TicketPopover
{...mockProps2}
href='https://something-2.atlassian.net/jira/issues/?selectedIssue='
/>
);
const instance = wrapper.instance() as TicketPopover;
expect(instance.getIssueKey()).toEqual(null);
});
});
});
Loading

0 comments on commit acd959e

Please sign in to comment.