Skip to content
Snippets Groups Projects
Commit f7ea0b87 authored by Kushal Pandya's avatar Kushal Pandya
Browse files

Merge branch '1979-fe-part4-approvals-shared-components' into '1979-fe-multiple-approval-rules'

(Part 4) FE multiple approval rules - setup for mr

See merge request gitlab-org/gitlab-ee!9176
parents 04b316e3 1dd9fd50
No related branches found
No related tags found
2 merge requests!9280WIP: FE Demo for Multiple Approval Rules,!9176(Part 4) FE multiple approval rules - setup for mr
Pipeline #44476169 failed
Showing
with 231 additions and 128 deletions
<script>
import { mapState, mapActions } from 'vuex';
import { mapState, mapActions, mapGetters } from 'vuex';
import { GlLoadingIcon, GlButton } from '@gitlab/ui';
import ModalRuleCreate from './modal_rule_create.vue';
import ModalRuleRemove from './modal_rule_remove.vue';
import RulesEmpty from './rules_empty.vue';
import Rules from './rules.vue';
const CREATE_MODAL_ID = 'approvals-settings-create-modal';
const REMOVE_MODAL_ID = 'approvals-settings-remove-modal';
export default {
components: {
ModalRuleCreate,
ModalRuleRemove,
Rules,
RulesEmpty,
GlButton,
GlLoadingIcon,
},
computed: {
...mapState(['isLoading', 'rules']),
isEmpty() {
return !this.rules || !this.rules.length;
...mapState({
settings: 'settings',
isLoading: state => state.approvals.isLoading,
}),
...mapGetters(['isEmpty']),
createModalId() {
return `${this.settings.prefix}-approvals-create-modal`;
},
removeModalId() {
return `${this.settings.prefix}-approvals-remove-modal`;
},
},
created() {
......@@ -30,10 +32,7 @@ export default {
methods: {
...mapActions(['fetchRules']),
...mapActions({ openCreateModal: 'createModal/open' }),
...mapActions({ openDeleteModal: 'deleteModal/open' }),
},
CREATE_MODAL_ID,
REMOVE_MODAL_ID,
};
</script>
......@@ -41,25 +40,21 @@ export default {
<div>
<template v-if="isEmpty">
<gl-loading-icon v-if="isLoading" :size="2" />
<rules-empty v-else @click="openCreateModal(null);" />
<rules-empty v-else @click="openCreateModal(null)" />
</template>
<template v-else>
<rules
class="m-0"
:rules="rules"
@edit="openCreateModal($event);"
@remove="openDeleteModal($event);"
/>
<div class="border-top border-bottom py-3 px-2">
<div class="border-bottom"><slot name="rules"></slot></div>
<div v-if="settings.canEdit" class="border-bottom py-3 px-2">
<gl-loading-icon v-if="isLoading" />
<div class="d-flex">
<gl-button class="ml-auto btn-info btn-inverted" @click="openCreateModal(null);">{{
<gl-button class="ml-auto btn-info btn-inverted" @click="openCreateModal(null)">{{
__('Add approvers')
}}</gl-button>
</div>
</div>
</template>
<modal-rule-create :modal-id="$options.CREATE_MODAL_ID" />
<modal-rule-remove :modal-id="$options.REMOVE_MODAL_ID" />
<slot name="footer"></slot>
<modal-rule-create :modal-id="createModalId" />
<modal-rule-remove :modal-id="removeModalId" />
</div>
</template>
......@@ -29,7 +29,7 @@ export default {
v-for="(approver, index) in value"
:key="approver.type + approver.id"
:approver="approver"
@remove="removeApprover(index);"
@remove="removeApprover(index)"
/>
</ul>
</template>
......@@ -35,7 +35,7 @@ export default {
<li class="settings-flex-row">
<div class="px-3 d-flex align-items-center">
<avatar :project="approver" :size="24" /><span>{{ displayName }}</span>
<gl-button variant="none" class="ml-auto" @click="$emit('remove', approver);">
<gl-button variant="none" class="ml-auto" @click="$emit('remove', approver)">
<icon name="remove" :aria-label="__('Remove')" />
</gl-button>
</div>
......
<script>
import App from '../app.vue';
import ProjectRules from './project_rules.vue';
export default {
components: {
App,
ProjectRules,
},
};
</script>
<template>
<app><project-rules slot="rules"/></app>
</template>
<script>
import { n__, sprintf } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue';
import UserAvatarList from '~/vue_shared/components/user_avatar/user_avatar_list.vue';
import Rules from '../rules.vue';
import RuleControls from '../rule_controls.vue';
export default {
components: {
Icon,
UserAvatarList,
Rules,
RuleControls,
},
methods: {
summaryText(rule) {
return sprintf(
n__(
'%{count} approval required from %{name}',
'%{count} approvals required from %{name}',
rule.approvalsRequired,
),
{ name: rule.name, count: rule.approvalsRequired },
);
},
},
};
</script>
<template>
<rules>
<template slot="thead">
<tr class="d-none d-sm-table-row">
<th>{{ s__('ApprovalRule|Name') }}</th>
<th class="w-50">{{ s__('ApprovalRule|Members') }}</th>
<th>{{ s__('ApprovalRule|No. approvals required') }}</th>
<th></th>
</tr>
</template>
<template slot="tr" slot-scope="{ rule }">
<td data-name="name">
<div class="d-none d-sm-block">{{ rule.name }}</div>
<div class="d-block d-sm-none">{{ summaryText(rule) }}</div>
</td>
<td data-name="members" class="d-none d-sm-table-cell">
<user-avatar-list :items="rule.approvers" :img-size="24" />
</td>
<td data-name="approvals_required" class="d-none d-sm-table-cell">
<icon name="approval" class="align-top text-tertiary" />
<span>{{ rule.approvalsRequired }}</span>
</td>
<td data-name="controls" class="text-nowrap px-2 w-0"><rule-controls :rule="rule" /></td>
</template>
</rules>
</template>
<script>
import { mapActions } from 'vuex';
import { GlButton } from '@gitlab/ui';
import Icon from '~/vue_shared/components/icon.vue';
export default {
components: {
GlButton,
Icon,
},
props: {
rule: {
type: Object,
required: true,
},
},
methods: {
...mapActions({ openCreateModal: 'createModal/open' }),
...mapActions({ openDeleteModal: 'deleteModal/open' }),
},
};
</script>
<template>
<div>
<gl-button variant="none" @click="openCreateModal(rule)">
<icon name="pencil" :aria-label="__('Edit')" /> </gl-button
><gl-button class="prepend-left-8 btn-inverted" variant="remove" @click="openDeleteModal(rule)">
<icon name="remove" :aria-label="__('Remove')" />
</gl-button>
</div>
</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) {
......@@ -84,6 +90,8 @@ export default {
approvalsRequired: this.approvalsRequired,
users: this.userIds,
groups: this.groupIds,
userRecords: this.users,
groupRecords: this.groups,
};
this.showValidation = true;
......@@ -131,7 +139,7 @@ export default {
<div class="form-group col-sm-6">
<label class="label-wrapper">
<span class="form-label label-bold">{{
s__('ApprovalRuleForm|No. approvals required')
s__('ApprovalRule|No. approvals required')
}}</span>
<input
v-model.number="approvalsRequired"
......
<script>
import { GlButton } from '@gitlab/ui';
import { n__, sprintf } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue';
import UserAvatarList from '~/vue_shared/components/user_avatar/user_avatar_list.vue';
import { mapState } from 'vuex';
export default {
components: {
GlButton,
Icon,
UserAvatarList,
},
props: {
rules: {
type: Array,
required: true,
default: () => [],
},
},
methods: {
summaryText(rule) {
return sprintf(
n__(
'%d approval required from %{name}',
'%d approvals required from %{name}',
rule.approvalsRequired,
),
{ name: rule.name },
);
},
computed: {
...mapState({
rules: state => state.approvals.rules,
}),
},
};
</script>
<template>
<table class="table">
<table class="table m-0">
<thead class="thead-white text-nowrap">
<tr class="d-none d-sm-table-row">
<th>{{ s__('ApprovalRule|Name') }}</th>
<th class="w-50">{{ s__('ApprovalRule|Members') }}</th>
<th>{{ s__('ApprovalRuleForm|No. approvals required') }}</th>
<th></th>
</tr>
<slot name="thead"></slot>
</thead>
<tbody>
<tr v-for="rule in rules" :key="rule.id">
<td>
<div class="d-none d-sm-block">{{ rule.name }}</div>
<div class="d-block d-sm-none">{{ summaryText(rule) }}</div>
</td>
<td class="d-none d-sm-table-cell">
<div v-if="!rule.approvers.length">{{ __('None') }}</div>
<user-avatar-list v-else :items="rule.approvers" :img-size="24" />
</td>
<td class="d-none d-sm-table-cell">
<icon name="approval" class="align-top text-tertiary" />
<span>{{ rule.approvalsRequired }}</span>
</td>
<td class="text-nowrap px-2 w-0">
<gl-button variant="none" @click="$emit('edit', rule);">
<icon name="pencil" :aria-label="__('Edit')" /> </gl-button
><gl-button
class="prepend-left-8 btn-inverted"
variant="remove"
@click="$emit('remove', rule);"
>
<icon name="remove" :aria-label="__('Remove')" />
</gl-button>
</td>
<slot :rule="rule" name="tr"></slot>
</tr>
</tbody>
</table>
......
<script>
import { mapState } from 'vuex';
import { GlButton } from '@gitlab/ui';
import { __ } from '~/locale';
import Callout from '~/vue_shared/components/callout.vue';
......@@ -12,15 +13,18 @@ export default {
Callout,
GlButton,
},
computed: {
...mapState(['settings']),
},
message,
};
</script>
<template>
<callout category="info">
<callout class="m-0" category="info">
<div>{{ $options.message }}</div>
<div class="prepend-top-default">
<gl-button class="btn-info btn-inverted" @click="$emit('click');">{{
<div v-if="settings.canEdit" class="prepend-top-default">
<gl-button class="btn-info btn-inverted" @click="$emit('click')">{{
__('Add approvers')
}}</gl-button>
</div>
......
import Vue from 'vue';
import Vuex from 'vuex';
import createStore from './stores';
import Settings from './components/settings.vue';
import projectSettingsModule from './stores/modules/project_settings';
import ProjectSettingsApp from './components/project_settings/app.vue';
Vue.use(Vuex);
export default function mountApprovalsSettings(el) {
export default function mountProjectSettingsApprovals(el) {
if (!el) {
return null;
}
const store = createStore();
store.dispatch('setSettings', el.dataset);
const store = createStore(projectSettingsModule(), {
prefix: 'project-settings',
...el.dataset,
});
return new Vue({
el,
store,
render(h) {
return h(Settings);
return h(ProjectSettingsApp);
},
});
}
import Vuex from 'vuex';
import modalModule from '~/vuex_shared/modules/modal';
import state from './state';
import mutations from './mutations';
import * as actions from './actions';
export default () =>
new Vuex.Store({
state: state(),
mutations,
actions,
modules: {
createModal: modalModule(),
deleteModal: modalModule(),
},
});
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));
export const isEmpty = state => !state.rules || !state.rules.length;
export default () => {};
import createState from './state';
import * as getters from './getters';
import mutations from './mutations';
export default () => ({
state: createState(),
getters,
mutations,
});
export const SET_SETTINGS = 'SET_SETTINGS';
export const SET_LOADING = 'SET_LOADING';
export const SET_RULES = 'SET_RULES';
import * as types from './mutation_types';
export default {
[types.SET_SETTINGS](state, settings) {
state.settings = { ...settings };
},
[types.SET_LOADING](state, isLoading) {
state.isLoading = isLoading;
},
......
export default () => ({
isLoading: false,
rules: [],
});
import createFlash from '~/flash';
import { __ } from '~/locale';
import Api from 'ee/api';
import * as types from './mutation_types';
import { mapApprovalRuleRequest, mapApprovalRulesResponse } from '../mappers';
export const setSettings = ({ commit }, settings) => {
commit(types.SET_SETTINGS, settings);
};
import * as types from '../base/mutation_types';
import { mapApprovalRuleRequest, mapApprovalRulesResponse } from '../../../mappers';
export const requestRules = ({ commit }) => {
commit(types.SET_LOADING, true);
......@@ -21,12 +17,8 @@ export const receiveRulesError = () => {
createFlash(__('An error occurred fetching the approval rules.'));
};
export const fetchRules = ({ state, dispatch }) => {
if (state.isLoading) {
return Promise.resolve();
}
const { projectId } = state.settings;
export const fetchRules = ({ rootState, dispatch }) => {
const { projectId } = rootState.settings;
dispatch('requestRules');
......@@ -44,16 +36,16 @@ export const postRuleError = () => {
createFlash(__('An error occurred while updating approvers'));
};
export const postRule = ({ state, dispatch }, rule) => {
const { projectId } = state.settings;
export const postRule = ({ rootState, dispatch }, rule) => {
const { projectId } = rootState.settings;
return Api.postProjectApprovalRule(projectId, mapApprovalRuleRequest(rule))
.then(() => dispatch('postRuleSuccess'))
.catch(() => dispatch('postRuleError'));
};
export const putRule = ({ state, dispatch }, { id, ...newRule }) => {
const { projectId } = state.settings;
export const putRule = ({ rootState, dispatch }, { id, ...newRule }) => {
const { projectId } = rootState.settings;
return Api.putProjectApprovalRule(projectId, id, mapApprovalRuleRequest(newRule))
.then(() => dispatch('postRuleSuccess'))
......@@ -69,10 +61,12 @@ export const deleteRuleError = () => {
createFlash(__('An error occurred while deleting the approvers group'));
};
export const deleteRule = ({ state, dispatch }, id) => {
const { projectId } = state.settings;
export const deleteRule = ({ rootState, dispatch }, id) => {
const { projectId } = rootState.settings;
return Api.deleteProjectApprovalRule(projectId, id)
.then(() => dispatch('deleteRuleSuccess'))
.catch(() => dispatch('deleteRuleError'));
};
export default () => {};
import base from '../base';
import * as actions from './actions';
export default () => ({
...base(),
actions,
});
export default () => ({
settings: {},
isLoading: false,
rules: [],
export const DEFAULT_SETTINGS = {
canEdit: true,
};
export default (settings = {}) => ({
settings: {
...DEFAULT_SETTINGS,
...settings,
},
});
......@@ -5,7 +5,7 @@ import UsersSelect from '~/users_select';
import UserCallout from '~/user_callout';
import groupsSelect from '~/groups_select';
import ApproversSelect from 'ee/approvers_select';
import mountApprovalsSettings from 'ee/approvals';
import mountApprovals from 'ee/approvals/mount_project_settings';
import initServiceDesk from 'ee/projects/settings_service_desk';
document.addEventListener('DOMContentLoaded', () => {
......@@ -16,5 +16,5 @@ document.addEventListener('DOMContentLoaded', () => {
new UserCallout({ className: 'js-mr-approval-callout' });
new ApproversSelect();
initServiceDesk();
mountApprovalsSettings(document.getElementById('js-mr-approvals-settings'));
mountApprovals(document.getElementById('js-mr-approvals-settings'));
});
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment