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

[GH-626]: Supported filtering on comment visibility for subscriptions #894

Open
wants to merge 14 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
26 changes: 26 additions & 0 deletions server/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,10 @@ import (
)

const autocompleteSearchRoute = "2/jql/autocompletedata/suggestions"
const commentVisibilityRoute = "2/user"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm thinking we want to use project roles for this

I have a WIP here that implements the data fetching piece, but not the comment filtering piece master...comment-security

err = client.RESTGet("2/project/"+projectKey, nil, &result)
if err != nil {
return http.StatusInternalServerError,
errors.WithMessage(err, "error fetching comment security levels")
}
roles := result.Roles
out := &AutoCompleteResult{}
for role := range roles {
out.Results = append(out.Results, Result{
Value: role,
DisplayName: role,
})
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mickmister We are getting the user groups here. I don't think the project roles API returns that response

const userSearchRoute = "2/user/assignable/search"
const unrecognizedEndpoint = "_unrecognized"
const visibleToAllUsers = "visible-to-all-users"

// Client is the combined interface for all upstream APIs and convenience methods.
type Client interface {
Expand Down Expand Up @@ -66,6 +68,7 @@ type SearchService interface {
SearchUsersAssignableToIssue(issueKey, query string, maxResults int) ([]jira.User, error)
SearchUsersAssignableInProject(projectKey, query string, maxResults int) ([]jira.User, error)
SearchAutoCompleteFields(params map[string]string) (*AutoCompleteResult, error)
SearchCommentVisibilityFields(params map[string]string) (*CommentVisibilityResult, error)
}

// IssueService is the interface for issue-related APIs.
Expand Down Expand Up @@ -254,6 +257,18 @@ type AutoCompleteResult struct {
Results []Result `json:"results"`
}

type JiraUserGroup struct {
Name string `json:"name"`
}

type JiraUserGroupCollection struct {
JiraUserGroups []*JiraUserGroup `json:"items"`
}

type CommentVisibilityResult struct {
Groups *JiraUserGroupCollection `json:"groups"`
}

// SearchAutoCompleteFields searches fieldValue specified in the params and returns autocomplete suggestions
// for that fieldValue
func (client JiraClient) SearchAutoCompleteFields(params map[string]string) (*AutoCompleteResult, error) {
Expand All @@ -266,6 +281,17 @@ func (client JiraClient) SearchAutoCompleteFields(params map[string]string) (*Au
return result, nil
}

// SearchCommentVisibilityFields searches fieldValue specified in the params and returns the comment visibility suggestions
// for that fieldValue
func (client JiraClient) SearchCommentVisibilityFields(params map[string]string) (*CommentVisibilityResult, error) {
result := &CommentVisibilityResult{}
if err := client.RESTGet(commentVisibilityRoute, params, result); err != nil {
return nil, err
}
result.Groups.JiraUserGroups = append(result.Groups.JiraUserGroups, &JiraUserGroup{visibleToAllUsers})
return result, nil
}

// DoTransition executes a transition on an issue.
func (client JiraClient) DoTransition(issueKey, transitionID string) error {
resp, err := client.Jira.Issue.DoTransition(issueKey, transitionID)
Expand Down
2 changes: 2 additions & 0 deletions server/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import (
)

const (
routeAPIGetCommentVisibilityFields = "/get-comment-visibility-fields"
routeAutocomplete = "/autocomplete"
routeAutocompleteConnect = "/connect"
routeAutocompleteUserInstance = "/user-instance"
Expand Down Expand Up @@ -100,6 +101,7 @@ func (p *Plugin) initializeRouter() {
apiRouter := p.router.PathPrefix(routeAPI).Subrouter()

// Issue APIs
apiRouter.HandleFunc(routeAPIGetCommentVisibilityFields, p.checkAuth(p.handleResponse(p.httpGetCommentVisibilityFields))).Methods(http.MethodGet)
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 Down
60 changes: 53 additions & 7 deletions server/issue.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,21 @@ import (
)

const (
labelsField = "labels"
statusField = "status"
reporterField = "reporter"
priorityField = "priority"
descriptionField = "description"
resolutionField = "resolution"
securityLevelField = "security"
labelsField = "labels"
statusField = "status"
reporterField = "reporter"
priorityField = "priority"
descriptionField = "description"
resolutionField = "resolution"
securityLevelField = "security"
headerMattermostUserID = "Mattermost-User-ID"
instanceIDQueryParam = "instance_id"
fieldValueQueryParam = "fieldValue"

QueryParamInstanceID = "instance_id"
QueryParamProjectID = "project_id"

expandValueGroups = "groups"
)

type CreateMetaInfo struct {
Expand Down Expand Up @@ -407,6 +412,47 @@ func (p *Plugin) GetCreateIssueMetadataForProjects(instanceID, mattermostUserID
}, nil
}

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

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

instanceID := r.FormValue(instanceIDQueryParam)
client, _, connection, err := p.getClient(types.ID(instanceID), types.ID(mattermostUserID))
if err != nil {
return http.StatusInternalServerError, err
}

params := map[string]string{
"fieldValue": r.FormValue(fieldValueQueryParam),
"expand": expandValueGroups,
"accountId": connection.AccountID,
}
response, err := client.SearchCommentVisibilityFields(params)
if err != nil {
return http.StatusInternalServerError, err
}
if response == nil {
return http.StatusInternalServerError, errors.New("failed to return the response")
}

jsonResponse, err := json.Marshal(response)
if err != nil {
return http.StatusInternalServerError, errors.WithMessage(err, "failed to marshal the response")
}

w.Header().Set("Content-Type", "application/json")
if _, err = w.Write(jsonResponse); err != nil {
return http.StatusInternalServerError, errors.WithMessage(err, "failed to write the response")
}
return http.StatusOK, nil
}

func (p *Plugin) httpGetSearchIssues(w http.ResponseWriter, r *http.Request) (int, error) {
mattermostUserID := r.Header.Get("Mattermost-User-Id")
instanceID := r.FormValue("instance_id")
Expand Down
9 changes: 9 additions & 0 deletions server/subscribe.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ const (
FilterEmpty = "empty"

MaxSubscriptionNameLength = 100
CommentVisibility = "commentVisibility"
)

type FieldFilter struct {
Expand Down Expand Up @@ -170,6 +171,14 @@ func (p *Plugin) matchesSubsciptionFilters(wh *webhook, filters SubscriptionFilt
}

value := getIssueFieldValue(issue, field.Key)
if field.Key == CommentVisibility {
if wh.Comment.Visibility.Value != "" {
value = value.Add(wh.Comment.Visibility.Value)
} else {
value = value.Add(visibleToAllUsers)
}
}

if !isValidFieldInclusion(field, value, inclusion) {
return false
}
Expand Down
7 changes: 7 additions & 0 deletions webapp/src/actions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,13 @@ export const searchAutoCompleteFields = (params) => {
};
};

export const searchCommentVisibilityFields = (params) => {
return async (dispatch, getState) => {
const url = `${getPluginServerRoute(getState())}/api/v2/get-comment-visibility-fields`;
return doFetchWithResponse(`${url}${buildQueryString(params)}`);
};
};

export const searchUsers = (params) => {
return async (dispatch, getState) => {
const url = getPluginServerRoute(getState()) + '/api/v2/get-search-users';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.

import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';

import {searchCommentVisibilityFields} from 'actions';

import JiraCommentVisibilitySelector from './jira_commentvisibility_selector';

const mapDispatchToProps = (dispatch) => bindActionCreators({searchCommentVisibilityFields}, dispatch);

export default connect(null, mapDispatchToProps)(JiraCommentVisibilitySelector);
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.

import React from 'react';

import {ReactSelectOption} from 'types/model';

import BackendSelector, {Props as BackendSelectorProps} from '../backend_selector';

const stripHTML = (text: string) => {
if (!text) {
return text;
}

const doc = new DOMParser().parseFromString(text, 'text/html');
return doc.body.textContent || '';
};

type Props = BackendSelectorProps & {
searchCommentVisibilityFields: (params: {fieldValue: string}) => (
Promise<{data: {groups: {items: {name: string}[]}}; error?: Error}>
);
fieldName: string;
};

const JiraCommentVisibilitySelector = (props: Props) => {
const {value, isMulti, instanceID, searchCommentVisibilityFields} = props;

const commentVisibilityFields = async (inputValue: string): Promise<ReactSelectOption[]> => {
const params = {
fieldValue: inputValue,
instance_id: instanceID,
};
return searchCommentVisibilityFields(params).then(({data}) => {
return data.groups.items.map((suggestion) => ({
value: suggestion.name,
label: stripHTML(suggestion.name),
}));
});
};

const fetchInitialSelectedValues = async (): Promise<ReactSelectOption[]> => (value?.length ? commentVisibilityFields('') : []);

return (
<BackendSelector
{...props}
fetchInitialSelectedValues={fetchInitialSelectedValues}
search={commentVisibilityFields}
/>
);
};

export default JiraCommentVisibilitySelector;
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ exports[`components/ChannelSubscriptionFilter should match snapshot 1`] = `
"label": "Affects versions",
"value": "versions",
},
Object {
"label": "Comment Visibility",
"value": "commentVisibility",
},
Object {
"label": "Epic Link",
"value": "customfield_10014",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,20 @@ exports[`components/ChannelSubscriptionFilters should match snapshot 1`] = `
}
fields={
Array [
Object {
"issueTypes": Array [
Object {
"id": "10001",
"name": "Bug",
},
],
"key": "commentVisibility",
"name": "Comment Visibility",
"schema": Object {
"type": "commentVisibility",
},
"values": Array [],
},
Object {
"issueTypes": Array [
Object {
Expand Down Expand Up @@ -209,6 +223,20 @@ exports[`components/ChannelSubscriptionFilters should match snapshot 1`] = `
}
fields={
Array [
Object {
"issueTypes": Array [
Object {
"id": "10001",
"name": "Bug",
},
],
"key": "commentVisibility",
"name": "Comment Visibility",
"schema": Object {
"type": "commentVisibility",
},
"values": Array [],
},
Object {
"issueTypes": Array [
Object {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -520,6 +520,36 @@ exports[`components/EditChannelSubscription should match snapshot after fetching
},
],
},
Object {
"issueTypes": Array [
Object {
"id": "10002",
"name": "Task",
},
Object {
"id": "10003",
"name": "Sub-task",
},
Object {
"id": "10001",
"name": "Story",
},
Object {
"id": "10004",
"name": "Bug",
},
Object {
"id": "10000",
"name": "Epic",
},
],
"key": "commentVisibility",
"name": "Comment Visibility",
"schema": Object {
"type": "commentVisibility",
},
"values": Array [],
},
Object {
"issueTypes": Array [
Object {
Expand Down
Loading
Loading