Commit 54bdd83e authored by Phil Hughes's avatar Phil Hughes Committed by Igor Drozdov

Converts widget conflicts state data to GraphQL

Moves the state data used in the conflicts widget state
over to GraphQL.

Closes https://gitlab.com/gitlab-org/gitlab/-/issues/235712
parent dbb363fd
<script>
import $ from 'jquery';
import { escape } from 'lodash';
import { GlButton, GlModalDirective } from '@gitlab/ui';
import { GlButton, GlModalDirective, GlSkeletonLoader } from '@gitlab/ui';
import { s__, sprintf } from '~/locale';
import { mouseenter, debouncedMouseleave, togglePopover } from '~/shared/popover';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import mergeRequestQueryVariablesMixin from '../../mixins/merge_request_query_variables';
import StatusIcon from '../mr_widget_status_icon.vue';
import userPermissionsQuery from '../../queries/permissions.query.graphql';
import conflictsStateQuery from '../../queries/states/conflicts.query.graphql';
export default {
name: 'MRWidgetConflicts',
components: {
GlSkeletonLoader,
StatusIcon,
GlButton,
},
directives: {
GlModalDirective,
},
mixins: [glFeatureFlagMixin(), mergeRequestQueryVariablesMixin],
apollo: {
userPermissions: {
query: userPermissionsQuery,
skip() {
return !this.glFeatures.mergeRequestWidgetGraphql;
},
variables() {
return this.mergeRequestQueryVariables;
},
update: data => data.project.mergeRequest.userPermissions,
},
stateData: {
query: conflictsStateQuery,
skip() {
return !this.glFeatures.mergeRequestWidgetGraphql;
},
variables() {
return this.mergeRequestQueryVariables;
},
update: data => data.project.mergeRequest,
},
},
props: {
/* TODO: This is providing all store and service down when it
only needs a few props */
......@@ -24,21 +52,72 @@ export default {
default: () => ({}),
},
},
data() {
return {
userPermissions: {},
stateData: {},
};
},
computed: {
isLoading() {
return (
this.glFeatures.mergeRequestWidgetGraphql &&
this.$apollo.queries.userPermissions.loading &&
this.$apollo.queries.stateData.loading
);
},
canPushToSourceBranch() {
if (this.glFeatures.mergeRequestWidgetGraphql) {
return this.userPermissions.pushToSourceBranch;
}
return this.mr.canPushToSourceBranch;
},
canMerge() {
if (this.glFeatures.mergeRequestWidgetGraphql) {
return this.userPermissions.canMerge;
}
return this.mr.canMerge;
},
shouldBeRebased() {
if (this.glFeatures.mergeRequestWidgetGraphql) {
return this.stateData.shouldBeRebased;
}
return this.mr.shouldBeRebased;
},
sourceBranchProtected() {
if (this.glFeatures.mergeRequestWidgetGraphql) {
return this.stateData.sourceBranchProtected;
}
return this.mr.sourceBranchProtected;
},
popoverTitle() {
return s__(
'mrWidget|This feature merges changes from the target branch to the source branch. You cannot use this feature since the source branch is protected.',
);
},
showResolveButton() {
return this.mr.conflictResolutionPath && this.mr.canPushToSourceBranch;
return this.mr.conflictResolutionPath && this.canPushToSourceBranch;
},
showPopover() {
return this.showResolveButton && this.mr.sourceBranchProtected;
return this.showResolveButton && this.sourceBranchProtected;
},
},
mounted() {
if (this.showPopover) {
watch: {
showPopover: {
handler(newVal) {
if (newVal) {
this.$nextTick(this.initPopover);
}
},
immediate: true,
},
},
methods: {
initPopover() {
const $el = $(this.$refs.popover);
$el
......@@ -68,7 +147,7 @@ export default {
.on('show.bs.popover', () => {
window.addEventListener('scroll', togglePopover.bind($el, false), { once: true });
});
}
},
},
};
</script>
......@@ -76,17 +155,24 @@ export default {
<div class="mr-widget-body media">
<status-icon :show-disabled-button="true" status="warning" />
<div class="media-body space-children">
<span v-if="mr.shouldBeRebased" class="bold">
<div v-if="isLoading" class="gl-ml-4 gl-w-full mr-conflict-loader">
<gl-skeleton-loader :width="334" :height="30">
<rect x="0" y="7" width="150" height="16" rx="4" />
<rect x="158" y="7" width="84" height="16" rx="4" />
<rect x="250" y="7" width="84" height="16" rx="4" />
</gl-skeleton-loader>
</div>
<div v-else class="media-body space-children">
<span v-if="shouldBeRebased" class="bold">
{{
s__(`mrWidget|Fast-forward merge is not possible.
To merge this request, first rebase locally.`)
To merge this request, first rebase locally.`)
}}
</span>
<template v-else>
<span class="bold">
{{ s__('mrWidget|There are merge conflicts') }}<span v-if="!mr.canMerge">.</span>
<span v-if="!mr.canMerge">
{{ s__('mrWidget|There are merge conflicts') }}<span v-if="!canMerge">.</span>
<span v-if="!canMerge">
{{
s__(`mrWidget|Resolve these conflicts or ask someone
with write access to this repository to merge it locally`)
......@@ -95,15 +181,15 @@ To merge this request, first rebase locally.`)
</span>
<span v-if="showResolveButton" ref="popover">
<gl-button
:href="mr.conflictResolutionPath"
:disabled="mr.sourceBranchProtected"
:href="!sourceBranchProtected && mr.conflictResolutionPath"
:disabled="sourceBranchProtected"
class="js-resolve-conflicts-button"
>
{{ s__('mrWidget|Resolve conflicts') }}
</gl-button>
</span>
<gl-button
v-if="mr.canMerge"
v-if="canMerge"
v-gl-modal-directive="'modal-merge-info'"
class="js-merge-locally-button"
>
......
query userPermissionsQuery($projectPath: ID!, $iid: String!) {
project(fullPath: $projectPath) {
mergeRequest(iid: $iid) {
userPermissions {
canMerge
pushToSourceBranch
}
}
}
}
query workInProgressQuery($projectPath: ID!, $iid: String!) {
project(fullPath: $projectPath) {
mergeRequest(iid: $iid) {
shouldBeRebased
sourceBranchProtected
}
}
}
......@@ -1039,3 +1039,11 @@ $mr-widget-min-height: 69px;
.diff-file-row.is-active {
background-color: $gray-50;
}
.mr-conflict-loader {
max-width: 334px;
> svg {
vertical-align: middle;
}
}
......@@ -49,6 +49,8 @@ module Types
description: 'ID of the merge request target project'
field :source_branch, GraphQL::STRING_TYPE, null: false,
description: 'Source branch of the merge request'
field :source_branch_protected, GraphQL::BOOLEAN_TYPE, null: false, calls_gitaly: true,
description: 'Indicates if the source branch is protected'
field :target_branch, GraphQL::STRING_TYPE, null: false,
description: 'Target branch of the merge request'
field :work_in_progress, GraphQL::BOOLEAN_TYPE, method: :work_in_progress?, null: false,
......@@ -194,6 +196,10 @@ module Types
def commit_count
object&.metrics&.commits_count
end
def source_branch_protected
object.source_project.present? && ProtectedBranch.protected?(object.source_project, object.source_branch)
end
end
end
Types::MergeRequestType.prepend_if_ee('::EE::Types::MergeRequestType')
......@@ -12780,6 +12780,11 @@ type MergeRequest implements CurrentUserTodos & Noteable {
"""
sourceBranchExists: Boolean!
"""
Indicates if the source branch is protected
"""
sourceBranchProtected: Boolean!
"""
Source project of the merge request
"""
......
......@@ -35042,6 +35042,24 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "sourceBranchProtected",
"description": "Indicates if the source branch is protected",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Boolean",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "sourceProject",
"description": "Source project of the merge request",
......@@ -1950,6 +1950,7 @@ Autogenerated return type of MarkAsSpamSnippet.
| `shouldRemoveSourceBranch` | Boolean | Indicates if the source branch of the merge request will be deleted after merge |
| `sourceBranch` | String! | Source branch of the merge request |
| `sourceBranchExists` | Boolean! | Indicates if the source branch of the merge request exists |
| `sourceBranchProtected` | Boolean! | Indicates if the source branch is protected |
| `sourceProject` | Project | Source project of the merge request |
| `sourceProjectId` | Int | ID of the merge request source project |
| `state` | MergeRequestState! | State of the merge request |
......
......@@ -6,6 +6,7 @@ import ConflictsComponent from '~/vue_merge_request_widget/components/states/mr_
describe('MRWidgetConflicts', () => {
let vm;
let mergeRequestWidgetGraphql = null;
const path = '/conflicts';
function createComponent(propsData = {}) {
......@@ -13,7 +14,35 @@ describe('MRWidgetConflicts', () => {
vm = shallowMount(localVue.extend(ConflictsComponent), {
propsData,
provide: {
glFeatures: {
mergeRequestWidgetGraphql,
},
},
mocks: {
$apollo: {
queries: {
userPermissions: { loading: false },
stateData: { loading: false },
},
},
},
});
if (mergeRequestWidgetGraphql) {
vm.setData({
userPermissions: {
canMerge: propsData.mr.canMerge,
pushToSourceBranch: propsData.mr.canPushToSourceBranch,
},
stateData: {
shouldBeRebased: propsData.mr.shouldBeRebased,
sourceBranchProtected: propsData.mr.sourceBranchProtected,
},
});
}
return vm.vm.$nextTick();
}
beforeEach(() => {
......@@ -21,9 +50,16 @@ describe('MRWidgetConflicts', () => {
});
afterEach(() => {
mergeRequestWidgetGraphql = null;
vm.destroy();
});
[false, true].forEach(featureEnabled => {
describe(`with GraphQL feature flag ${featureEnabled ? 'enabled' : 'disabled'}`, () => {
beforeEach(() => {
mergeRequestWidgetGraphql = featureEnabled;
});
// There are two permissions we need to consider:
//
// 1. Is the user allowed to merge to the target branch?
......@@ -34,8 +70,8 @@ describe('MRWidgetConflicts', () => {
// branch should be allowed to resolve conflicts. This is
// consistent with what the backend does.
describe('when allowed to merge but not allowed to push to source branch', () => {
beforeEach(() => {
createComponent({
beforeEach(async () => {
await createComponent({
mr: {
canMerge: true,
canPushToSourceBranch: false,
......@@ -62,8 +98,8 @@ describe('MRWidgetConflicts', () => {
});
describe('when not allowed to merge but allowed to push to source branch', () => {
beforeEach(() => {
createComponent({
beforeEach(async () => {
await createComponent({
mr: {
canMerge: false,
canPushToSourceBranch: true,
......@@ -91,8 +127,8 @@ describe('MRWidgetConflicts', () => {
});
describe('when allowed to merge and push to source branch', () => {
beforeEach(() => {
createComponent({
beforeEach(async () => {
await createComponent({
mr: {
canMerge: true,
canPushToSourceBranch: true,
......@@ -122,8 +158,8 @@ describe('MRWidgetConflicts', () => {
});
describe('when user does not have permission to push to source branch', () => {
it('should show proper message', () => {
createComponent({
it('should show proper message', async () => {
await createComponent({
mr: {
canMerge: false,
canPushToSourceBranch: false,
......@@ -139,8 +175,8 @@ describe('MRWidgetConflicts', () => {
).toContain('ask someone with write access');
});
it('should not have action buttons', () => {
createComponent({
it('should not have action buttons', async () => {
await createComponent({
mr: {
canMerge: false,
canPushToSourceBranch: false,
......@@ -152,8 +188,8 @@ describe('MRWidgetConflicts', () => {
expect(vm.find('.js-merge-locally-button').exists()).toBe(false);
});
it('should not have resolve button when no conflict resolution path', () => {
createComponent({
it('should not have resolve button when no conflict resolution path', async () => {
await createComponent({
mr: {
canMerge: true,
conflictResolutionPath: null,
......@@ -166,8 +202,8 @@ describe('MRWidgetConflicts', () => {
});
describe('when fast-forward or semi-linear merge enabled', () => {
it('should tell you to rebase locally', () => {
createComponent({
it('should tell you to rebase locally', async () => {
await createComponent({
mr: {
shouldBeRebased: true,
conflictsDocsPath: '',
......@@ -181,8 +217,8 @@ describe('MRWidgetConflicts', () => {
});
describe('when source branch protected', () => {
beforeEach(() => {
createComponent({
beforeEach(async () => {
await createComponent({
mr: {
canMerge: true,
canPushToSourceBranch: true,
......@@ -203,8 +239,8 @@ describe('MRWidgetConflicts', () => {
});
describe('when source branch not protected', () => {
beforeEach(() => {
createComponent({
beforeEach(async () => {
await createComponent({
mr: {
canMerge: true,
canPushToSourceBranch: true,
......@@ -223,4 +259,6 @@ describe('MRWidgetConflicts', () => {
expect($.fn.popover).not.toHaveBeenCalled();
});
});
});
});
});
......@@ -27,7 +27,7 @@ RSpec.describe GitlabSchema.types['MergeRequest'] do
upvotes downvotes head_pipeline pipelines task_completion_status
milestone assignees participants subscribed labels discussion_locked time_estimate
total_time_spent reference author merged_at commit_count current_user_todos
conflicts auto_merge_enabled approved_by
conflicts auto_merge_enabled approved_by source_branch_protected
]
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