Commit 9e85eeac authored by Paul Slaughter's avatar Paul Slaughter

Add actions and handling for approval input module

**Note:**
- Instead of making fetches to the server, this module mutates
data on the client.
- There is a `rules_hidden_inputs` component which maps the
current state into hidden form inputs.
parent a8eb4326
Pipeline #43455923 failed with stages
in 88 minutes and 13 seconds
......@@ -52,6 +52,7 @@ export default {
</div>
</div>
</template>
<slot name="footer"></slot>
<modal-rule-create :modal-id="$options.CREATE_MODAL_ID" />
<modal-rule-remove :modal-id="$options.REMOVE_MODAL_ID" />
</div>
......
<script>
import App from './app_base.vue';
import RulesInput from './rules_input.vue';
import RulesHiddenInputs from './rules_hidden_inputs.vue';
export default {
components: {
App,
RulesInput,
RulesHiddenInputs,
},
};
</script>
<template>
<app> <rules-input slot="rules" /> </app>
<app>
<rules-input slot="rules" />
<rules-hidden-inputs slot="footer" />
</app>
</template>
......@@ -35,11 +35,17 @@ export default {
approversByType() {
return _.groupBy(this.approvers, x => x.type);
},
users() {
return this.approversByType[TYPE_USER] || [];
},
groups() {
return this.approversByType[TYPE_GROUP] || [];
},
userIds() {
return (this.approversByType[TYPE_USER] || []).map(x => x.id);
return this.users.map(x => x.id);
},
groupIds() {
return (this.approversByType[TYPE_GROUP] || []).map(x => x.id);
return this.groups.map(x => x.id);
},
validation() {
if (!this.showValidation) {
......@@ -80,6 +86,8 @@ export default {
approvalsRequired: this.approvalsRequired,
users: this.userIds,
groups: this.groupIds,
userRecords: this.users,
groupRecords: this.groups,
};
this.showValidation = true;
......
<script>
import { mapState } from 'vuex';
export default {
computed: {
...mapState(['settings']),
...mapState({
rules: state => state.rules.rules,
}),
},
};
</script>
<template>
<div v-if="settings.canEdit">
<div v-for="rule in rules" :key="rule.id">
<input
v-if="!rule.isNew"
:value="rule.id"
name="merge_request[approval_rules_attributes][][id]"
type="hidden"
/>
<input
v-if="rule.isNew && rule.sourceId"
:value="rule.sourceId"
name="merge_request[approval_rules_attributes][][approval_project_rule_id]"
type="hidden"
/>
<input
:value="rule.approvalsRequired"
name="merge_request[approval_rules_attributes][][approvals_required]"
type="hidden"
/>
<input
:value="rule.name"
name="merge_request[approval_rules_attributes][][name]"
type="hidden"
/>
<input
v-for="user in rule.users"
:key="user.id"
:value="user.id"
name="merge_request[approval_rules_attributes][][user_ids][]"
type="hidden"
/>
<input
v-for="group in rule.groups"
:key="group.id"
:value="group.id"
name="merge_request[approval_rules_attributes][][group_ids][]"
type="hidden"
/>
</div>
</div>
</template>
<script>
import { mapState } from 'vuex';
import { mapState, mapActions } from 'vuex';
import UserAvatarList from '~/vue_shared/components/user_avatar/user_avatar_list.vue';
import RulesBase from './rules_base.vue';
import RuleControls from './rule_controls.vue';
......@@ -13,6 +13,9 @@ export default {
computed: {
...mapState(['settings']),
},
methods: {
...mapActions(['putRule']),
},
};
</script>
......@@ -32,19 +35,13 @@ export default {
<user-avatar-list :items="rule.approvers" :img-size="24" />
</td>
<td>
<input
:name="`merge_request[approval_rules_attributes][][approval_project_rule_id]`"
:value="rule.id"
:disabled="!settings.canEdit"
type="hidden"
/>
<input
:value="rule.approvalsRequired"
:name="`merge_request[approval_rules_attributes][][approvals_required]`"
:disabled="!settings.canEdit"
class="form-control mw-6em"
type="number"
min="0"
@input="putRule({ id: rule.id, approvalsRequired: $event.target.value });"
/>
</td>
<td class="text-nowrap px-2 w-0"><rule-controls v-if="settings.canEdit" :rule="rule" /></td>
......
......@@ -12,6 +12,7 @@ export const mapApprovalRuleResponse = res => ({
approvers: res.approvers,
users: res.users,
groups: res.groups,
isCodeOwner: res.code_owner,
});
export const mapApprovalRulesResponse = req => ({
......
import createFlash from '~/flash';
import _ from 'underscore';
import { __ } from '~/locale';
import Api from 'ee/api';
import axios from '~/lib/utils/axios_utils';
import * as types from './mutation_types';
import { mapApprovalRulesResponse } from '../../../mappers';
const fetchGroupMembers = id => Api.groupMembers(id).then(response => response.data);
const fetchApprovers = ({ userRecords, groups }) => {
const groupUsersAsync = Promise.all(groups.map(fetchGroupMembers));
return groupUsersAsync
.then(_.flatten)
.then(groupUsers => groupUsers.concat(userRecords))
.then(users => _.uniq(users, false, x => x.id));
};
const seedApprovers = rule =>
rule.groups || rule.userRecords
? fetchApprovers(rule).then(approvers => ({
...rule,
approvers,
}))
: Promise.resolve(rule);
const seedUsers = ({ userRecords, ...rule }) =>
userRecords ? { ...rule, users: userRecords } : rule;
const seedGroups = ({ groupRecords, ...rule }) =>
groupRecords ? { ...rule, groups: groupRecords } : rule;
const seedRule = rule =>
seedApprovers(rule)
.then(seedUsers)
.then(seedGroups);
export const requestRules = ({ commit }) => {
commit(types.SET_LOADING, true);
};
export const receiveRulesSuccess = ({ commit }, rules) => {
commit(types.SET_LOADING, false);
commit(types.SET_RULES, rules);
};
export const receiveRulesError = () => {
createFlash(__('An error occurred fetching the approval rules.'));
};
export const fetchRules = ({ rootState, dispatch }) => {
dispatch('requestRules');
const { mrId } = rootState.settings;
const async = mrId ? dispatch('fetchMergeRequestRules') : dispatch('fetchProjectRules');
return async
.then(rules => dispatch('receiveRulesSuccess', rules))
.catch(() => dispatch('receiveRulesError'));
};
export const fetchProjectRules = ({ rootState }) => {
const { projectId } = rootState.settings;
// These will be `new` MR rules so we pull `id` out of the reponse and set it to `sourceId`
return Api.getProjectApprovalRules(projectId)
.then(response => mapApprovalRulesResponse(response.data))
.then(({ rules }) =>
rules.map(({ id, ...rule }) => ({
...rule,
sourceId: id,
isNew: true,
id: _.uniqueId('new'),
})),
);
};
export const fetchMergeRequestRules = ({ rootState }) => {
const { mrRulesPath } = rootState.settings;
return axios
.get(mrRulesPath)
.then(response => mapApprovalRulesResponse(response.data))
.then(({ rules }) => rules.filter(x => !x.isCodeOwner));
};
export const postRule = ({ commit, dispatch }, rule) =>
seedRule({ ...rule, isNew: true, id: _.uniqueId('new') })
.then(newRule => {
commit(types.POST_RULE, newRule);
dispatch('createModal/close');
})
.catch(e => {
createFlash(__('An error occurred fetching the approvers for the new rule.'));
throw e;
});
export const putRule = ({ commit, dispatch }, rule) =>
seedRule(rule)
.then(newRule => {
commit(types.PUT_RULE, newRule);
dispatch('createModal/close');
})
.catch(e => {
createFlash(__('An error occurred fetching the approvers for the new rule.'));
throw e;
});
export const deleteRule = ({ commit, dispatch }, id) => {
commit(types.DELETE_RULE, id);
dispatch('deleteModal/close');
};
import base from '../base';
import * as actions from './actions';
import mutations from './mutations';
export default () => ({
...base(),
actions,
mutations,
});
export * from '../base/mutation_types';
export const DELETE_RULE = 'DELETE_RULE';
export const PUT_RULE = 'PUT_RULE';
export const POST_RULE = 'POST_RULE';
import _ from 'underscore';
import base from '../base/mutations';
import * as types from './mutation_types';
export default {
...base,
[types.DELETE_RULE](state, id) {
const idx = _.findIndex(state.rules, x => x.id === id);
if (idx < 0) {
return;
}
state.rules.splice(idx, 1);
},
[types.PUT_RULE](state, { id, ...newRule }) {
const idx = _.findIndex(state.rules, x => x.id === id);
if (idx < 0) {
return;
}
const rule = { ...state.rules[idx], ...newRule };
state.rules.splice(idx, 1, rule);
},
[types.POST_RULE](state, rule) {
state.rules.push(rule);
},
};
......@@ -110,21 +110,24 @@ describe('Approvals RuleForm', () => {
});
it('on submit with data, posts rule', () => {
const users = [1, 2];
const groups = [2, 3];
const userRecords = users.map(id => ({ id, type: TYPE_USER }));
const groupRecords = groups.map(id => ({ id, type: TYPE_GROUP }));
const expected = {
name: 'Lorem',
approvalsRequired: 2,
users: [1, 2],
groups: [2, 3],
users,
groups,
userRecords,
groupRecords,
};
const name = findNameInput(wrapper);
const approvalsRequired = findApprovalsRequiredInput(wrapper);
const groups = expected.groups.map(id => ({ id, type: TYPE_GROUP }));
const users = expected.users.map(id => ({ id, type: TYPE_USER }));
name.node.setValue(expected.name);
approvalsRequired.node.setValue(expected.approvalsRequired);
wrapper.vm.approvers = groups.concat(users);
wrapper.vm.approvers = groupRecords.concat(userRecords);
wrapper.vm.submit();
......@@ -160,10 +163,17 @@ describe('Approvals RuleForm', () => {
});
it('on submit, puts rule', () => {
const userRecords = TEST_RULE.users.map(x => ({ ...x, type: TYPE_USER }));
const groupRecords = TEST_RULE.groups.map(x => ({ ...x, type: TYPE_GROUP }));
const users = userRecords.map(x => x.id);
const groups = groupRecords.map(x => x.id);
const expected = {
...TEST_RULE,
users: TEST_RULE.users.map(x => x.id),
groups: TEST_RULE.groups.map(x => x.id),
users,
groups,
userRecords,
groupRecords,
};
wrapper.vm.submit();
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment