diff --git a/app/assets/javascripts/vue_merge_request_widget/components/checks/constants.js b/app/assets/javascripts/vue_merge_request_widget/components/checks/constants.js
index a8d2736531a515a2d3ea64259cb9ce44a6b08f9e..24bc7017e060b9323d09ad14231f1240d81923c0 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/checks/constants.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/checks/constants.js
@@ -1,6 +1,7 @@
export const COMPONENTS = {
conflict: () => import('./conflicts.vue'),
discussions_not_resolved: () => import('./unresolved_discussions.vue'),
+ draft_status: () => import('./draft.vue'),
need_rebase: () => import('./rebase.vue'),
default: () => import('./message.vue'),
};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/checks/draft.stories.js b/app/assets/javascripts/vue_merge_request_widget/components/checks/draft.stories.js
new file mode 100644
index 0000000000000000000000000000000000000000..537c975652f7ccea4552644bb70e0bc1bff11fc5
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/checks/draft.stories.js
@@ -0,0 +1,74 @@
+import createMockApollo from 'helpers/mock_apollo_helper';
+import draftStateQuery from '../../queries/states/draft.query.graphql';
+import removeDraftMutation from '../../queries/toggle_draft.mutation.graphql';
+import Draft from './draft.vue';
+
+const defaultRender = ({ apolloProvider, check, mr }) => ({
+ components: { Draft },
+ apolloProvider,
+ data() {
+ return { mr, check };
+ },
+ template: '',
+});
+
+const Template = ({ userPermissionUpdateMergeRequest }) => {
+ const requestHandlers = [
+ [
+ draftStateQuery,
+ () =>
+ Promise.resolve({
+ data: {
+ project: {
+ id: '1',
+ mergeRequest: {
+ draft: false,
+ id: '2',
+ title: 'MR title',
+ mergeableDiscussionsState: true,
+ userPermissions: {
+ updateMergeRequest: userPermissionUpdateMergeRequest,
+ },
+ },
+ },
+ },
+ }),
+ ],
+ [
+ removeDraftMutation,
+ () =>
+ Promise.resolve({
+ data: {
+ mergeRequestSetDraft: {
+ mergeRequest: {
+ draft: false,
+ id: '2',
+ title: 'MR title',
+ mergeableDiscussionsState: true,
+ },
+ errors: [],
+ },
+ },
+ }),
+ ],
+ ];
+ const apolloProvider = createMockApollo(requestHandlers);
+
+ return defaultRender({
+ apolloProvider,
+ check: {
+ identifier: 'draft_status',
+ status: 'FAILED',
+ },
+ });
+};
+
+export const Default = Template.bind({});
+Default.args = {
+ userPermissionUpdateMergeRequest: true,
+};
+
+export default {
+ title: 'vue_merge_request_widget/merge_checks/draft',
+ component: Draft,
+};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/checks/draft.vue b/app/assets/javascripts/vue_merge_request_widget/components/checks/draft.vue
new file mode 100644
index 0000000000000000000000000000000000000000..dbe0d2ac243b52586ec219048166fc0373ea25f5
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/checks/draft.vue
@@ -0,0 +1,169 @@
+
+
+
+
+
+
+
+
+
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/checks/i18n.js b/app/assets/javascripts/vue_merge_request_widget/components/checks/i18n.js
new file mode 100644
index 0000000000000000000000000000000000000000..de504af5fccdf37bd772e2ea78af3d4444a88bcf
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/checks/i18n.js
@@ -0,0 +1,4 @@
+import { __, s__ } from '~/locale';
+
+export const DRAFT_CHECK_ERROR = __('Something went wrong. Please try again.');
+export const DRAFT_CHECK_READY = s__('mrWidgetDraftCheck|Mark as ready');
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/checks/message.vue b/app/assets/javascripts/vue_merge_request_widget/components/checks/message.vue
index e91c76e7ff07a7f70c0d3365e9062bbf2c616bab..7f21445559a0ee30baf22917d683519505474617 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/checks/message.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/checks/message.vue
@@ -8,7 +8,7 @@ const ICON_NAMES = {
success: 'success',
};
-const FAILURE_REASONS = {
+export const FAILURE_REASONS = {
broken_status: __('Cannot merge the source into the target branch, due to a conflict.'),
ci_must_pass: __('Pipeline must succeed.'),
conflict: __('Merge conflicts must be resolved.'),
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/merge_checks.stories.js b/app/assets/javascripts/vue_merge_request_widget/components/merge_checks.stories.js
index 77dc5b1d0daaa5a64ab81a07c40d63c1b1ee48a4..a1171fe5d256e368c4a790749a12164a82c23b61 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/merge_checks.stories.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/merge_checks.stories.js
@@ -41,6 +41,10 @@ const Template = ({ canMerge, failed, pushToSourceBranch }) => {
identifier: 'CONFLICT',
status: failed ? 'FAILED' : 'SUCCESS',
},
+ {
+ identifier: 'DRAFT_STATUS',
+ status: failed ? 'FAILED' : 'SUCCESS',
+ },
],
},
},
diff --git a/app/assets/javascripts/vue_merge_request_widget/queries/states/draft.query.graphql b/app/assets/javascripts/vue_merge_request_widget/queries/states/draft.query.graphql
index 54f2233439f55f34ef20d7c95b6ef3012d87f6ab..c1190a07ef84a3b23bd788c1379545eccb6fcd4f 100644
--- a/app/assets/javascripts/vue_merge_request_widget/queries/states/draft.query.graphql
+++ b/app/assets/javascripts/vue_merge_request_widget/queries/states/draft.query.graphql
@@ -2,7 +2,10 @@ query mrUserPermission($projectPath: ID!, $iid: String!) {
project(fullPath: $projectPath) {
id
mergeRequest(iid: $iid) {
+ draft
id
+ mergeableDiscussionsState
+ title
userPermissions {
updateMergeRequest
}
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 6872275f38490b22caa6f390e142aeda9baa683c..a364af32443d07a8ca169a6d67e3c6ace0a2dd63 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -57666,6 +57666,9 @@ msgstr ""
msgid "mrWidgetCommitsAdded|The changes were not merged into %{targetBranch}."
msgstr ""
+msgid "mrWidgetDraftCheck|Mark as ready"
+msgstr ""
+
msgid "mrWidgetNothingToMerge|Merge request contains no changes"
msgstr ""
diff --git a/spec/frontend/vue_merge_request_widget/components/checks/draft_spec.js b/spec/frontend/vue_merge_request_widget/components/checks/draft_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..cc605c8c83deecbf265baa0720bdc3c6f01b77b7
--- /dev/null
+++ b/spec/frontend/vue_merge_request_widget/components/checks/draft_spec.js
@@ -0,0 +1,196 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+
+import getStateQueryResponse from 'test_fixtures/graphql/merge_requests/get_state.query.graphql.json';
+
+import { createAlert } from '~/alert';
+
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+
+import MergeRequest from '~/merge_request';
+
+import DraftCheck from '~/vue_merge_request_widget/components/checks/draft.vue';
+import {
+ DRAFT_CHECK_READY,
+ DRAFT_CHECK_ERROR,
+} from '~/vue_merge_request_widget/components/checks/i18n';
+import { FAILURE_REASONS } from '~/vue_merge_request_widget/components/checks/message.vue';
+
+import draftQuery from '~/vue_merge_request_widget/queries/states/draft.query.graphql';
+import getStateQuery from '~/vue_merge_request_widget/queries/get_state.query.graphql';
+import removeDraftMutation from '~/vue_merge_request_widget/queries/toggle_draft.mutation.graphql';
+
+Vue.use(VueApollo);
+
+const TEST_PROJECT_ID = getStateQueryResponse.data.project.id;
+const TEST_MR_ID = getStateQueryResponse.data.project.mergeRequest.id;
+const TEST_MR_IID = '23';
+const TEST_MR_TITLE = 'Test MR Title';
+const TEST_PROJECT_PATH = 'lorem/ipsum';
+
+jest.mock('~/alert');
+jest.mock('~/merge_request', () => ({ toggleDraftStatus: jest.fn() }));
+
+describe('~/vue_merge_request_widget/components/checks/draft.vue', () => {
+ let wrapper;
+ let apolloProvider;
+
+ let draftQuerySpy;
+ let removeDraftMutationSpy;
+
+ const findMarkReadyButton = () => wrapper.findByTestId('mark-as-ready-button');
+
+ const createDraftQueryResponse = (canUpdateMergeRequest) => ({
+ data: {
+ project: {
+ __typename: 'Project',
+ id: TEST_PROJECT_ID,
+ mergeRequest: {
+ __typename: 'MergeRequest',
+ id: TEST_MR_ID,
+ draft: true,
+ title: TEST_MR_TITLE,
+ mergeableDiscussionsState: false,
+ userPermissions: {
+ updateMergeRequest: canUpdateMergeRequest,
+ },
+ },
+ },
+ },
+ });
+ const createRemoveDraftMutationResponse = () => ({
+ data: {
+ mergeRequestSetDraft: {
+ __typename: 'MergeRequestSetWipPayload',
+ errors: [],
+ mergeRequest: {
+ __typename: 'MergeRequest',
+ id: TEST_MR_ID,
+ title: TEST_MR_TITLE,
+ draft: false,
+ mergeableDiscussionsState: true,
+ },
+ },
+ },
+ });
+
+ const createComponent = async () => {
+ wrapper = mountExtended(DraftCheck, {
+ apolloProvider,
+ propsData: {
+ mr: {
+ issuableId: TEST_MR_ID,
+ title: TEST_MR_TITLE,
+ iid: TEST_MR_IID,
+ targetProjectFullPath: TEST_PROJECT_PATH,
+ },
+ check: {
+ identifier: 'draft_status',
+ status: 'FAILED',
+ },
+ },
+ });
+
+ await waitForPromises();
+
+ // why: draft.vue has some coupling that this query has been read before
+ // for some reason this has to happen **after** the component has mounted
+ // or apollo throws errors.
+ apolloProvider.defaultClient.cache.writeQuery({
+ query: getStateQuery,
+ variables: {
+ projectPath: TEST_PROJECT_PATH,
+ iid: TEST_MR_IID,
+ },
+ data: getStateQueryResponse.data,
+ });
+ };
+
+ beforeEach(() => {
+ draftQuerySpy = jest.fn().mockResolvedValue(createDraftQueryResponse(true));
+ removeDraftMutationSpy = jest.fn().mockResolvedValue(createRemoveDraftMutationResponse());
+
+ apolloProvider = createMockApollo([
+ [draftQuery, draftQuerySpy],
+ [removeDraftMutation, removeDraftMutationSpy],
+ ]);
+ });
+
+ describe('when user can update MR', () => {
+ beforeEach(async () => {
+ await createComponent();
+ });
+
+ it('renders text', () => {
+ const message = wrapper.text();
+ expect(message).toContain(FAILURE_REASONS.draft_status);
+ });
+
+ it('renders mark ready button', () => {
+ expect(findMarkReadyButton().text()).toBe(DRAFT_CHECK_READY);
+ });
+
+ it('does not call remove draft mutation', () => {
+ expect(removeDraftMutationSpy).not.toHaveBeenCalled();
+ });
+
+ describe('when mark ready button is clicked', () => {
+ beforeEach(async () => {
+ findMarkReadyButton().vm.$emit('click');
+
+ await waitForPromises();
+ });
+
+ it('calls mutation spy', () => {
+ expect(removeDraftMutationSpy).toHaveBeenCalledWith({
+ draft: false,
+ iid: TEST_MR_IID,
+ projectPath: TEST_PROJECT_PATH,
+ });
+ });
+
+ it('does not create alert', () => {
+ expect(createAlert).not.toHaveBeenCalled();
+ });
+
+ it('calls toggleDraftStatus', () => {
+ expect(MergeRequest.toggleDraftStatus).toHaveBeenCalledWith(TEST_MR_TITLE, true);
+ });
+ });
+
+ describe('when mutation fails and ready button is clicked', () => {
+ beforeEach(async () => {
+ removeDraftMutationSpy.mockRejectedValue(new Error('TEST FAIL'));
+ findMarkReadyButton().vm.$emit('click');
+
+ await waitForPromises();
+ });
+
+ it('creates alert', () => {
+ expect(createAlert).toHaveBeenCalledWith({
+ message: DRAFT_CHECK_ERROR,
+ });
+ });
+
+ it('does not call toggleDraftStatus', () => {
+ expect(MergeRequest.toggleDraftStatus).not.toHaveBeenCalled();
+ });
+ });
+ });
+
+ describe('when user cannot update MR', () => {
+ beforeEach(async () => {
+ draftQuerySpy.mockResolvedValue(createDraftQueryResponse(false));
+
+ createComponent();
+
+ await waitForPromises();
+ });
+
+ it('does not render mark ready button', () => {
+ expect(findMarkReadyButton().exists()).toBe(false);
+ });
+ });
+});
diff --git a/spec/frontend/vue_merge_request_widget/components/states/work_in_progress_spec.js b/spec/frontend/vue_merge_request_widget/components/states/work_in_progress_spec.js
index f46829539a8f7754b68d1218f9cb9c2c5b2ab667..509b5adb3b23053a7abc2347695f355b50db7e7b 100644
--- a/spec/frontend/vue_merge_request_widget/components/states/work_in_progress_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/states/work_in_progress_spec.js
@@ -42,6 +42,9 @@ describe('~/vue_merge_request_widget/components/states/work_in_progress.vue', ()
mergeRequest: {
__typename: 'MergeRequest',
id: TEST_MR_ID,
+ draft: true,
+ title: TEST_MR_TITLE,
+ mergeableDiscussionsState: false,
userPermissions: {
updateMergeRequest: canUpdateMergeRequest,
},