Commit 244e87d1 authored by Natalia Tepluhina's avatar Natalia Tepluhina

Merge branch '327638-add-status-checks-delete-modal' into 'master'

Create the delete modal for status checks

See merge request gitlab-org/gitlab!61784
parents 286004e7 3d28e372
......@@ -24,7 +24,11 @@ export default {
<gl-button data-testid="edit-btn" @click="$emit('open-update-modal', statusCheck)">
{{ $options.i18n.editButton }}
</gl-button>
<gl-button class="gl-ml-3" data-testid="remove-btn">
<gl-button
class="gl-ml-3"
data-testid="remove-btn"
@click="$emit('open-delete-modal', statusCheck)"
>
{{ $options.i18n.removeButton }}
</gl-button>
</div>
......
<script>
import { GlModal, GlModalDirective, GlSprintf } from '@gitlab/ui';
import { mapActions, mapState } from 'vuex';
import createFlash from '~/flash';
import { __, s__, sprintf } from '~/locale';
const i18n = {
cancelButton: __('Cancel'),
deleteError: s__('StatusCheck|An error occurred deleting the %{name} status check.'),
primaryButton: s__('StatusCheck|Remove status check'),
title: s__('StatusCheck|Remove status check?'),
warningText: s__('StatusCheck|You are about to remove the %{name} status check.'),
};
export default {
components: {
GlModal,
GlSprintf,
},
directives: {
GlModal: GlModalDirective,
},
props: {
statusCheck: {
type: Object,
required: true,
},
},
data() {
return {
submitting: false,
};
},
computed: {
...mapState({
projectId: ({ settings }) => settings.projectId,
}),
primaryActionProps() {
return {
text: i18n.primaryButton,
attributes: [{ variant: 'danger', loading: this.submitting }],
};
},
},
methods: {
...mapActions(['deleteStatusCheck']),
async submit() {
const { id, name } = this.statusCheck;
this.submitting = true;
try {
await this.deleteStatusCheck(id);
} catch (error) {
createFlash({
message: sprintf(i18n.deleteError, { name }),
captureError: true,
error,
});
}
this.submitting = false;
this.$refs.modal.hide();
},
show() {
this.$refs.modal.show();
},
},
modalId: 'status-checks-delete-modal',
cancelActionProps: {
text: i18n.cancelButton,
},
i18n,
};
</script>
<template>
<gl-modal
ref="modal"
:modal-id="$options.modalId"
:title="$options.i18n.title"
:action-primary="primaryActionProps"
:action-cancel="$options.cancelActionProps"
size="sm"
@ok.prevent="submit"
>
<gl-sprintf :message="$options.i18n.warningText">
<template #name>
<strong>{{ statusCheck.name }}</strong>
</template>
</gl-sprintf>
</gl-modal>
</template>
......@@ -8,6 +8,7 @@ import { EMPTY_STATUS_CHECK } from '../constants';
import Actions from './actions.vue';
import Branch from './branch.vue';
import ModalCreate from './modal_create.vue';
import ModalDelete from './modal_delete.vue';
import ModalUpdate from './modal_update.vue';
export const i18n = {
......@@ -23,10 +24,12 @@ export default {
Branch,
GlTable,
ModalCreate,
ModalDelete,
ModalUpdate,
},
data() {
return {
statusCheckToDelete: EMPTY_STATUS_CHECK,
statusCheckToUpdate: EMPTY_STATUS_CHECK,
};
},
......@@ -34,6 +37,10 @@ export default {
...mapState(['statusChecks']),
},
methods: {
openDeleteModal(statusCheck) {
this.statusCheckToDelete = statusCheck;
this.$refs.deleteModal.show();
},
openUpdateModal(statusCheck) {
this.statusCheckToUpdate = statusCheck;
this.$refs.updateModal.show();
......@@ -80,11 +87,16 @@ export default {
<branch :branches="item.protectedBranches" />
</template>
<template #cell(actions)="{ item }">
<actions :status-check="item" @open-update-modal="openUpdateModal" />
<actions
:status-check="item"
@open-delete-modal="openDeleteModal"
@open-update-modal="openUpdateModal"
/>
</template>
</gl-table>
<modal-create />
<modal-delete ref="deleteModal" :status-check="statusCheckToDelete" />
<modal-update ref="updateModal" :status-check="statusCheckToUpdate" />
</div>
</template>
......@@ -35,3 +35,9 @@ export const postStatusCheck = ({ dispatch, rootState }, statusCheck) => {
return axios.post(statusChecksPath, data).then(() => dispatch('fetchStatusChecks'));
};
export const deleteStatusCheck = ({ rootState, dispatch }, id) => {
const { statusChecksPath } = rootState.settings;
return axios.delete(`${statusChecksPath}/${id}`).then(() => dispatch('fetchStatusChecks'));
};
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Modal delete Modal the modal text matches the snapshot 1`] = `
<gl-modal-stub
actioncancel="[object Object]"
actionprimary="[object Object]"
dismisslabel="Close"
modalclass=""
modalid="status-checks-delete-modal"
size="sm"
title="Remove status check?"
titletag="h4"
>
You are about to remove the
<strong>
Foo
</strong>
status check.
</gl-modal-stub>
`;
......@@ -34,19 +34,19 @@ describe('Status checks actions', () => {
const findEditBtn = () => wrapper.findByTestId('edit-btn');
const findRemoveBtn = () => wrapper.findByTestId('remove-btn');
describe('Edit button', () => {
it('renders the edit button', () => {
expect(findEditBtn().text()).toBe('Edit');
describe.each`
text | button | event
${'Edit'} | ${findEditBtn} | ${'open-update-modal'}
${'Remove...'} | ${findRemoveBtn} | ${'open-delete-modal'}
`('$text button', ({ text, button, event }) => {
it(`renders the button text as '${text}'`, () => {
expect(button().text()).toBe(text);
});
it('sends the status check to the update event', () => {
findEditBtn().trigger('click');
it(`sends the status check with the '${event}' event`, () => {
button().trigger('click');
expect(wrapper.emitted('open-update-modal')[0][0]).toStrictEqual(statusCheck);
expect(wrapper.emitted(event)[0][0]).toStrictEqual(statusCheck);
});
});
it('renders the remove button', () => {
expect(findRemoveBtn().text()).toBe('Remove...');
});
});
import { GlModal, GlSprintf } from '@gitlab/ui';
import Vue from 'vue';
import Vuex from 'vuex';
import ModalDelete from 'ee/status_checks/components/modal_delete.vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import createFlash from '~/flash';
jest.mock('~/flash');
Vue.use(Vuex);
const projectId = '1';
const statusChecksPath = '/api/v4/projects/1/external_approval_rules';
const statusCheck = {
externalUrl: 'https://foo.com',
id: 1,
name: 'Foo',
protectedBranches: [],
};
const modalId = 'status-checks-delete-modal';
describe('Modal delete', () => {
let wrapper;
let store;
const glModalDirective = jest.fn();
const actions = {
deleteStatusCheck: jest.fn(),
};
const createWrapper = () => {
store = new Vuex.Store({
actions,
state: {
isLoading: false,
settings: { projectId, statusChecksPath },
statusChecks: [],
},
});
wrapper = shallowMountExtended(ModalDelete, {
directives: {
glModal: {
bind(el, { modifiers }) {
glModalDirective(modifiers);
},
},
},
propsData: {
statusCheck,
},
store,
stubs: { GlSprintf },
});
wrapper.vm.$refs.modal.hide = jest.fn();
};
beforeEach(() => {
createWrapper();
});
afterEach(() => {
wrapper.destroy();
});
const findModal = () => wrapper.findComponent(GlModal);
const clickModalOk = async () => {
await findModal().vm.$emit('ok', { preventDefault: () => null });
return waitForPromises();
};
describe('Modal', () => {
it('sets the modals props', () => {
expect(findModal().props()).toMatchObject({
actionPrimary: {
text: 'Remove status check',
attributes: [{ variant: 'danger', loading: false }],
},
actionCancel: { text: 'Cancel' },
modalId,
size: 'sm',
title: 'Remove status check?',
});
});
it('the modal text matches the snapshot', () => {
expect(wrapper.element).toMatchSnapshot();
});
});
describe('Submission', () => {
it('submits and hides the modal', async () => {
await clickModalOk();
expect(actions.deleteStatusCheck).toHaveBeenCalledWith(expect.any(Object), statusCheck.id);
expect(wrapper.vm.$refs.modal.hide).toHaveBeenCalled();
});
it('submits, hides the modal and shows the error', async () => {
const error = new Error('Something went wrong');
actions.deleteStatusCheck.mockRejectedValueOnce(error);
await clickModalOk();
expect(actions.deleteStatusCheck).toHaveBeenCalledWith(expect.any(Object), statusCheck.id);
expect(wrapper.vm.$refs.modal.hide).toHaveBeenCalled();
expect(createFlash).toHaveBeenCalledWith({
message: 'An error occurred deleting the Foo status check.',
captureError: true,
error,
});
});
});
});
......@@ -5,6 +5,7 @@ import Vuex from 'vuex';
import Actions from 'ee/status_checks/components/actions.vue';
import Branch from 'ee/status_checks/components/branch.vue';
import ModalCreate from 'ee/status_checks/components/modal_create.vue';
import ModalDelete from 'ee/status_checks/components/modal_delete.vue';
import ModalUpdate from 'ee/status_checks/components/modal_update.vue';
import StatusChecks, { i18n } from 'ee/status_checks/components/status_checks.vue';
import createStore from 'ee/status_checks/store';
......@@ -25,6 +26,7 @@ describe('Status checks', () => {
store = createStore();
wrapper = mountFn(StatusChecks, { store });
wrapper.vm.$refs.deleteModal.show = jest.fn();
wrapper.vm.$refs.updateModal.show = jest.fn();
};
......@@ -33,6 +35,7 @@ describe('Status checks', () => {
});
const findCreateModal = () => wrapper.findComponent(ModalCreate);
const findDeleteModal = () => wrapper.findComponent(ModalDelete);
const findUpdateModal = () => wrapper.findComponent(ModalUpdate);
const findTable = () => wrapper.findComponent(GlTable);
const findHeaders = () => findTable().find('thead').find('tr').findAll('th');
......@@ -103,6 +106,31 @@ describe('Status checks', () => {
});
});
describe('Delete modal filling', () => {
beforeEach(() => {
createWrapper();
store.commit(SET_STATUS_CHECKS, statusChecks);
});
it('opens the delete modal with the correct status check when a remove button is clicked', async () => {
const statusCheck = findActions(0, 1).props('statusCheck');
await findActions(0, 1).vm.$emit('open-delete-modal', statusCheck);
expect(findDeleteModal().props('statusCheck')).toStrictEqual(statusCheck);
expect(wrapper.vm.$refs.deleteModal.show).toHaveBeenCalled();
});
it('updates the status check prop for the delete modal when another remove button is clicked', async () => {
const statusCheck = findActions(1, 1).props('statusCheck');
await findActions(0, 1).vm.$emit('open-delete-modal', findActions(0, 1).props('statusCheck'));
await findActions(1, 1).vm.$emit('open-delete-modal', statusCheck);
expect(findDeleteModal().props('statusCheck')).toStrictEqual(statusCheck);
});
});
describe('Update modal filling', () => {
beforeEach(() => {
createWrapper();
......
......@@ -79,4 +79,16 @@ describe('Status checks actions', () => {
},
);
});
describe('deleteStatusCheck', () => {
it(`should DELETE call the API and then dispatch a new fetchStatusChecks`, async () => {
const id = 1;
mockAxios.onPost(statusChecksPath).replyOnce(httpStatusCodes.OK);
await actions.postStatusCheck({ dispatch, rootState }, id);
expect(dispatch).toHaveBeenCalledWith('fetchStatusChecks');
});
});
});
......@@ -31066,6 +31066,9 @@ msgstr ""
msgid "StatusCheck|Add status check"
msgstr ""
msgid "StatusCheck|An error occurred deleting the %{name} status check."
msgstr ""
msgid "StatusCheck|An error occurred fetching the status checks."
msgstr ""
......
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