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

Add "reporter" and "assignee" fields in subscription filters #890

Closed
wants to merge 12 commits into from
Closed
13 changes: 7 additions & 6 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,38 +10,39 @@ linters-settings:
simplify: true
goimports:
local-prefixes: github.com/mattermost/mattermost-plugin-jira
golint:
min-confidence: 0
govet:
check-shadowing: true
enable-all: true
disable:
- fieldalignment
misspell:
locale: US
revive:
rules:
- name: error-strings
disabled: true

linters:
disable-all: true
enable:
- bodyclose
- deadcode
- errcheck
- goconst
- gocritic
- gofmt
- goimports
- golint
- gosec
- gosimple
- govet
- ineffassign
- misspell
- nakedret
- revive
- staticcheck
- structcheck
- stylecheck
- typecheck
- unconvert
- unused
- varcheck
- whitespace

issues:
Expand Down
10 changes: 3 additions & 7 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,13 @@ require (
github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/gorilla/mux v1.8.0
github.com/jarcoal/httpmock v1.0.8
github.com/mattermost/mattermost-plugin-api v0.0.26-0.20220223141232-cb8b1984774a
github.com/mattermost/mattermost-plugin-api v0.0.27
github.com/mattermost/mattermost-plugin-autolink v1.2.2-0.20210709183311-c8fa30db649f
github.com/mattermost/mattermost-server/v6 v6.3.0
github.com/mattermost/mattermost-server/v6 v6.5.2
github.com/mholt/archiver/v3 v3.5.1
github.com/pkg/errors v0.9.1
github.com/rbriski/atlassian-jwt v0.0.0-20180307182949-7bb4ae273058
github.com/stretchr/testify v1.7.0
golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8
golang.org/x/text v0.3.7
)

// Until github.com/mattermost/mattermost-server/v6 v6.5.0 is releated,
// this replacement is needed to also import github.com/mattermost/mattermost-plugin-api,
// which uses a different server version.
replace github.com/mattermost/mattermost-server/v6 v6.3.0 => github.com/mattermost/mattermost-server/v6 v6.0.0-20220210052000-0d67995eb491
143 changes: 137 additions & 6 deletions go.sum

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
"description": "Atlassian Jira plugin for Mattermost.",
"homepage_url": "https://github.com/mattermost/mattermost-plugin-jira",
"support_url": "https://github.com/mattermost/mattermost-plugin-jira/issues",
"release_notes_url": "https://github.com/mattermost/mattermost-plugin-jira/releases/tag/v3.2.0",
"release_notes_url": "https://github.com/mattermost/mattermost-plugin-jira/releases/tag/v3.2.1",
"icon_path": "assets/icon.svg",
"version": "3.2.0",
"version": "3.2.1",
Copy link
Collaborator

Choose a reason for hiding this comment

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

Unrelated change?

Copy link
Collaborator

Choose a reason for hiding this comment

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

This is not resolved

"min_server_version": "6.5.0",
"server": {
"executables": {
Expand Down
3 changes: 1 addition & 2 deletions server/instance_cloud.go
Original file line number Diff line number Diff line change
Expand Up @@ -260,8 +260,7 @@ func (ci *cloudInstance) parseHTTPRequestJWT(r *http.Request) (*jwt.Token, strin

token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, errors.New(
fmt.Sprintf("unsupported signing method: %v", token.Header["alg"]))
return nil, errors.Errorf("unsupported signing method: %v", token.Header["alg"])
}
// HMAC secret is a []byte
return []byte(ci.AtlassianSecurityContext.SharedSecret), nil
Expand Down
9 changes: 7 additions & 2 deletions server/issue.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ const (
priorityField = "priority"
descriptionField = "description"
resolutionField = "resolution"
assigneeField = "assignee"
)

func makePost(userID, channelID, message string) *model.Post {
Expand Down Expand Up @@ -783,6 +784,10 @@ func getIssueFieldValue(issue *jira.Issue, key string) StringSet {
return NewStringSet(issue.Fields.Labels...)
case priorityField:
return NewStringSet(issue.Fields.Priority.ID)
case reporterField:
return NewStringSet(issue.Fields.Reporter.AccountID)
case assigneeField:
return NewStringSet(issue.Fields.Assignee.AccountID)
case "fixversions":
result := NewStringSet()
for _, v := range issue.Fields.FixVersions {
Expand Down Expand Up @@ -847,12 +852,12 @@ func (p *Plugin) UnassignIssue(instance Instance, mattermostUserID types.ID, iss
// check for valid issue key
_, err = client.GetIssue(issueKey, nil)
if err != nil {
return "", errors.Errorf("We couldn't find the issue key `%s`. Please confirm the issue key and try again.", issueKey)
return "", errors.Errorf("We couldn't find the issue key `%s`. Please confirm the issue key and try again.", issueKey)
}

if err := client.UpdateAssignee(issueKey, &jira.User{}); err != nil {
if StatusCode(err) == http.StatusForbidden {
return "", errors.New("you do not have the appropriate permissions to perform this action. Please contact your Jira administrator")
return "", errors.New("You do not have the appropriate permissions to perform this action. Please contact your Jira administrator.")
}
return "", err
}
Expand Down
4 changes: 2 additions & 2 deletions server/jira_test_util_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ func getJiraTestData(filename string) ([]byte, error) {

func withExistingChannelSubscriptions(subscriptions []ChannelSubscription) *Subscriptions {
ret := NewSubscriptions()
for i, sub := range subscriptions {
sub.InstanceID = testInstance1.GetID()
for i := range subscriptions {
subscriptions[i].InstanceID = testInstance1.GetID()
ret.Channel.add(&subscriptions[i])
}
return ret
Expand Down
26 changes: 13 additions & 13 deletions server/kv.go
Original file line number Diff line number Diff line change
Expand Up @@ -519,7 +519,7 @@ func (store *store) LoadInstanceFullKey(fullkey string) (Instance, error) {
return &si, nil
}

return nil, errors.New(fmt.Sprintf("Jira instance %s has unsupported type: %s", fullkey, si.Type))
return nil, errors.Errorf("Jira instance %s has unsupported type: %s", fullkey, si.Type)
}

func (store *store) StoreInstance(instance Instance) error {
Expand Down Expand Up @@ -568,13 +568,13 @@ func UpdateInstances(store InstanceStore, updatef func(instances *Instances) err

// MigrateV2Instances migrates instance record(s) from the V2 data model.
//
// - v2keyKnownJiraInstances ("known_jira_instances") was stored as a
// map[string]string (InstanceID->Type), needs to be stored as Instances.
// https://github.com/mattermost/mattermost-plugin-jira/blob/885efe8eb70c92bcea64d1ced6e67710eda77b6e/server/kv.go#L375
// - v2keyCurrentJIRAInstance ("current_jira_instance") stored an Instance; will
// be used to set the default instance.
// - The instances themselves should be forward-compatible, including
// CurrentInstance.
// - v2keyKnownJiraInstances ("known_jira_instances") was stored as a
// map[string]string (InstanceID->Type), needs to be stored as Instances.
// https://github.com/mattermost/mattermost-plugin-jira/blob/885efe8eb70c92bcea64d1ced6e67710eda77b6e/server/kv.go#L375
// - v2keyCurrentJIRAInstance ("current_jira_instance") stored an Instance; will
// be used to set the default instance.
// - The instances themselves should be forward-compatible, including
// CurrentInstance.
func MigrateV2Instances(p *Plugin) (*Instances, error) {
// Check if V3 instances exist and return them if found
instances, err := p.instanceStore.LoadInstances()
Expand Down Expand Up @@ -646,7 +646,7 @@ func MigrateV2Instances(p *Plugin) (*Instances, error) {
return instances, nil
}

// MigrateV3ToV2 performs necessary migrations when reverting from V3 to V2
// MigrateV3ToV2 performs necessary migrations when reverting from V3 to V2
func MigrateV3ToV2(p *Plugin) string {
// migrate V3 instances to v2
v2Instances, msg := MigrateV3InstancesToV2(p)
Expand Down Expand Up @@ -675,10 +675,10 @@ func MigrateV3ToV2(p *Plugin) string {

// MigrateV3InstancesToV2 migrates instance record(s) from the V3 data model.
//
// - v3 instances need to be stored as v2keyKnownJiraInstances
// (known_jira_instances) map[string]string (InstanceID->Type),
// - v2keyCurrentJIRAInstance ("current_jira_instance") stored an Instance; will
// be used to set the default instance.
// - v3 instances need to be stored as v2keyKnownJiraInstances
// (known_jira_instances) map[string]string (InstanceID->Type),
// - v2keyCurrentJIRAInstance ("current_jira_instance") stored an Instance; will
// be used to set the default instance.
func MigrateV3InstancesToV2(p *Plugin) (JiraV2Instances, string) {
v3instances, err := p.instanceStore.LoadInstances()
if err != nil {
Expand Down
2 changes: 1 addition & 1 deletion server/manifest.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@ var manifest = struct {
Version string
}{
ID: "jira",
Version: "3.2.0",
Version: "3.2.1",
}
4 changes: 2 additions & 2 deletions server/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@ func (p *Plugin) OnDeactivate() error {
if p.telemetryClient != nil {
err := p.telemetryClient.Close()
if err != nil {
return errors.Wrap(err, "OnDeactivate: Failed to close telemetryClient.")
return errors.Wrap(err, "OnDeactivate: Failed to close telemetryClient")
}
}
return nil
Expand Down Expand Up @@ -427,7 +427,7 @@ func (p *Plugin) errorf(f string, args ...interface{}) {
func (p *Plugin) CheckSiteURL() error {
ustr := p.GetSiteURL()
if ustr == "" {
return errors.Errorf("Mattermost SITEURL must not be empty.")
return errors.New("Mattermost SITEURL must not be empty.")
}
u, err := url.Parse(ustr)
if err != nil {
Expand Down
5 changes: 3 additions & 2 deletions server/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,12 +130,13 @@ func (p *Plugin) httpUserDisconnect(w http.ResponseWriter, r *http.Request) (int
_, err = p.DisconnectUser(disconnectPayload.InstanceID, types.ID(mattermostUserID))
if errors.Cause(err) == kvstore.ErrNotFound {
return respondErr(w, http.StatusNotFound,
errors.Errorf("Could not complete the **disconnection** request. You do not currently have a Jira account at %q linked to your Mattermost account.",
errors.Errorf(
"could not complete the **disconnection** request. You do not currently have a Jira account at %q linked to your Mattermost account",
disconnectPayload.InstanceID))
}
if err != nil {
return respondErr(w, http.StatusNotFound,
errors.Errorf("Could not complete the **disconnection** request. Error: %v", err))
errors.Errorf("could not complete the **disconnection** request. Error: %v", err))
}

_, err = w.Write([]byte(`{"success": true}`))
Expand Down
5 changes: 4 additions & 1 deletion server/webhook_parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import (
"time"

"github.com/pkg/errors"
"golang.org/x/text/cases"
"golang.org/x/text/language"

"github.com/mattermost/mattermost-server/v6/model"
)
Expand Down Expand Up @@ -439,7 +441,8 @@ func mergeWebhookEvents(events []*webhook) Webhook {
if event.fieldInfo.name == descriptionField || strings.HasPrefix(event.fieldInfo.from, strike) {
strike = ""
}
msg := "**" + strings.Title(event.fieldInfo.name) + ":** " + strike +
// Use the english language for now. Using the server's local might be better.
msg := "**" + cases.Title(language.English, cases.NoLower).String(event.fieldInfo.name) + ":** " + strike +
event.fieldInfo.from + strike + " " + event.fieldInfo.to
merged.fields = append(merged.fields, &model.SlackAttachmentField{
Value: msg,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {Theme} from 'mattermost-redux/types/preferences';
import ReactSelectSetting from 'components/react_select_setting';
import JiraEpicSelector from 'components/data_selectors/jira_epic_selector';

import {isEpicLinkField, isMultiSelectField, isLabelField} from 'utils/jira_issue_metadata';
import {isEpicLinkField, isMultiSelectField, isLabelField, isUserField} from 'utils/jira_issue_metadata';
import {FilterField, FilterValue, ReactSelectOption, IssueMetadata, IssueType, FilterFieldInclusion} from 'types/model';
import ConfirmModal from 'components/confirm_modal';
import JiraAutoCompleteSelector from 'components/data_selectors/jira_autocomplete_selector';
Expand Down Expand Up @@ -262,6 +262,15 @@ export default class ChannelSubscriptionFilter extends React.PureComponent<Props
onChange={this.handleEpicLinkChange}
/>
);
} else if (isUserField(field)) {
valueSelector = (
<JiraAutoCompleteSelector
{...selectProps}
fieldName={field.name}
value={value.values}
onChange={this.handleEpicLinkChange}
/>
);
} else {
valueSelector = (
<ReactSelectSetting
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import JiraInstanceAndProjectSelector from 'components/jira_instance_and_project
const allowedFields: string[] = [
JiraFieldTypeEnums.PROJECT,
JiraFieldTypeEnums.ISSUE_TYPE,
JiraFieldTypeEnums.REPORTER,
JiraFieldTypeEnums.PRIORITY,
JiraFieldTypeEnums.DESCRIPTION,
JiraFieldTypeEnums.SUMMARY,
Expand Down
2 changes: 1 addition & 1 deletion webapp/src/manifest.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// This file is automatically generated. Do not modify it manually.

export const id = 'jira';
export const version = '3.2.0';
export const version = '3.2.1';
2 changes: 2 additions & 0 deletions webapp/src/types/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ export type ProjectMetadata = {
export enum JiraFieldTypeEnums {
PROJECT = 'project',
ISSUE_TYPE = 'issuetype',

REPORTER = 'reporter',
PRIORITY = 'priority',
DESCRIPTION = 'description',
SUMMARY = 'summary',
Expand Down
20 changes: 18 additions & 2 deletions webapp/src/utils/jira_issue_metadata.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ function isValidFieldForFilter(field: JiraField): boolean {
}

return allowedTypes.includes(type) || (custom && acceptedCustomTypesForFilters.includes(custom)) ||
Copy link
Contributor

Choose a reason for hiding this comment

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

I find this part a bit difficult to follow and understand, but this would be a future refactor.

Copy link
Contributor

Choose a reason for hiding this comment

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

also allowedTypes is a bit confusing, because we have here other types (option, user) but those aren't in the allowedTypes array, but they're called types too 🤷 .

But as I said, this is for the future.

Copy link
Contributor

@mickmister mickmister Dec 1, 2022

Choose a reason for hiding this comment

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

@javaguirre Yeah I too think option and user should be added to allowedTypes. I don't see any logic that would break if we did that

type === 'option' || // single select
type === 'option' || type === 'user' ||
(type === 'array' && allowedArrayTypes.includes(items));
}

Expand All @@ -199,6 +199,18 @@ export function getCustomFieldFiltersForProjects(metadata: IssueMetadata | null,
} as FilterField;
});

const userFields = fields.filter((field) => field.schema.type === 'user' && !field.allowedValues) as (StringArrayField & FieldWithInfo)[];
const populatedUserFields = userFields.map((field) => {
return {
key: field.key,
name: field.name,
schema: field.schema,
issueTypes: field.validIssueTypes,
} as FilterField;
});

const userResult = populatedFields.concat(populatedUserFields);

const stringArrayFields = fields.filter((field) => field.schema.type === 'array' && field.schema.items === 'string' && !field.allowedValues) as (StringArrayField & FieldWithInfo)[];
const userDefinedFields = stringArrayFields.map((field) => {
return {
Expand All @@ -210,7 +222,7 @@ export function getCustomFieldFiltersForProjects(metadata: IssueMetadata | null,
} as FilterField;
});

const result = populatedFields.concat(userDefinedFields);
const result = userResult.concat(userDefinedFields);
const epicLinkField = fields.find(isEpicLinkField);
if (epicLinkField) {
result.unshift({
Expand Down Expand Up @@ -264,6 +276,10 @@ export function isLabelField(field: JiraField | FilterField): boolean {
return field.schema.system === 'labels' || field.schema.custom === 'com.atlassian.jira.plugin.system.customfieldtypes:labels';
}

export function isUserField(field: JiraField | FilterField): boolean {
return field.schema.type === 'user' || field.schema.custom === 'com.atlassian.jira.plugin.system.customfieldtypes:userpicker';
Copy link
Contributor

Choose a reason for hiding this comment

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

Could the string 'com.atlassian.....' be a constant more to the top of the file? It seems some kind of "magic string".

}

export function isEpicIssueType(issueType: IssueType): boolean {
return issueType.name === 'Epic';
}
Expand Down
1 change: 1 addition & 0 deletions webapp/webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ const config = {
},
externals: {
react: 'React',
'react-dom': 'ReactDOM',
redux: 'Redux',
'react-redux': 'ReactRedux',
'prop-types': 'PropTypes',
Expand Down