diff --git a/app/assets/javascripts/ci/pipeline_mini_graph/job_action_button.vue b/app/assets/javascripts/ci/pipeline_mini_graph/job_action_button.vue index 5ed2fc7022b7fd0da90ca7f8692bd4df07b37c0e..a6b083fbb30fbdd2f27379a29bcdfc4b758c84a4 100644 --- a/app/assets/javascripts/ci/pipeline_mini_graph/job_action_button.vue +++ b/app/assets/javascripts/ci/pipeline_mini_graph/job_action_button.vue @@ -7,6 +7,7 @@ import cancelJobMutation from './graphql/mutations/job_cancel.mutation.graphql'; import playJobMutation from './graphql/mutations/job_play.mutation.graphql'; import retryJobMutation from './graphql/mutations/job_retry.mutation.graphql'; import unscheduleJobMutation from './graphql/mutations/job_unschedule.mutation.graphql'; +import JobActionModal from './job_action_modal.vue'; export const i18n = { errors: { @@ -46,6 +47,7 @@ export default { GlButton, GlIcon, GlLoadingIcon, + JobActionModal, }, directives: { GlTooltip: GlTooltipDirective, @@ -74,10 +76,17 @@ export default { actionType() { return this.jobAction.icon; }, + hasConfirmationModal() { + return this.jobAction?.confirmationMessage !== null; + }, }, methods: { onActionButtonClick() { - this.executeJobAction(); + if (this.hasConfirmationModal) { + this.showConfirmationModal = true; + } else { + this.executeJobAction(); + } }, async executeJobAction() { try { @@ -117,5 +126,12 @@ export default { <gl-loading-icon v-if="isLoading" size="sm" class="gl-m-2" /> <gl-icon v-else :name="jobAction.icon" :size="12" /> </gl-button> + <job-action-modal + v-if="hasConfirmationModal" + v-model="showConfirmationModal" + :job-name="jobName" + :custom-message="jobAction.confirmationMessage" + @confirm="executeJobAction" + /> </div> </template> diff --git a/app/assets/javascripts/ci/pipeline_mini_graph/job_action_modal.vue b/app/assets/javascripts/ci/pipeline_mini_graph/job_action_modal.vue new file mode 100644 index 0000000000000000000000000000000000000000..4efcd91e6102017b85748db71f5cb4eb62c7fec7 --- /dev/null +++ b/app/assets/javascripts/ci/pipeline_mini_graph/job_action_modal.vue @@ -0,0 +1,69 @@ +<script> +import { GlModal } from '@gitlab/ui'; +import { s__, __, sprintf } from '~/locale'; + +export default { + name: 'JobActionModal', + i18n: { + title: s__('PipelineGraph|Are you sure you want to run %{jobName}?'), + confirmationText: s__('PipelineGraph|Do you want to continue?'), + actionCancel: { text: __('Cancel') }, + }, + components: { + GlModal, + }, + model: { + prop: 'visible', + event: 'change', + }, + props: { + customMessage: { + type: String, + required: true, + }, + visible: { + type: Boolean, + required: false, + default: false, + }, + jobName: { + type: String, + required: true, + }, + }, + computed: { + modalText() { + return { + confirmButton: { + text: sprintf(__('Yes, run %{jobName}'), { + jobName: this.jobName, + }), + }, + message: sprintf(__('Custom confirmation message: %{message}'), { + message: this.customMessage, + }), + title: sprintf(this.$options.i18n.title, { + jobName: this.jobName, + }), + }; + }, + }, +}; +</script> + +<template> + <gl-modal + modal-id="job-action-modal" + :action-cancel="$options.i18n.actionCancel" + :action-primary="modalText.confirmButton" + :title="modalText.title" + :visible="visible" + @primary="$emit('confirm')" + @change="$emit('change', $event)" + > + <div> + <p>{{ modalText.message }}</p> + <span>{{ $options.i18n.confirmationText }}</span> + </div> + </gl-modal> +</template> diff --git a/spec/frontend/ci/pipeline_mini_graph/job_action_modal_spec.js b/spec/frontend/ci/pipeline_mini_graph/job_action_modal_spec.js new file mode 100644 index 0000000000000000000000000000000000000000..f7464a00ea485afbff4062d9baf3e7626fe8fee3 --- /dev/null +++ b/spec/frontend/ci/pipeline_mini_graph/job_action_modal_spec.js @@ -0,0 +1,58 @@ +import { shallowMount } from '@vue/test-utils'; +import { GlModal, GlSprintf } from '@gitlab/ui'; +import JobActionModal from '~/ci/pipeline_mini_graph/job_action_modal.vue'; + +describe('JobActionModal', () => { + let wrapper; + + const defaultProps = { + customMessage: 'This is a custom message.', + jobName: 'test_job', + }; + + const createComponent = ({ props = {} } = {}) => { + wrapper = shallowMount(JobActionModal, { + propsData: { + ...defaultProps, + ...props, + }, + stubs: { + GlSprintf, + }, + }); + }; + + const findModal = () => wrapper.findComponent(GlModal); + + beforeEach(() => { + createComponent(); + }); + + it('shows modal', () => { + expect(findModal().props()).toMatchObject({ + actionCancel: { text: 'Cancel' }, + actionPrimary: { text: 'Yes, run test_job' }, + modalId: 'job-action-modal', + title: 'Are you sure you want to run test_job?', + }); + }); + + it('displays the custom message', () => { + expect(findModal().text()).toContain(defaultProps.customMessage); + }); + + it('emits change event when modal visibility changes', async () => { + await findModal().vm.$emit('change', true); + expect(wrapper.emitted('change')).toEqual([[true]]); + }); + + it('passes visible prop to gl-modal', () => { + createComponent({ + props: { + visible: true, + }, + }); + + expect(findModal().props('visible')).toBe(true); + }); +});