Commit 44926ce2 authored by Phil Hughes's avatar Phil Hughes Committed by Igor Drozdov

Moves the auto merge state to GraphQL

Moves the auto merge failed and enabled states into
GraphQL for its state data.

Closes https://gitlab.com/gitlab-org/gitlab/-/issues/235715
parent d65e7145
<script>
import { GlLoadingIcon } from '@gitlab/ui';
import { GlLoadingIcon, GlSkeletonLoader } from '@gitlab/ui';
import autoMergeMixin from 'ee_else_ce/vue_merge_request_widget/mixins/auto_merge';
import autoMergeEnabledQuery from 'ee_else_ce/vue_merge_request_widget/queries/states/auto_merge_enabled.query.graphql';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { deprecatedCreateFlash as Flash } from '../../../flash';
import statusIcon from '../mr_widget_status_icon.vue';
import MrWidgetAuthor from '../mr_widget_author.vue';
import eventHub from '../../event_hub';
import { AUTO_MERGE_STRATEGIES } from '../../constants';
import { __ } from '~/locale';
import mergeRequestQueryVariablesMixin from '../../mixins/merge_request_query_variables';
export default {
name: 'MRWidgetAutoMergeEnabled',
apollo: {
state: {
query: autoMergeEnabledQuery,
skip() {
return !this.glFeatures.mergeRequestWidgetGraphql;
},
variables() {
return this.mergeRequestQueryVariables;
},
update: (data) => data.project?.mergeRequest,
},
},
components: {
MrWidgetAuthor,
statusIcon,
GlLoadingIcon,
GlSkeletonLoader,
},
mixins: [autoMergeMixin],
mixins: [autoMergeMixin, glFeatureFlagMixin(), mergeRequestQueryVariablesMixin],
props: {
mr: {
type: Object,
......@@ -30,20 +46,47 @@ export default {
},
data() {
return {
state: {},
isCancellingAutoMerge: false,
isRemovingSourceBranch: false,
};
},
computed: {
loading() {
return this.glFeatures.mergeRequestWidgetGraphql && this.$apollo.queries.state.loading;
},
mergeUser() {
if (this.glFeatures.mergeRequestWidgetGraphql) {
return this.state.mergeUser;
}
return this.mr.setToAutoMergeBy;
},
targetBranch() {
return (this.glFeatures.mergeRequestWidgetGraphql ? this.state : this.mr).targetBranch;
},
shouldRemoveSourceBranch() {
if (this.glFeatures.mergeRequestWidgetGraphql) {
return this.state.shouldRemoveSourceBranch || this.state.forceRemoveSourceBranch;
}
return this.mr.shouldRemoveSourceBranch;
},
autoMergeStrategy() {
return (this.glFeatures.mergeRequestWidgetGraphql ? this.state : this.mr).autoMergeStrategy;
},
canRemoveSourceBranch() {
const {
shouldRemoveSourceBranch,
canRemoveSourceBranch,
mergeUserId,
currentUserId,
} = this.mr;
const { currentUserId } = this.mr;
const mergeUserId = this.glFeatures.mergeRequestWidgetGraphql
? this.state.mergeUser?.id
: this.mr.mergeUserId;
const canRemoveSourceBranch = this.glFeatures.mergeRequestWidgetGraphql
? this.state.userPermissions.removeSourceBranch
: this.mr.canRemoveSourceBranch;
return !shouldRemoveSourceBranch && canRemoveSourceBranch && mergeUserId === currentUserId;
return (
!this.shouldRemoveSourceBranch && canRemoveSourceBranch && mergeUserId === currentUserId
);
},
},
methods: {
......@@ -63,7 +106,7 @@ export default {
removeSourceBranch() {
const options = {
sha: this.mr.sha,
auto_merge_strategy: this.mr.autoMergeStrategy,
auto_merge_strategy: this.autoMergeStrategy,
should_remove_source_branch: true,
};
......@@ -86,49 +129,64 @@ export default {
</script>
<template>
<div class="mr-widget-body media">
<status-icon status="success" />
<div class="media-body">
<h4 class="d-flex align-items-start">
<span class="gl-mr-3">
<span class="js-status-text-before-author">{{ statusTextBeforeAuthor }}</span>
<mr-widget-author :author="mr.setToAutoMergeBy" />
<span class="js-status-text-after-author">{{ statusTextAfterAuthor }}</span>
</span>
<a
v-if="mr.canCancelAutomaticMerge"
:disabled="isCancellingAutoMerge"
role="button"
href="#"
class="btn btn-sm btn-default js-cancel-auto-merge"
@click.prevent="cancelAutomaticMerge"
>
<gl-loading-icon v-if="isCancellingAutoMerge" inline class="gl-mr-1" />
{{ cancelButtonText }}
</a>
</h4>
<section class="mr-info-list">
<p>
{{ s__('mrWidget|The changes will be merged into') }}
<a :href="mr.targetBranchPath" class="label-branch">{{ mr.targetBranch }}</a>
</p>
<p v-if="mr.shouldRemoveSourceBranch">
{{ s__('mrWidget|The source branch will be deleted') }}
</p>
<p v-else class="d-flex align-items-start">
<span class="gl-mr-3">{{ s__('mrWidget|The source branch will not be deleted') }}</span>
<div v-if="loading" class="gl-w-full mr-conflict-loader">
<gl-skeleton-loader :width="334" :height="30">
<rect x="0" y="3" width="24" height="24" rx="4" />
<rect x="32" y="7" width="150" height="16" rx="4" />
<rect x="190" y="7" width="144" height="16" rx="4" />
</gl-skeleton-loader>
</div>
<template v-else>
<status-icon status="success" />
<div class="media-body">
<h4 class="gl-display-flex">
<span class="gl-mr-3">
<span class="js-status-text-before-author" data-testid="beforeStatusText">{{
statusTextBeforeAuthor
}}</span>
<mr-widget-author :author="mergeUser" />
<span class="js-status-text-after-author" data-testid="afterStatusText">{{
statusTextAfterAuthor
}}</span>
</span>
<a
v-if="canRemoveSourceBranch"
:disabled="isRemovingSourceBranch"
v-if="mr.canCancelAutomaticMerge"
:disabled="isCancellingAutoMerge"
role="button"
class="btn btn-sm btn-default js-remove-source-branch"
href="#"
@click.prevent="removeSourceBranch"
class="btn btn-sm btn-default js-cancel-auto-merge"
data-testid="cancelAutomaticMergeButton"
@click.prevent="cancelAutomaticMerge"
>
<gl-loading-icon v-if="isRemovingSourceBranch" inline class="gl-mr-1" />
{{ s__('mrWidget|Delete source branch') }}
<gl-loading-icon v-if="isCancellingAutoMerge" inline class="gl-mr-1" />
{{ cancelButtonText }}
</a>
</p>
</section>
</div>
</h4>
<section class="mr-info-list">
<p>
{{ s__('mrWidget|The changes will be merged into') }}
<a :href="mr.targetBranchPath" class="label-branch">{{ targetBranch }}</a>
</p>
<p v-if="shouldRemoveSourceBranch">
{{ s__('mrWidget|The source branch will be deleted') }}
</p>
<p v-else class="gl-display-flex">
<span class="gl-mr-3">{{ s__('mrWidget|The source branch will not be deleted') }}</span>
<a
v-if="canRemoveSourceBranch"
:disabled="isRemovingSourceBranch"
role="button"
class="btn btn-sm btn-default js-remove-source-branch"
href="#"
data-testid="removeSourceBranchButton"
@click.prevent="removeSourceBranch"
>
<gl-loading-icon v-if="isRemovingSourceBranch" inline class="gl-mr-1" />
{{ s__('mrWidget|Delete source branch') }}
</a>
</p>
</section>
</div>
</template>
</div>
</template>
<script>
import { GlLoadingIcon, GlButton } from '@gitlab/ui';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import eventHub from '../../event_hub';
import statusIcon from '../mr_widget_status_icon.vue';
import autoMergeFailedQuery from '../../queries/states/auto_merge_failed.query.graphql';
import mergeRequestQueryVariablesMixin from '../../mixins/merge_request_query_variables';
export default {
name: 'MRWidgetAutoMergeFailed',
......@@ -10,6 +13,19 @@ export default {
GlLoadingIcon,
GlButton,
},
mixins: [glFeatureFlagMixin(), mergeRequestQueryVariablesMixin],
apollo: {
mergeError: {
query: autoMergeFailedQuery,
skip() {
return !this.glFeatures.mergeRequestWidgetGraphql;
},
variables() {
return this.mergeRequestQueryVariables;
},
update: (data) => data.project?.mergeRequest?.mergeError,
},
},
props: {
mr: {
type: Object,
......@@ -18,6 +34,7 @@ export default {
},
data() {
return {
mergeError: this.glFeatures.mergeRequestWidgetGraphql ? null : this.mr.mergeError,
isRefreshing: false,
};
},
......@@ -36,7 +53,7 @@ export default {
<status-icon status="warning" />
<div class="media-body space-children gl-display-flex gl-flex-wrap gl-align-items-center">
<span class="bold">
<template v-if="mr.mergeError">{{ mr.mergeError }}</template>
<template v-if="mergeError">{{ mergeError }}</template>
{{ s__('mrWidget|This merge request failed to be merged automatically') }}
</span>
<gl-button
......
fragment autoMergeEnabled on MergeRequest {
autoMergeStrategy
mergeUser {
name
username
webUrl
avatarUrl
}
targetBranch
shouldRemoveSourceBranch
forceRemoveSourceBranch
userPermissions {
removeSourceBranch
}
}
#import "./auto_merge_enabled.fragment.graphql"
query autoMergeEnabledQuery($projectPath: ID!, $iid: String!) {
project(fullPath: $projectPath) {
mergeRequest(iid: $iid) {
...autoMergeEnabled
mergeTrainsCount
}
}
}
query autoMergeFailedQuery($projectPath: ID!, $iid: String!) {
project(fullPath: $projectPath) {
mergeRequest(iid: $iid) {
mergeError
}
}
}
......@@ -175,6 +175,10 @@ module Types
calls_gitaly: true, description: 'Merge request commits excluding merge commits'
field :security_auto_fix, GraphQL::BOOLEAN_TYPE, null: true,
description: 'Indicates if the merge request is created by @GitLab-Security-Bot.'
field :auto_merge_strategy, GraphQL::STRING_TYPE, null: true,
description: 'Selected auto merge strategy'
field :merge_user, Types::UserType, null: true,
description: 'User who merged this merge request'
def approved_by
object.approved_by_users
......
......@@ -13786,6 +13786,11 @@ type MergeRequest implements CurrentUserTodos & Noteable {
"""
autoMergeEnabled: Boolean!
"""
Selected auto merge strategy
"""
autoMergeStrategy: String
"""
Array of available auto merge strategies
"""
......@@ -14025,6 +14030,11 @@ type MergeRequest implements CurrentUserTodos & Noteable {
"""
mergeTrainsCount: Int
"""
User who merged this merge request
"""
mergeUser: User
"""
Indicates if the merge has been set to be merged when its pipeline succeeds (MWPS)
"""
......
......@@ -37858,6 +37858,20 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "autoMergeStrategy",
"description": "Selected auto merge strategy",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "availableAutoMergeStrategies",
"description": "Array of available auto merge strategies",
......@@ -38509,6 +38523,20 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "mergeUser",
"description": "User who merged this merge request",
"args": [
],
"type": {
"kind": "OBJECT",
"name": "User",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "mergeWhenPipelineSucceeds",
"description": "Indicates if the merge has been set to be merged when its pipeline succeeds (MWPS)",
......@@ -2086,6 +2086,7 @@ Autogenerated return type of MarkAsSpamSnippet.
| `assignees` | UserConnection | Assignees of the merge request |
| `author` | User | User who created this merge request |
| `autoMergeEnabled` | Boolean! | Indicates if auto merge is enabled for the merge request |
| `autoMergeStrategy` | String | Selected auto merge strategy |
| `availableAutoMergeStrategies` | String! => Array | Array of available auto merge strategies |
| `commitCount` | Int | Number of commits in the merge request |
| `commitsWithoutMergeCommits` | CommitConnection | Merge request commits excluding merge commits |
......@@ -2116,6 +2117,7 @@ Autogenerated return type of MarkAsSpamSnippet.
| `mergeOngoing` | Boolean! | Indicates if a merge is currently occurring |
| `mergeStatus` | String | Status of the merge request |
| `mergeTrainsCount` | Int | |
| `mergeUser` | User | User who merged this merge request |
| `mergeWhenPipelineSucceeds` | Boolean | Indicates if the merge has been set to be merged when its pipeline succeeds (MWPS) |
| `mergeable` | Boolean! | Indicates if the merge request is mergeable |
| `mergeableDiscussionsState` | Boolean | Indicates if all discussions in the merge request have been resolved, allowing the merge request to be merged |
......
......@@ -8,28 +8,27 @@ import { s__ } from '~/locale';
export default {
computed: {
statusTextBeforeAuthor() {
if (this.mr.autoMergeStrategy === MT_MERGE_STRATEGY) {
if (this.autoMergeStrategy === MT_MERGE_STRATEGY) {
return s__('mrWidget|Added to the merge train by');
}
return s__('mrWidget|Set by');
},
statusTextAfterAuthor() {
if (this.mr.autoMergeStrategy === MTWPS_MERGE_STRATEGY && this.mr.mergeTrainsCount === 0) {
const { mergeTrainsCount } = this.glFeatures.mergeRequestWidgetGraphql ? this.state : this.mr;
if (this.autoMergeStrategy === MTWPS_MERGE_STRATEGY && mergeTrainsCount === 0) {
return s__('mrWidget|to start a merge train when the pipeline succeeds');
} else if (
this.mr.autoMergeStrategy === MTWPS_MERGE_STRATEGY &&
this.mr.mergeTrainsCount !== 0
) {
} else if (this.autoMergeStrategy === MTWPS_MERGE_STRATEGY && mergeTrainsCount !== 0) {
return s__('mrWidget|to be added to the merge train when the pipeline succeeds');
} else if (this.mr.autoMergeStrategy === MWPS_MERGE_STRATEGY) {
} else if (this.autoMergeStrategy === MWPS_MERGE_STRATEGY) {
return s__('mrWidget|to be merged automatically when the pipeline succeeds');
}
return '';
},
cancelButtonText() {
if (this.mr.autoMergeStrategy === MT_MERGE_STRATEGY) {
if (this.autoMergeStrategy === MT_MERGE_STRATEGY) {
return s__('mrWidget|Remove from merge train');
}
......
#import "~/vue_merge_request_widget/queries/states/auto_merge_enabled.fragment.graphql"
query autoMergeEnabledQuery($projectPath: ID!, $iid: String!) {
project(fullPath: $projectPath) {
mergeRequest(iid: $iid) {
...autoMergeEnabled
}
}
}
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`MRWidgetAutoMergeEnabled when graphql is disabled template should have correct elements 1`] = `
<div
class="mr-widget-body media"
>
<status-icon-stub
status="success"
/>
<div
class="media-body"
>
<h4
class="gl-display-flex"
>
<span
class="gl-mr-3"
>
<span
class="js-status-text-before-author"
data-testid="beforeStatusText"
>
Set by
</span>
<mr-widget-author-stub
author="[object Object]"
showauthorname="true"
/>
<span
class="js-status-text-after-author"
data-testid="afterStatusText"
>
to be merged automatically when the pipeline succeeds
</span>
</span>
<a
class="btn btn-sm btn-default js-cancel-auto-merge"
data-testid="cancelAutomaticMergeButton"
href="#"
role="button"
>
<!---->
Cancel automatic merge
</a>
</h4>
<section
class="mr-info-list"
>
<p>
The changes will be merged into
<a
class="label-branch"
href="/foo/bar"
>
foo
</a>
</p>
<p
class="gl-display-flex"
>
<span
class="gl-mr-3"
>
The source branch will not be deleted
</span>
<a
class="btn btn-sm btn-default js-remove-source-branch"
data-testid="removeSourceBranchButton"
href="#"
role="button"
>
<!---->
Delete source branch
</a>
</p>
</section>
</div>
</div>
`;
exports[`MRWidgetAutoMergeEnabled when graphql is enabled template should have correct elements 1`] = `
<div
class="mr-widget-body media"
>
<status-icon-stub
status="success"
/>
<div
class="media-body"
>
<h4
class="gl-display-flex"
>
<span
class="gl-mr-3"
>
<span
class="js-status-text-before-author"
data-testid="beforeStatusText"
>
Set by
</span>
<mr-widget-author-stub
author="[object Object]"
showauthorname="true"
/>
<span
class="js-status-text-after-author"
data-testid="afterStatusText"
>
to be merged automatically when the pipeline succeeds
</span>
</span>
<a
class="btn btn-sm btn-default js-cancel-auto-merge"
data-testid="cancelAutomaticMergeButton"
href="#"
role="button"
>
<!---->
Cancel automatic merge
</a>
</h4>
<section
class="mr-info-list"
>
<p>
The changes will be merged into
<a
class="label-branch"
href="/foo/bar"
>
foo
</a>
</p>
<p
class="gl-display-flex"
>
<span
class="gl-mr-3"
>
The source branch will not be deleted
</span>
<a
class="btn btn-sm btn-default js-remove-source-branch"
data-testid="removeSourceBranchButton"
href="#"
role="button"
>
<!---->
Delete source branch
</a>
</p>
</section>
</div>
</div>
`;
import { nextTick } from 'vue';
import { shallowMount } from '@vue/test-utils';
import { GlLoadingIcon, GlButton } from '@gitlab/ui';
import AutoMergeFailedComponent from '~/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.vue';
......@@ -8,43 +9,60 @@ describe('MRWidgetAutoMergeFailed', () => {
const mergeError = 'This is the merge error';
const findButton = () => wrapper.find(GlButton);
const createComponent = (props = {}) => {
const createComponent = (props = {}, mergeRequestWidgetGraphql = false) => {
wrapper = shallowMount(AutoMergeFailedComponent, {
propsData: { ...props },
});
};
data() {
if (mergeRequestWidgetGraphql) {
return { mergeError: props.mr?.mergeError };
}
beforeEach(() => {
createComponent({
mr: { mergeError },
return {};
},
provide: {
glFeatures: { mergeRequestWidgetGraphql },
},
});
});
};
afterEach(() => {
wrapper.destroy();
});
it('renders failed message', () => {
expect(wrapper.text()).toContain('This merge request failed to be merged automatically');
});
[true, false].forEach((mergeRequestWidgetGraphql) => {
describe(`when graphql is ${mergeRequestWidgetGraphql ? 'enabled' : 'dislabed'}`, () => {
beforeEach(() => {
createComponent(
{
mr: { mergeError },
},
mergeRequestWidgetGraphql,
);
});
it('renders merge error provided', () => {
expect(wrapper.text()).toContain(mergeError);
});
it('renders failed message', () => {
expect(wrapper.text()).toContain('This merge request failed to be merged automatically');
});
it('render refresh button', () => {
expect(findButton().text()).toEqual('Refresh');
});
it('renders merge error provided', () => {
expect(wrapper.text()).toContain(mergeError);
});
it('render refresh button', () => {
expect(findButton().text()).toBe('Refresh');
});
it('emits event and shows loading icon when button is clicked', async () => {
jest.spyOn(eventHub, '$emit');
findButton().vm.$emit('click');
it('emits event and shows loading icon when button is clicked', () => {
jest.spyOn(eventHub, '$emit');
findButton().vm.$emit('click');
expect(eventHub.$emit.mock.calls[0][0]).toBe('MRWidgetUpdateRequested');
expect(eventHub.$emit.mock.calls[0][0]).toBe('MRWidgetUpdateRequested');
await nextTick();
return wrapper.vm.$nextTick(() => {
expect(findButton().attributes('disabled')).toBe('true');
expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
expect(findButton().attributes('disabled')).toBe('true');
expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
});
});
});
});
......@@ -30,6 +30,7 @@ RSpec.describe GitlabSchema.types['MergeRequest'] do
conflicts auto_merge_enabled approved_by source_branch_protected
default_merge_commit_message_with_description squash_on_merge available_auto_merge_strategies
has_ci mergeable commits_without_merge_commits squash security_auto_fix default_squash_commit_message
auto_merge_strategy merge_user
]
if Gitlab.ee?
......
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