diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss index f8a0706a204aa072ab0f2289a8bde89a7accd39a..1c0130010792780d91a19f1a28df2dee10bda020 100644 --- a/app/assets/stylesheets/framework/buttons.scss +++ b/app/assets/stylesheets/framework/buttons.scss @@ -166,7 +166,8 @@ @include btn-outline($white-light, $green-600, $green-500, $green-500, $white-light, $green-600, $green-600, $green-700); } - &.btn-remove { + &.btn-remove, + &.btn-danger { @include btn-outline($white-light, $red-500, $red-500, $red-500, $white-light, $red-600, $red-600, $red-700); } diff --git a/ee/app/assets/javascripts/approvals/components/app.vue b/ee/app/assets/javascripts/approvals/components/app.vue new file mode 100644 index 0000000000000000000000000000000000000000..d76487e3cd41e679cbedff75ca027978f6065022 --- /dev/null +++ b/ee/app/assets/javascripts/approvals/components/app.vue @@ -0,0 +1,61 @@ + + + diff --git a/ee/app/assets/javascripts/approvals/components/approvers_list.vue b/ee/app/assets/javascripts/approvals/components/approvers_list.vue new file mode 100644 index 0000000000000000000000000000000000000000..cf92a36bdb75262c0556d35f4cae008e3e986a0d --- /dev/null +++ b/ee/app/assets/javascripts/approvals/components/approvers_list.vue @@ -0,0 +1,35 @@ + + + diff --git a/ee/app/assets/javascripts/approvals/components/approvers_list_empty.vue b/ee/app/assets/javascripts/approvals/components/approvers_list_empty.vue new file mode 100644 index 0000000000000000000000000000000000000000..d1ac017f5517aac4ac7fcbd02e2eea93dc2fa34d --- /dev/null +++ b/ee/app/assets/javascripts/approvals/components/approvers_list_empty.vue @@ -0,0 +1,7 @@ + diff --git a/ee/app/assets/javascripts/approvals/components/approvers_list_item.vue b/ee/app/assets/javascripts/approvals/components/approvers_list_item.vue new file mode 100644 index 0000000000000000000000000000000000000000..1ad19127ab0e9030ab3b8aa6f9d263934a924363 --- /dev/null +++ b/ee/app/assets/javascripts/approvals/components/approvers_list_item.vue @@ -0,0 +1,44 @@ + + + diff --git a/ee/app/assets/javascripts/approvals/components/approvers_select.vue b/ee/app/assets/javascripts/approvals/components/approvers_select.vue new file mode 100644 index 0000000000000000000000000000000000000000..41017f09530fa91fd29f5d7b696d814828d75177 --- /dev/null +++ b/ee/app/assets/javascripts/approvals/components/approvers_select.vue @@ -0,0 +1,154 @@ + + + diff --git a/ee/app/assets/javascripts/approvals/components/fallback_rules.vue b/ee/app/assets/javascripts/approvals/components/fallback_rules.vue new file mode 100644 index 0000000000000000000000000000000000000000..54bb2ec7b9262b9c491d5b03da57ecf67108104c --- /dev/null +++ b/ee/app/assets/javascripts/approvals/components/fallback_rules.vue @@ -0,0 +1,68 @@ + + + diff --git a/ee/app/assets/javascripts/approvals/components/modal_rule_create.vue b/ee/app/assets/javascripts/approvals/components/modal_rule_create.vue new file mode 100644 index 0000000000000000000000000000000000000000..ba4a90eb56e542e7c8c0db08a37d7fc877a79ee7 --- /dev/null +++ b/ee/app/assets/javascripts/approvals/components/modal_rule_create.vue @@ -0,0 +1,46 @@ + + + diff --git a/ee/app/assets/javascripts/approvals/components/modal_rule_remove.vue b/ee/app/assets/javascripts/approvals/components/modal_rule_remove.vue new file mode 100644 index 0000000000000000000000000000000000000000..c45d8f07cf6baac89d45d4903ffd5feb07629960 --- /dev/null +++ b/ee/app/assets/javascripts/approvals/components/modal_rule_remove.vue @@ -0,0 +1,71 @@ + + + diff --git a/ee/app/assets/javascripts/approvals/components/mr_edit/app.vue b/ee/app/assets/javascripts/approvals/components/mr_edit/app.vue new file mode 100644 index 0000000000000000000000000000000000000000..55ae63e02ce983315be4d4f6a75f87a9225918c6 --- /dev/null +++ b/ee/app/assets/javascripts/approvals/components/mr_edit/app.vue @@ -0,0 +1,23 @@ + + + diff --git a/ee/app/assets/javascripts/approvals/components/mr_edit/mr_fallback_rules.vue b/ee/app/assets/javascripts/approvals/components/mr_edit/mr_fallback_rules.vue new file mode 100644 index 0000000000000000000000000000000000000000..ee69064288a89b4e36914a961501ef66e784ddc1 --- /dev/null +++ b/ee/app/assets/javascripts/approvals/components/mr_edit/mr_fallback_rules.vue @@ -0,0 +1,31 @@ + + + diff --git a/ee/app/assets/javascripts/approvals/components/mr_edit/mr_rules.vue b/ee/app/assets/javascripts/approvals/components/mr_edit/mr_rules.vue new file mode 100644 index 0000000000000000000000000000000000000000..abf164bafccaec399307f34e555df4a573460cc2 --- /dev/null +++ b/ee/app/assets/javascripts/approvals/components/mr_edit/mr_rules.vue @@ -0,0 +1,62 @@ + + + diff --git a/ee/app/assets/javascripts/approvals/components/mr_edit/mr_rules_hidden_inputs.vue b/ee/app/assets/javascripts/approvals/components/mr_edit/mr_rules_hidden_inputs.vue new file mode 100644 index 0000000000000000000000000000000000000000..0e7a9a61f111036c445988c01e17a8ae5ddbdde0 --- /dev/null +++ b/ee/app/assets/javascripts/approvals/components/mr_edit/mr_rules_hidden_inputs.vue @@ -0,0 +1,87 @@ + + + diff --git a/ee/app/assets/javascripts/approvals/components/project_settings/app.vue b/ee/app/assets/javascripts/approvals/components/project_settings/app.vue new file mode 100644 index 0000000000000000000000000000000000000000..3133244d61d7c395088faa054103dafc0413bed0 --- /dev/null +++ b/ee/app/assets/javascripts/approvals/components/project_settings/app.vue @@ -0,0 +1,15 @@ + + + diff --git a/ee/app/assets/javascripts/approvals/components/project_settings/project_rules.vue b/ee/app/assets/javascripts/approvals/components/project_settings/project_rules.vue new file mode 100644 index 0000000000000000000000000000000000000000..b48452b081925975f2835f6d3c3e42725ca53d77 --- /dev/null +++ b/ee/app/assets/javascripts/approvals/components/project_settings/project_rules.vue @@ -0,0 +1,86 @@ + + + diff --git a/ee/app/assets/javascripts/approvals/components/rule_controls.vue b/ee/app/assets/javascripts/approvals/components/rule_controls.vue new file mode 100644 index 0000000000000000000000000000000000000000..4a123f35474dbe377597454202dab5ee100c58b8 --- /dev/null +++ b/ee/app/assets/javascripts/approvals/components/rule_controls.vue @@ -0,0 +1,42 @@ + + + diff --git a/ee/app/assets/javascripts/approvals/components/rule_form.vue b/ee/app/assets/javascripts/approvals/components/rule_form.vue new file mode 100644 index 0000000000000000000000000000000000000000..9ff9566b9f20e5111f9f2410e15b73aec2718c0e --- /dev/null +++ b/ee/app/assets/javascripts/approvals/components/rule_form.vue @@ -0,0 +1,262 @@ + + + diff --git a/ee/app/assets/javascripts/approvals/components/rules.vue b/ee/app/assets/javascripts/approvals/components/rules.vue new file mode 100644 index 0000000000000000000000000000000000000000..f4a406b9860ed1d4ac52152bccd197a220e37a1f --- /dev/null +++ b/ee/app/assets/javascripts/approvals/components/rules.vue @@ -0,0 +1,32 @@ + + + diff --git a/ee/app/assets/javascripts/approvals/constants.js b/ee/app/assets/javascripts/approvals/constants.js new file mode 100644 index 0000000000000000000000000000000000000000..bd8532e23d6c54b8064e4d00183c7f1234e84c19 --- /dev/null +++ b/ee/app/assets/javascripts/approvals/constants.js @@ -0,0 +1,6 @@ +export const TYPE_USER = 'user'; +export const TYPE_GROUP = 'group'; + +export const RULE_TYPE_FALLBACK = 'fallback'; +export const RULE_TYPE_REGULAR = 'regular'; +export const RULE_TYPE_CODE_OWNER = 'code_owner'; diff --git a/ee/app/assets/javascripts/approvals/mappers.js b/ee/app/assets/javascripts/approvals/mappers.js new file mode 100644 index 0000000000000000000000000000000000000000..27b9c6ed04352a7123fe896064a3d484863edc82 --- /dev/null +++ b/ee/app/assets/javascripts/approvals/mappers.js @@ -0,0 +1,69 @@ +import _ from 'underscore'; +import { RULE_TYPE_REGULAR, RULE_TYPE_FALLBACK } from './constants'; + +export const mapApprovalRuleRequest = req => ({ + name: req.name, + approvals_required: req.approvalsRequired, + users: req.users, + groups: req.groups, +}); + +export const mapApprovalFallbackRuleRequest = req => ({ + fallback_approvals_required: req.approvalsRequired, +}); + +export const mapApprovalRuleResponse = res => ({ + id: res.id, + hasSource: !!res.source_rule, + name: res.name, + approvalsRequired: res.approvals_required, + minApprovalsRequired: res.source_rule ? res.source_rule.approvals_required : 0, + approvers: res.approvers, + users: res.users, + groups: res.groups, +}); + +export const mapApprovalSettingsResponse = res => ({ + rules: res.rules.map(mapApprovalRuleResponse), + fallbackApprovalsRequired: res.fallback_approvals_required, +}); + +/** + * Map the sourced approval rule response for the MR view + * + * This rule is sourced from project settings, which implies: + * - Not a real MR rule, so no "id". + * - The approvals required are the minimum. + */ +export const mapMRSourceRule = ({ id, ...rule }) => ({ + ...rule, + hasSource: true, + sourceId: id, + minApprovalsRequired: rule.approvalsRequired || 0, +}); + +/** + * Map the approval settings response for the MR view + * + * - Only show regular rules. + * - If needed, extract the fallback approvals required + * from the fallback rule. + */ +export const mapMRApprovalSettingsResponse = res => { + const rulesByType = _.groupBy(res.rules, x => x.rule_type); + + const regularRules = rulesByType[RULE_TYPE_REGULAR] || []; + + const [fallback] = rulesByType[RULE_TYPE_FALLBACK] || []; + const fallbackApprovalsRequired = fallback + ? fallback.approvals_required + : res.fallback_approvals_required || 0; + + return { + rules: regularRules + .map(mapApprovalRuleResponse) + .map(res.approval_rules_overwritten ? x => x : mapMRSourceRule), + fallbackApprovalsRequired, + minFallbackApprovalsRequired: 0, + }; +}; diff --git a/ee/app/assets/javascripts/approvals/mount_mr_edit.js b/ee/app/assets/javascripts/approvals/mount_mr_edit.js new file mode 100644 index 0000000000000000000000000000000000000000..62e533ed660ea91779166bac6d64c87c6dd54860 --- /dev/null +++ b/ee/app/assets/javascripts/approvals/mount_mr_edit.js @@ -0,0 +1,29 @@ +import Vue from 'vue'; +import Vuex from 'vuex'; +import { parseBoolean } from '~/lib/utils/common_utils'; +import createStore from './stores'; +import mrEditModule from './stores/modules/mr_edit'; +import MrEditApp from './components/mr_edit/app.vue'; + +Vue.use(Vuex); + +export default function mountApprovalInput(el) { + if (!el) { + return null; + } + + const store = createStore(mrEditModule(), { + ...el.dataset, + prefix: 'mr-edit', + canEdit: parseBoolean(el.dataset.canEdit), + allowMultiRule: parseBoolean(el.dataset.allowMultiRule), + }); + + return new Vue({ + el, + store, + render(h) { + return h(MrEditApp); + }, + }); +} diff --git a/ee/app/assets/javascripts/approvals/mount_project_settings.js b/ee/app/assets/javascripts/approvals/mount_project_settings.js new file mode 100644 index 0000000000000000000000000000000000000000..f8a335bb2859e7d831515d620101625bcb78b60b --- /dev/null +++ b/ee/app/assets/javascripts/approvals/mount_project_settings.js @@ -0,0 +1,28 @@ +import Vue from 'vue'; +import Vuex from 'vuex'; +import createStore from './stores'; +import projectSettingsModule from './stores/modules/project_settings'; +import ProjectSettingsApp from './components/project_settings/app.vue'; +import { parseBoolean } from '~/lib/utils/common_utils'; + +Vue.use(Vuex); + +export default function mountProjectSettingsApprovals(el) { + if (!el) { + return null; + } + + const store = createStore(projectSettingsModule(), { + ...el.dataset, + prefix: 'project-settings', + allowMultiRule: parseBoolean(el.dataset.allowMultiRule), + }); + + return new Vue({ + el, + store, + render(h) { + return h(ProjectSettingsApp); + }, + }); +} diff --git a/ee/app/assets/javascripts/approvals.js b/ee/app/assets/javascripts/approvals/setup_single_rule_approvals.js similarity index 100% rename from ee/app/assets/javascripts/approvals.js rename to ee/app/assets/javascripts/approvals/setup_single_rule_approvals.js diff --git a/ee/app/assets/javascripts/approvals/stores/index.js b/ee/app/assets/javascripts/approvals/stores/index.js new file mode 100644 index 0000000000000000000000000000000000000000..b17514eab74678f0cb7ba9b621973d04299ca357 --- /dev/null +++ b/ee/app/assets/javascripts/approvals/stores/index.js @@ -0,0 +1,15 @@ +import Vuex from 'vuex'; +import modalModule from '~/vuex_shared/modules/modal'; +import state from './state'; + +export const createStoreOptions = (approvalsModule, settings) => ({ + state: state(settings), + modules: { + ...(approvalsModule ? { approvals: approvalsModule } : {}), + createModal: modalModule(), + deleteModal: modalModule(), + }, +}); + +export default (approvalsModule, settings = {}) => + new Vuex.Store(createStoreOptions(approvalsModule, settings)); diff --git a/ee/app/assets/javascripts/approvals/stores/modules/base/getters.js b/ee/app/assets/javascripts/approvals/stores/modules/base/getters.js new file mode 100644 index 0000000000000000000000000000000000000000..851813512c359572f17f292a216854abf509d1e2 --- /dev/null +++ b/ee/app/assets/javascripts/approvals/stores/modules/base/getters.js @@ -0,0 +1,3 @@ +export const isEmpty = state => !state.rules || !state.rules.length; + +export default () => {}; diff --git a/ee/app/assets/javascripts/approvals/stores/modules/base/index.js b/ee/app/assets/javascripts/approvals/stores/modules/base/index.js new file mode 100644 index 0000000000000000000000000000000000000000..260b0ac26f8c2de97de8aac1b4df62e69b3114ac --- /dev/null +++ b/ee/app/assets/javascripts/approvals/stores/modules/base/index.js @@ -0,0 +1,9 @@ +import createState from './state'; +import mutations from './mutations'; +import * as getters from './getters'; + +export default () => ({ + state: createState(), + mutations, + getters, +}); diff --git a/ee/app/assets/javascripts/approvals/stores/modules/base/mutation_types.js b/ee/app/assets/javascripts/approvals/stores/modules/base/mutation_types.js new file mode 100644 index 0000000000000000000000000000000000000000..3e6a8207c2eb19a0c84a2c34a6d5626102988432 --- /dev/null +++ b/ee/app/assets/javascripts/approvals/stores/modules/base/mutation_types.js @@ -0,0 +1,2 @@ +export const SET_LOADING = 'SET_LOADING'; +export const SET_APPROVAL_SETTINGS = 'SET_APPROVAL_SETTINGS'; diff --git a/ee/app/assets/javascripts/approvals/stores/modules/base/mutations.js b/ee/app/assets/javascripts/approvals/stores/modules/base/mutations.js new file mode 100644 index 0000000000000000000000000000000000000000..236e10797ba3f9a37a524b9ba87ecba1b70d4803 --- /dev/null +++ b/ee/app/assets/javascripts/approvals/stores/modules/base/mutations.js @@ -0,0 +1,13 @@ +import * as types from './mutation_types'; + +export default { + [types.SET_LOADING](state, isLoading) { + state.isLoading = isLoading; + }, + [types.SET_APPROVAL_SETTINGS](state, settings) { + state.hasLoaded = true; + state.rules = settings.rules; + state.fallbackApprovalsRequired = settings.fallbackApprovalsRequired; + state.minFallbackApprovalsRequired = settings.minFallbackApprovalsRequired; + }, +}; diff --git a/ee/app/assets/javascripts/approvals/stores/modules/base/state.js b/ee/app/assets/javascripts/approvals/stores/modules/base/state.js new file mode 100644 index 0000000000000000000000000000000000000000..2b2b6ec45575890ec8589ace325b39a5fc52a22d --- /dev/null +++ b/ee/app/assets/javascripts/approvals/stores/modules/base/state.js @@ -0,0 +1,7 @@ +export default () => ({ + hasLoaded: false, + isLoading: false, + rules: [], + fallbackApprovalsRequired: 0, + minFallbackApprovalsRequired: 0, +}); diff --git a/ee/app/assets/javascripts/approvals/stores/modules/mr_edit/actions.js b/ee/app/assets/javascripts/approvals/stores/modules/mr_edit/actions.js new file mode 100644 index 0000000000000000000000000000000000000000..9b20e67dd864c849c305017f019e2d9b8e6e0cc1 --- /dev/null +++ b/ee/app/assets/javascripts/approvals/stores/modules/mr_edit/actions.js @@ -0,0 +1,116 @@ +import createFlash from '~/flash'; +import _ from 'underscore'; +import { __ } from '~/locale'; +import Api from '~/api'; +import axios from '~/lib/utils/axios_utils'; +import * as types from './mutation_types'; +import { mapMRApprovalSettingsResponse } from '../../../mappers'; + +const fetchGroupMembers = _.memoize(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 seedLocalRule = rule => + seedApprovers(rule) + .then(seedUsers) + .then(seedGroups); + +const seedNewRule = rule => ({ + ...rule, + isNew: true, + id: _.uniqueId('new'), +}); + +export const requestRules = ({ commit }) => { + commit(types.SET_LOADING, true); +}; + +export const receiveRulesSuccess = ({ commit }, settings) => { + commit(types.SET_LOADING, false); + commit(types.SET_APPROVAL_SETTINGS, settings); +}; + +export const receiveRulesError = () => { + createFlash(__('An error occurred fetching the approval rules.')); +}; + +export const fetchRules = ({ rootState, dispatch }) => { + dispatch('requestRules'); + + const { mrSettingsPath, projectSettingsPath } = rootState.settings; + const path = mrSettingsPath || projectSettingsPath; + + return axios + .get(path) + .then(response => mapMRApprovalSettingsResponse(response.data)) + .then(settings => ({ + ...settings, + rules: settings.rules.map(x => (x.id ? x : seedNewRule(x))), + })) + .then(settings => dispatch('receiveRulesSuccess', settings)) + .catch(() => dispatch('receiveRulesError')); +}; + +export const postRule = ({ commit, dispatch }, rule) => + seedLocalRule(rule) + .then(seedNewRule) + .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) => + seedLocalRule(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'); +}; + +export const putFallbackRule = ({ commit, dispatch }, fallback) => { + commit(types.SET_FALLBACK_RULE, fallback); + dispatch('createModal/close'); +}; + +export const requestEditRule = ({ dispatch }, rule) => { + dispatch('createModal/open', rule); +}; + +export const requestDeleteRule = ({ dispatch }, rule) => { + dispatch('deleteRule', rule.id); +}; + +export default () => {}; diff --git a/ee/app/assets/javascripts/approvals/stores/modules/mr_edit/index.js b/ee/app/assets/javascripts/approvals/stores/modules/mr_edit/index.js new file mode 100644 index 0000000000000000000000000000000000000000..116f62aa42754cc75e545eced0bfe0f133129f71 --- /dev/null +++ b/ee/app/assets/javascripts/approvals/stores/modules/mr_edit/index.js @@ -0,0 +1,11 @@ +import base from '../base'; +import * as actions from './actions'; +import mutations from './mutations'; +import createState from './state'; + +export default () => ({ + ...base(), + state: createState(), + actions, + mutations, +}); diff --git a/ee/app/assets/javascripts/approvals/stores/modules/mr_edit/mutation_types.js b/ee/app/assets/javascripts/approvals/stores/modules/mr_edit/mutation_types.js new file mode 100644 index 0000000000000000000000000000000000000000..a278fbeccd646539dc6899a206b40229d8c7b1e8 --- /dev/null +++ b/ee/app/assets/javascripts/approvals/stores/modules/mr_edit/mutation_types.js @@ -0,0 +1,6 @@ +export * from '../base/mutation_types'; + +export const DELETE_RULE = 'DELETE_RULE'; +export const PUT_RULE = 'PUT_RULE'; +export const POST_RULE = 'POST_RULE'; +export const SET_FALLBACK_RULE = 'SET_FALLBACK_RULE'; diff --git a/ee/app/assets/javascripts/approvals/stores/modules/mr_edit/mutations.js b/ee/app/assets/javascripts/approvals/stores/modules/mr_edit/mutations.js new file mode 100644 index 0000000000000000000000000000000000000000..55da0ca0e1c753ab5b6f81a8212bc67d2d13882f --- /dev/null +++ b/ee/app/assets/javascripts/approvals/stores/modules/mr_edit/mutations.js @@ -0,0 +1,39 @@ +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; + } + + const rule = state.rules[idx]; + + // Keep track of rules we need to submit that are deleted + if (!rule.isNew) { + state.rulesToDelete.push(rule.id); + } + + 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); + }, + [types.SET_FALLBACK_RULE](state, fallback) { + state.fallbackApprovalsRequired = fallback.approvalsRequired; + }, +}; diff --git a/ee/app/assets/javascripts/approvals/stores/modules/mr_edit/state.js b/ee/app/assets/javascripts/approvals/stores/modules/mr_edit/state.js new file mode 100644 index 0000000000000000000000000000000000000000..1712593c907ee7dc2922033c82fc494a07469fc3 --- /dev/null +++ b/ee/app/assets/javascripts/approvals/stores/modules/mr_edit/state.js @@ -0,0 +1,6 @@ +import baseState from '../base/state'; + +export default () => ({ + ...baseState(), + rulesToDelete: [], +}); diff --git a/ee/app/assets/javascripts/approvals/stores/modules/project_settings/actions.js b/ee/app/assets/javascripts/approvals/stores/modules/project_settings/actions.js new file mode 100644 index 0000000000000000000000000000000000000000..79f9e7d93c2436c9ab15d977ec8dba233653aca6 --- /dev/null +++ b/ee/app/assets/javascripts/approvals/stores/modules/project_settings/actions.js @@ -0,0 +1,106 @@ +import createFlash from '~/flash'; +import { __ } from '~/locale'; +import axios from '~/lib/utils/axios_utils'; +import * as types from '../base/mutation_types'; +import { + mapApprovalRuleRequest, + mapApprovalSettingsResponse, + mapApprovalFallbackRuleRequest, +} from '../../../mappers'; + +export const requestRules = ({ commit }) => { + commit(types.SET_LOADING, true); +}; + +export const receiveRulesSuccess = ({ commit }, approvalSettings) => { + commit(types.SET_APPROVAL_SETTINGS, approvalSettings); + commit(types.SET_LOADING, false); +}; + +export const receiveRulesError = () => { + createFlash(__('An error occurred fetching the approval rules.')); +}; + +export const fetchRules = ({ rootState, dispatch }) => { + const { settingsPath } = rootState.settings; + + dispatch('requestRules'); + + return axios + .get(settingsPath) + .then(response => dispatch('receiveRulesSuccess', mapApprovalSettingsResponse(response.data))) + .catch(() => dispatch('receiveRulesError')); +}; + +export const postRuleSuccess = ({ dispatch }) => { + dispatch('createModal/close'); + dispatch('fetchRules'); +}; + +export const postRuleError = () => { + createFlash(__('An error occurred while updating approvers')); +}; + +export const postRule = ({ rootState, dispatch }, rule) => { + const { rulesPath } = rootState.settings; + + return axios + .post(rulesPath, mapApprovalRuleRequest(rule)) + .then(() => dispatch('postRuleSuccess')) + .catch(() => dispatch('postRuleError')); +}; + +export const putRule = ({ rootState, dispatch }, { id, ...newRule }) => { + const { rulesPath } = rootState.settings; + + return axios + .put(`${rulesPath}/${id}`, mapApprovalRuleRequest(newRule)) + .then(() => dispatch('postRuleSuccess')) + .catch(() => dispatch('postRuleError')); +}; + +export const deleteRuleSuccess = ({ dispatch }) => { + dispatch('deleteModal/close'); + dispatch('fetchRules'); +}; + +export const deleteRuleError = () => { + createFlash(__('An error occurred while deleting the approvers group')); +}; + +export const deleteRule = ({ rootState, dispatch }, id) => { + const { rulesPath } = rootState.settings; + + return axios + .delete(`${rulesPath}/${id}`) + .then(() => dispatch('deleteRuleSuccess')) + .catch(() => dispatch('deleteRuleError')); +}; + +export const putFallbackRuleSuccess = ({ dispatch }) => { + dispatch('createModal/close'); + dispatch('fetchRules'); +}; + +export const putFallbackRuleError = () => { + createFlash(__('An error occurred while saving the approval settings')); +}; + +export const putFallbackRule = ({ rootState, dispatch }, fallback) => { + const { projectPath } = rootState.settings; + + return axios + .put(projectPath, mapApprovalFallbackRuleRequest(fallback)) + .then(() => dispatch('putFallbackRuleSuccess')) + .catch(() => dispatch('putFallbackRuleError')); +}; + +export const requestEditRule = ({ dispatch }, rule) => { + dispatch('createModal/open', rule); +}; + +export const requestDeleteRule = ({ dispatch }, rule) => { + dispatch('deleteModal/open', rule); +}; + +export default () => {}; diff --git a/ee/app/assets/javascripts/approvals/stores/modules/project_settings/index.js b/ee/app/assets/javascripts/approvals/stores/modules/project_settings/index.js new file mode 100644 index 0000000000000000000000000000000000000000..657b31ea6c4a1b23516dcae0cbb88f89ff0f3aee --- /dev/null +++ b/ee/app/assets/javascripts/approvals/stores/modules/project_settings/index.js @@ -0,0 +1,7 @@ +import base from '../base'; +import * as actions from './actions'; + +export default () => ({ + ...base(), + actions, +}); diff --git a/ee/app/assets/javascripts/approvals/stores/state.js b/ee/app/assets/javascripts/approvals/stores/state.js new file mode 100644 index 0000000000000000000000000000000000000000..fbde9034beb44512c8af200aa0372c4c970dfebf --- /dev/null +++ b/ee/app/assets/javascripts/approvals/stores/state.js @@ -0,0 +1,11 @@ +export const DEFAULT_SETTINGS = { + canEdit: true, + allowMultiRule: false, +}; + +export default (settings = {}) => ({ + settings: { + ...DEFAULT_SETTINGS, + ...settings, + }, +}); diff --git a/ee/app/assets/javascripts/pages/projects/edit/index.js b/ee/app/assets/javascripts/pages/projects/edit/index.js index c62a23c057693bd2c9e99c3ad4a43b87d52b61bb..59f658726bf818766b123b9cc0044e26e6a74d8a 100644 --- a/ee/app/assets/javascripts/pages/projects/edit/index.js +++ b/ee/app/assets/javascripts/pages/projects/edit/index.js @@ -5,6 +5,7 @@ import UsersSelect from '~/users_select'; import UserCallout from '~/user_callout'; import groupsSelect from '~/groups_select'; import ApproversSelect from 'ee/approvers_select'; +import mountApprovals from 'ee/approvals/mount_project_settings'; import initServiceDesk from 'ee/projects/settings_service_desk'; document.addEventListener('DOMContentLoaded', () => { @@ -15,4 +16,5 @@ document.addEventListener('DOMContentLoaded', () => { new UserCallout({ className: 'js-mr-approval-callout' }); new ApproversSelect(); initServiceDesk(); + mountApprovals(document.getElementById('js-mr-approvals-settings')); }); diff --git a/ee/app/assets/javascripts/pages/projects/merge_requests/shared/init_form.js b/ee/app/assets/javascripts/pages/projects/merge_requests/shared/init_form.js index b3d9865a2d4f7b0b4e74b46a8f591895b5433962..fb3c0a2b1ee32f06c57484dc8168d30c3a253e59 100644 --- a/ee/app/assets/javascripts/pages/projects/merge_requests/shared/init_form.js +++ b/ee/app/assets/javascripts/pages/projects/merge_requests/shared/init_form.js @@ -1,3 +1,7 @@ -import initApprovals from '../../../../approvals'; +import initApprovals from 'ee/approvals/setup_single_rule_approvals'; +import mountApprovals from 'ee/approvals/mount_mr_edit'; -export default () => initApprovals(); +export default () => { + initApprovals(); + mountApprovals(document.getElementById('js-mr-approvals-input')); +}; diff --git a/ee/app/assets/javascripts/vue_merge_request_widget/components/approvals/index.js b/ee/app/assets/javascripts/vue_merge_request_widget/components/approvals/index.js new file mode 100644 index 0000000000000000000000000000000000000000..dd562eac3ad5d048509095124515176467804e87 --- /dev/null +++ b/ee/app/assets/javascripts/vue_merge_request_widget/components/approvals/index.js @@ -0,0 +1,10 @@ +import MultipleRuleApprovals from './multiple_rule/approvals.vue'; +import SingleRuleApprovals from './single_rule/approvals.vue'; + +export default { + functional: true, + render(h, context) { + const component = gon.features.approvalRules ? MultipleRuleApprovals : SingleRuleApprovals; + return h(component, context.data, context.children); + }, +}; diff --git a/ee/app/assets/javascripts/vue_merge_request_widget/components/approvals/messages.js b/ee/app/assets/javascripts/vue_merge_request_widget/components/approvals/messages.js new file mode 100644 index 0000000000000000000000000000000000000000..0538c38307b420d98544d5c04138385583114607 --- /dev/null +++ b/ee/app/assets/javascripts/vue_merge_request_widget/components/approvals/messages.js @@ -0,0 +1,9 @@ +import { __, s__ } from '~/locale'; + +export const FETCH_LOADING = __('Checking approval status'); +export const FETCH_ERROR = s__( + 'mrWidget|An error occurred while retrieving approval data for this merge request.', +); +export const APPROVE_ERROR = s__('mrWidget|An error occurred while submitting your approval.'); +export const UNAPPROVE_ERROR = s__('mrWidget|An error occurred while removing your approval.'); +export const APPROVED_MESSAGE = s__('mrWidget|Merge request approved.'); diff --git a/ee/app/assets/javascripts/vue_merge_request_widget/components/approvals/multiple_rule/approvals.vue b/ee/app/assets/javascripts/vue_merge_request_widget/components/approvals/multiple_rule/approvals.vue new file mode 100644 index 0000000000000000000000000000000000000000..f385c79e95ed6be56fbe2a59daffe899b40b5f02 --- /dev/null +++ b/ee/app/assets/javascripts/vue_merge_request_widget/components/approvals/multiple_rule/approvals.vue @@ -0,0 +1,179 @@ + + + diff --git a/ee/app/assets/javascripts/vue_merge_request_widget/components/approvals/multiple_rule/approvals_footer.vue b/ee/app/assets/javascripts/vue_merge_request_widget/components/approvals/multiple_rule/approvals_footer.vue new file mode 100644 index 0000000000000000000000000000000000000000..ebf85e1eb43781a98fca6f43542ffd2665a0e365 --- /dev/null +++ b/ee/app/assets/javascripts/vue_merge_request_widget/components/approvals/multiple_rule/approvals_footer.vue @@ -0,0 +1,82 @@ + + + diff --git a/ee/app/assets/javascripts/vue_merge_request_widget/components/approvals/multiple_rule/approvals_list.vue b/ee/app/assets/javascripts/vue_merge_request_widget/components/approvals/multiple_rule/approvals_list.vue new file mode 100644 index 0000000000000000000000000000000000000000..19ef579a79d970de8dca9491df04ff3ea6ea2447 --- /dev/null +++ b/ee/app/assets/javascripts/vue_merge_request_widget/components/approvals/multiple_rule/approvals_list.vue @@ -0,0 +1,99 @@ + + + diff --git a/ee/app/assets/javascripts/vue_merge_request_widget/components/approvals/multiple_rule/approvals_summary.vue b/ee/app/assets/javascripts/vue_merge_request_widget/components/approvals/multiple_rule/approvals_summary.vue new file mode 100644 index 0000000000000000000000000000000000000000..11bfa19e5451469e35607936914980b1a36772de --- /dev/null +++ b/ee/app/assets/javascripts/vue_merge_request_widget/components/approvals/multiple_rule/approvals_summary.vue @@ -0,0 +1,68 @@ + + + diff --git a/ee/app/assets/javascripts/vue_merge_request_widget/components/approvals/multiple_rule/approved_icon.vue b/ee/app/assets/javascripts/vue_merge_request_widget/components/approvals/multiple_rule/approved_icon.vue new file mode 100644 index 0000000000000000000000000000000000000000..a31978b4969a4ebaae73411c87452c24f74551da --- /dev/null +++ b/ee/app/assets/javascripts/vue_merge_request_widget/components/approvals/multiple_rule/approved_icon.vue @@ -0,0 +1,20 @@ + + + diff --git a/ee/app/assets/javascripts/vue_merge_request_widget/components/approvals/mr_widget_approvals.vue b/ee/app/assets/javascripts/vue_merge_request_widget/components/approvals/single_rule/approvals.vue similarity index 86% rename from ee/app/assets/javascripts/vue_merge_request_widget/components/approvals/mr_widget_approvals.vue rename to ee/app/assets/javascripts/vue_merge_request_widget/components/approvals/single_rule/approvals.vue index 0e8039e7963a6019a4533fcf3cf30aafed0fdced..92ae3edb1fe47ef95797b1c46b0294045869275a 100644 --- a/ee/app/assets/javascripts/vue_merge_request_widget/components/approvals/mr_widget_approvals.vue +++ b/ee/app/assets/javascripts/vue_merge_request_widget/components/approvals/single_rule/approvals.vue @@ -1,13 +1,13 @@