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 { ...@@ -24,7 +24,11 @@ export default {
<gl-button data-testid="edit-btn" @click="$emit('open-update-modal', statusCheck)"> <gl-button data-testid="edit-btn" @click="$emit('open-update-modal', statusCheck)">
{{ $options.i18n.editButton }} {{ $options.i18n.editButton }}
</gl-button> </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 }} {{ $options.i18n.removeButton }}
</gl-button> </gl-button>
</div> </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'; ...@@ -8,6 +8,7 @@ import { EMPTY_STATUS_CHECK } from '../constants';
import Actions from './actions.vue'; import Actions from './actions.vue';
import Branch from './branch.vue'; import Branch from './branch.vue';
import ModalCreate from './modal_create.vue'; import ModalCreate from './modal_create.vue';
import ModalDelete from './modal_delete.vue';
import ModalUpdate from './modal_update.vue'; import ModalUpdate from './modal_update.vue';
export const i18n = { export const i18n = {
...@@ -23,10 +24,12 @@ export default { ...@@ -23,10 +24,12 @@ export default {
Branch, Branch,
GlTable, GlTable,
ModalCreate, ModalCreate,
ModalDelete,
ModalUpdate, ModalUpdate,
}, },
data() { data() {
return { return {
statusCheckToDelete: EMPTY_STATUS_CHECK,
statusCheckToUpdate: EMPTY_STATUS_CHECK, statusCheckToUpdate: EMPTY_STATUS_CHECK,
}; };
}, },
...@@ -34,6 +37,10 @@ export default { ...@@ -34,6 +37,10 @@ export default {
...mapState(['statusChecks']), ...mapState(['statusChecks']),
}, },
methods: { methods: {
openDeleteModal(statusCheck) {
this.statusCheckToDelete = statusCheck;
this.$refs.deleteModal.show();
},
openUpdateModal(statusCheck) { openUpdateModal(statusCheck) {
this.statusCheckToUpdate = statusCheck; this.statusCheckToUpdate = statusCheck;
this.$refs.updateModal.show(); this.$refs.updateModal.show();
...@@ -80,11 +87,16 @@ export default { ...@@ -80,11 +87,16 @@ export default {
<branch :branches="item.protectedBranches" /> <branch :branches="item.protectedBranches" />
</template> </template>
<template #cell(actions)="{ item }"> <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> </template>
</gl-table> </gl-table>
<modal-create /> <modal-create />
<modal-delete ref="deleteModal" :status-check="statusCheckToDelete" />
<modal-update ref="updateModal" :status-check="statusCheckToUpdate" /> <modal-update ref="updateModal" :status-check="statusCheckToUpdate" />
</div> </div>
</template> </template>
...@@ -35,3 +35,9 @@ export const postStatusCheck = ({ dispatch, rootState }, statusCheck) => { ...@@ -35,3 +35,9 @@ export const postStatusCheck = ({ dispatch, rootState }, statusCheck) => {
return axios.post(statusChecksPath, data).then(() => dispatch('fetchStatusChecks')); 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', () => { ...@@ -34,19 +34,19 @@ describe('Status checks actions', () => {
const findEditBtn = () => wrapper.findByTestId('edit-btn'); const findEditBtn = () => wrapper.findByTestId('edit-btn');
const findRemoveBtn = () => wrapper.findByTestId('remove-btn'); const findRemoveBtn = () => wrapper.findByTestId('remove-btn');
describe('Edit button', () => { describe.each`
it('renders the edit button', () => { text | button | event
expect(findEditBtn().text()).toBe('Edit'); ${'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', () => { it(`sends the status check with the '${event}' event`, () => {
findEditBtn().trigger('click'); 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'; ...@@ -5,6 +5,7 @@ import Vuex from 'vuex';
import Actions from 'ee/status_checks/components/actions.vue'; import Actions from 'ee/status_checks/components/actions.vue';
import Branch from 'ee/status_checks/components/branch.vue'; import Branch from 'ee/status_checks/components/branch.vue';
import ModalCreate from 'ee/status_checks/components/modal_create.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 ModalUpdate from 'ee/status_checks/components/modal_update.vue';
import StatusChecks, { i18n } from 'ee/status_checks/components/status_checks.vue'; import StatusChecks, { i18n } from 'ee/status_checks/components/status_checks.vue';
import createStore from 'ee/status_checks/store'; import createStore from 'ee/status_checks/store';
...@@ -25,6 +26,7 @@ describe('Status checks', () => { ...@@ -25,6 +26,7 @@ describe('Status checks', () => {
store = createStore(); store = createStore();
wrapper = mountFn(StatusChecks, { store }); wrapper = mountFn(StatusChecks, { store });
wrapper.vm.$refs.deleteModal.show = jest.fn();
wrapper.vm.$refs.updateModal.show = jest.fn(); wrapper.vm.$refs.updateModal.show = jest.fn();
}; };
...@@ -33,6 +35,7 @@ describe('Status checks', () => { ...@@ -33,6 +35,7 @@ describe('Status checks', () => {
}); });
const findCreateModal = () => wrapper.findComponent(ModalCreate); const findCreateModal = () => wrapper.findComponent(ModalCreate);
const findDeleteModal = () => wrapper.findComponent(ModalDelete);
const findUpdateModal = () => wrapper.findComponent(ModalUpdate); const findUpdateModal = () => wrapper.findComponent(ModalUpdate);
const findTable = () => wrapper.findComponent(GlTable); const findTable = () => wrapper.findComponent(GlTable);
const findHeaders = () => findTable().find('thead').find('tr').findAll('th'); const findHeaders = () => findTable().find('thead').find('tr').findAll('th');
...@@ -103,6 +106,31 @@ describe('Status checks', () => { ...@@ -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', () => { describe('Update modal filling', () => {
beforeEach(() => { beforeEach(() => {
createWrapper(); createWrapper();
......
...@@ -79,4 +79,16 @@ describe('Status checks actions', () => { ...@@ -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 "" ...@@ -31066,6 +31066,9 @@ msgstr ""
msgid "StatusCheck|Add status check" msgid "StatusCheck|Add status check"
msgstr "" msgstr ""
msgid "StatusCheck|An error occurred deleting the %{name} status check."
msgstr ""
msgid "StatusCheck|An error occurred fetching the status checks." msgid "StatusCheck|An error occurred fetching the status checks."
msgstr "" 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