Skip to content

Commit

Permalink
[MM-17061] Support multiple projects for a channel subscription (#232)
Browse files Browse the repository at this point in the history
* support multiple projects

* Fix test

* Rename variable
  • Loading branch information
mickmister committed Jul 20, 2019
1 parent 2360424 commit d864933
Show file tree
Hide file tree
Showing 9 changed files with 98 additions and 41 deletions.
2 changes: 1 addition & 1 deletion server/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ func handleHTTPRequest(p *Plugin, w http.ResponseWriter, r *http.Request) (int,
case routeAPICreateIssue:
return withInstance(p.currentInstanceStore, w, r, httpAPICreateIssue)
case routeAPIGetCreateIssueMetadata:
return withInstance(p.currentInstanceStore, w, r, httpAPIGetCreateIssueMetadataForProject)
return withInstance(p.currentInstanceStore, w, r, httpAPIGetCreateIssueMetadataForProjects)
case routeAPIGetJiraProjectMetadata:
return withInstance(p.currentInstanceStore, w, r, httpAPIGetJiraProjectMetadata)
case routeAPIGetSearchIssues:
Expand Down
10 changes: 5 additions & 5 deletions server/issue.go
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@ func httpAPICreateIssue(ji Instance, w http.ResponseWriter, r *http.Request) (in
return http.StatusOK, nil
}

func httpAPIGetCreateIssueMetadataForProject(ji Instance, w http.ResponseWriter, r *http.Request) (int, error) {
func httpAPIGetCreateIssueMetadataForProjects(ji Instance, w http.ResponseWriter, r *http.Request) (int, error) {
if r.Method != http.MethodGet {
return http.StatusMethodNotAllowed,
errors.New("Request: " + r.Method + " is not allowed, must be GET")
Expand All @@ -234,9 +234,9 @@ func httpAPIGetCreateIssueMetadataForProject(ji Instance, w http.ResponseWriter,
return http.StatusUnauthorized, errors.New("not authorized")
}

projectKey := r.FormValue("project-key")
if projectKey == "" {
return http.StatusBadRequest, errors.New("project-key query param is required")
projectKeys := r.FormValue("project-keys")
if projectKeys == "" {
return http.StatusBadRequest, errors.New("project-keys query param is required")
}

jiraUser, err := ji.GetPlugin().userStore.LoadJIRAUser(ji, mattermostUserId)
Expand All @@ -251,7 +251,7 @@ func httpAPIGetCreateIssueMetadataForProject(ji Instance, w http.ResponseWriter,

cimd, resp, err := jiraClient.Issue.GetCreateMetaWithOptions(&jira.GetQueryOptions{
Expand: "projects.issuetypes.fields",
ProjectKeys: projectKey,
ProjectKeys: projectKeys,
})
if err != nil {
err = userFriendlyJiraError(resp, err)
Expand Down
15 changes: 15 additions & 0 deletions server/subscribe_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,21 @@ func TestGetChannelsSubscribed(t *testing.T) {
}),
ChannelIds: []string{},
},
"multiple projects selected": {
WebhookTestData: "webhook-issue-created.json",
Subs: withExistingChannelSubscriptions([]ChannelSubscription{
ChannelSubscription{
Id: model.NewId(),
ChannelId: "sampleChannelId",
Filters: SubscriptionFilters{
Events: NewStringSet("event_created"),
Projects: NewStringSet("TES", "OTHER"),
IssueTypes: NewStringSet("10001"),
},
},
}),
ChannelIds: []string{"sampleChannelId"},
},
"issue type does not match": {
WebhookTestData: "webhook-issue-created.json",
Subs: withExistingChannelSubscriptions([]ChannelSubscription{
Expand Down
5 changes: 3 additions & 2 deletions webapp/src/actions/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,13 @@ export const closeAttachCommentToIssueModal = () => {
};
};

export const fetchJiraIssueMetadataForProject = (projectKey) => {
export const fetchJiraIssueMetadataForProjects = (projectKeys) => {
return async (dispatch, getState) => {
const baseUrl = getPluginServerRoute(getState());
const projectKeysParam = projectKeys.join(',');
let data = null;
try {
data = await doFetch(`${baseUrl}/api/v2/get-create-issue-metadata-for-project?project-key=${projectKey}`, {
data = await doFetch(`${baseUrl}/api/v2/get-create-issue-metadata-for-project?project-keys=${projectKeysParam}`, {
method: 'get',
});
} catch (error) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {Modal} from 'react-bootstrap';
import ReactSelectSetting from 'components/react_select_setting';
import FormButton from 'components/form_button';
import Loading from 'components/loading';
import {getProjectValues, getIssueValuesForMultipleProjects, getCustomFieldValuesForProject} from 'utils/jira_issue_metadata';
import {getProjectValues, getIssueValuesForMultipleProjects, getCustomFieldValuesForProjects} from 'utils/jira_issue_metadata';

const JiraEventOptions = [
{value: 'event_created', label: 'Issue Created'},
Expand Down Expand Up @@ -45,7 +45,7 @@ export default class ChannelSettingsModalInner extends PureComponent {
deleteChannelSubscription: PropTypes.func.isRequired,
editChannelSubscription: PropTypes.func.isRequired,
fetchChannelSubscriptions: PropTypes.func.isRequired,
fetchJiraIssueMetadataForProject: PropTypes.func.isRequired,
fetchJiraIssueMetadataForProjects: PropTypes.func.isRequired,
clearIssueMetadata: PropTypes.func.isRequired,
};

Expand All @@ -62,18 +62,18 @@ export default class ChannelSettingsModalInner extends PureComponent {
filters = Object.assign({}, filters, props.channelSubscriptions[0].filters);
}

let fetchingProject = false;
let fetchingProjects = false;
if (filters.projects.length) {
fetchingProject = true;
this.fetchProject(filters.projects[0]);
fetchingProjects = true;
this.fetchProjects(filters.projects);
}

this.state = {
error: null,
getMetaDataErr: null,
submitting: false,
filters,
fetchingProject,
fetchingProjects,
};
}

Expand All @@ -94,17 +94,19 @@ export default class ChannelSettingsModalInner extends PureComponent {

handleSettingChange = (id, value) => {
let finalValue = value;
if (!Array.isArray(finalValue)) {
if (!finalValue) {
finalValue = [];
} else if (!Array.isArray(finalValue)) {
finalValue = [finalValue];
}
const filters = {...this.state.filters};
filters[id] = finalValue;
this.setState({filters});
};

fetchProject = (projectKey) => {
this.props.fetchJiraIssueMetadataForProject(projectKey).then((fetched) => {
const state = {fetchingProject: false};
fetchProjects = (projectKeys) => {
this.props.fetchJiraIssueMetadataForProjects(projectKeys).then((fetched) => {
const state = {fetchingProjects: false};
if (fetched.error) {
state.getMetaDataErr = fetched.error.message;
}
Expand All @@ -113,21 +115,54 @@ export default class ChannelSettingsModalInner extends PureComponent {
};

handleProjectChange = (id, value) => {
const projectKey = value;
let projects = value;
if (!projects) {
projects = [];
} else if (!Array.isArray(projects)) {
projects = [projects];
}

const filters = {
projects: [value],
events: [],
issue_types: [],
let filters = {
...this.state.filters,
projects,
};

// User has removed a project from selection. Remove any irrelevant selected choices from the events and issue types.
if (projects.length < this.state.filters.projects.length) {
const issueOptions = getIssueValuesForMultipleProjects(this.props.jiraProjectMetadata, projects);
const customFields = getCustomFieldValuesForProjects(this.props.jiraIssueMetadata, projects);

const selectedIssueTypes = this.state.filters.issue_types.filter((issueType) => {
return Boolean(issueOptions.find((it) => it.value === issueType));
});

const selectedEventTypes = this.state.filters.events.filter((eventType) => {
if (eventType.includes('customfield')) {
return Boolean(customFields.find((et) => et.value === eventType));
}
return true;
});

filters = {
...filters,
issue_types: selectedIssueTypes,
events: selectedEventTypes,
};
}

let fetchingProjects = false;

this.props.clearIssueMetadata();
if (projects && projects.length) {
fetchingProjects = true;
this.fetchProjects(projects);
}

this.setState({
fetchingProject: true,
fetchingProjects,
getMetaDataErr: null,
filters,
});

this.props.clearIssueMetadata();
this.fetchProject(projectKey);
};

handleCreate = (e) => {
Expand Down Expand Up @@ -187,14 +222,14 @@ export default class ChannelSettingsModalInner extends PureComponent {

const projectOptions = getProjectValues(this.props.jiraProjectMetadata);
const issueOptions = getIssueValuesForMultipleProjects(this.props.jiraProjectMetadata, this.state.filters.projects);
const customFields = getCustomFieldValuesForProject(this.props.jiraIssueMetadata, this.state.filters.projects[0]);
const customFields = getCustomFieldValuesForProjects(this.props.jiraIssueMetadata, this.state.filters.projects);

const eventOptions = JiraEventOptions.concat(customFields);

let component = null;
if (this.props.channel && this.props.channelSubscriptions) {
let innerComponent = null;
if (this.state.fetchingProject) {
if (this.state.fetchingProjects) {
innerComponent = <Loading/>;
} else if (this.state.filters.projects[0] && !this.state.getMetaDataErr) {
innerComponent = (
Expand Down Expand Up @@ -231,7 +266,7 @@ export default class ChannelSettingsModalInner extends PureComponent {
required={true}
onChange={this.handleProjectChange}
options={projectOptions}
isMulti={false}
isMulti={true}
theme={this.props.theme}
value={projectOptions.filter((option) => this.state.filters.projects.includes(option.value))}
/>
Expand Down
4 changes: 2 additions & 2 deletions webapp/src/components/modals/channel_settings/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
editChannelSubscription,
closeChannelSettings,
fetchJiraProjectMetadata,
fetchJiraIssueMetadataForProject,
fetchJiraIssueMetadataForProjects,
clearIssueMetadata,
} from 'actions';

Expand Down Expand Up @@ -45,7 +45,7 @@ const mapStateToProps = (state) => {
const mapDispatchToProps = (dispatch) => bindActionCreators({
close: closeChannelSettings,
fetchJiraProjectMetadata,
fetchJiraIssueMetadataForProject,
fetchJiraIssueMetadataForProjects,
clearIssueMetadata,
createChannelSubscription,
fetchChannelSubscriptions,
Expand Down
4 changes: 2 additions & 2 deletions webapp/src/components/modals/create_issue/create_issue.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export default class CreateIssueModal extends PureComponent {
jiraIssueMetadata: PropTypes.object,
clearIssueMetadata: PropTypes.func.isRequired,
jiraProjectMetadata: PropTypes.object,
fetchJiraIssueMetadataForProject: PropTypes.func.isRequired,
fetchJiraIssueMetadataForProjects: PropTypes.func.isRequired,
fetchJiraProjectMetadata: PropTypes.func.isRequired,
};

Expand Down Expand Up @@ -188,7 +188,7 @@ export default class CreateIssueModal extends PureComponent {

// Clear the current metadata so that we display a loading indicator while we fetch the new metadata
this.props.clearIssueMetadata();
this.props.fetchJiraIssueMetadataForProject(projectKey).then((fetched) => {
this.props.fetchJiraIssueMetadataForProjects([projectKey]).then((fetched) => {
if (fetched.error) {
this.setState({getMetaDataError: fetched.error.message, submitting: false});
}
Expand Down
4 changes: 2 additions & 2 deletions webapp/src/components/modals/create_issue/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {bindActionCreators} from 'redux';
import {getPost} from 'mattermost-redux/selectors/entities/posts';
import {getCurrentTeam} from 'mattermost-redux/selectors/entities/teams';

import {closeCreateModal, createIssue, fetchJiraIssueMetadataForProject, fetchJiraProjectMetadata, clearIssueMetadata} from 'actions';
import {closeCreateModal, createIssue, fetchJiraIssueMetadataForProjects, fetchJiraProjectMetadata, clearIssueMetadata} from 'actions';
import {isCreateModalVisible, getCreateModal, getJiraIssueMetadata, getJiraProjectMetadata} from 'selectors';

import CreateIssue from './create_issue';
Expand All @@ -34,7 +34,7 @@ const mapStateToProps = (state) => {
const mapDispatchToProps = (dispatch) => bindActionCreators({
close: closeCreateModal,
create: createIssue,
fetchJiraIssueMetadataForProject,
fetchJiraIssueMetadataForProjects,
fetchJiraProjectMetadata,
clearIssueMetadata,
}, dispatch);
Expand Down
16 changes: 11 additions & 5 deletions webapp/src/utils/jira_issue_metadata.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,14 @@ export function getIssueValues(metadata, projectKey) {
}

export function getIssueValuesForMultipleProjects(metadata, projectKeys) {
return projectKeys.map((project) =>
getIssueValues(metadata, project)).flat().filter(Boolean).sort((a, b) => a.value - b.value).filter((ele, i, me) => i === 0 || ele.value !== me[i - 1].value);
const issueValues = projectKeys.map((project) => getIssueValues(metadata, project)).flat().filter(Boolean);

const issueTypeHash = {};
issueValues.forEach((issueType) => {
issueTypeHash[issueType.value] = issueType;
});

return Object.values(issueTypeHash);
}

export function getFields(metadata, projectKey, issueTypeId) {
Expand All @@ -38,12 +44,12 @@ export function getFields(metadata, projectKey, issueTypeId) {
return getIssueTypes(metadata, projectKey).find((issueType) => issueType.id === issueTypeId).fields;
}

export function getCustomFieldValuesForProject(metadata, projectKey) {
if (!metadata || !projectKey) {
export function getCustomFieldValuesForProjects(metadata, projectKeys) {
if (!metadata || !projectKeys || !projectKeys.length) {
return [];
}

const issueTypes = getIssueTypes(metadata, projectKey);
const issueTypes = projectKeys.map((key) => getIssueTypes(metadata, key)).flat();

const customFieldHash = {};
const fields = issueTypes.map((it) => Object.values(it.fields)).flat();
Expand Down

0 comments on commit d864933

Please sign in to comment.