diff options
Diffstat (limited to 'app/assets/javascripts/vue_merge_request_widget')
31 files changed, 413 insertions, 160 deletions
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals.vue b/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals.vue index 0f9d1b8395b..7297f8f8677 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals.vue @@ -1,6 +1,6 @@ <script> import { GlButton } from '@gitlab/ui'; -import createFlash from '~/flash'; +import { deprecatedCreateFlash as createFlash } from '~/flash'; import { s__ } from '~/locale'; import eventHub from '../../event_hub'; import approvalsMixin from '../../mixins/approvals'; diff --git a/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals_summary_optional.vue b/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals_summary_optional.vue index 66af0c5a83e..24cd9d6428d 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals_summary_optional.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals_summary_optional.vue @@ -1,10 +1,6 @@ <script> import { GlTooltipDirective, GlLink } from '@gitlab/ui'; import Icon from '~/vue_shared/components/icon.vue'; -import { - OPTIONAL, - OPTIONAL_CAN_APPROVE, -} from '~/vue_merge_request_widget/components/approvals/messages'; export default { components: { @@ -25,17 +21,12 @@ export default { default: '', }, }, - computed: { - message() { - return this.canApprove ? OPTIONAL_CAN_APPROVE : OPTIONAL; - }, - }, }; </script> <template> <div class="d-flex align-items-center"> - <span class="text-muted">{{ message }}</span> + <span class="text-muted">{{ s__('mrWidget|Approval is optional') }}</span> <gl-link v-if="canApprove && helpPath" v-gl-tooltip diff --git a/app/assets/javascripts/vue_merge_request_widget/components/approvals/messages.js b/app/assets/javascripts/vue_merge_request_widget/components/approvals/messages.js index 1d9368f71aa..0538c38307b 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/approvals/messages.js +++ b/app/assets/javascripts/vue_merge_request_widget/components/approvals/messages.js @@ -7,5 +7,3 @@ export const FETCH_ERROR = s__( export const APPROVE_ERROR = s__('mrWidget|An error occurred while submitting your approval.'); export const UNAPPROVE_ERROR = s__('mrWidget|An error occurred while removing your approval.'); export const APPROVED_MESSAGE = s__('mrWidget|Merge request approved.'); -export const OPTIONAL_CAN_APPROVE = s__('mrWidget|No approval required; you can still approve'); -export const OPTIONAL = s__('mrWidget|No approval required'); diff --git a/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_actions.vue b/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_actions.vue index 573fc388cca..af0b4087d46 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_actions.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_actions.vue @@ -1,7 +1,7 @@ <script> import { GlIcon } from '@gitlab/ui'; import { __, s__ } from '~/locale'; -import createFlash from '~/flash'; +import { deprecatedCreateFlash as createFlash } from '~/flash'; import { visitUrl } from '~/lib/utils/url_utility'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import MRWidgetService from '../../services/mr_widget_service'; diff --git a/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_view_button.vue b/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_view_button.vue index bce25ca20ec..b12250d1d1c 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_view_button.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_view_button.vue @@ -60,28 +60,24 @@ export default { :main-action-link="deploymentExternalUrl" filter-key="path" > - <template slot="mainAction" slot-scope="slotProps"> + <template #mainAction="{ className }"> <review-app-link :display="appButtonText" :link="deploymentExternalUrl" - :css-class="`deploy-link js-deploy-url inline ${slotProps.className}`" + :css-class="`deploy-link js-deploy-url inline ${className}`" /> </template> - <template slot="result" slot-scope="slotProps"> + <template #result="{ result }"> <gl-link - :href="slotProps.result.external_url" + :href="result.external_url" target="_blank" rel="noopener noreferrer nofollow" class="js-deploy-url-menu-item menu-item" > - <strong class="str-truncated-100 gl-mb-0 d-block"> - {{ slotProps.result.path }} - </strong> + <strong class="str-truncated-100 gl-mb-0 d-block">{{ result.path }}</strong> - <p class="text-secondary str-truncated-100 gl-mb-0 d-block"> - {{ slotProps.result.external_url }} - </p> + <p class="text-secondary str-truncated-100 gl-mb-0 d-block">{{ result.external_url }}</p> </gl-link> </template> </filtered-search-dropdown> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_expandable_section.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_expandable_section.vue index fd999540f4a..c368399ed6f 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_expandable_section.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_expandable_section.vue @@ -1,6 +1,6 @@ <script> -import { __ } from '~/locale'; import { GlButton, GlCollapse, GlIcon } from '@gitlab/ui'; +import { __ } from '~/locale'; /** * Renders header section with icon and expand button diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue index a096eb1a1fe..7326bd0804d 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue @@ -84,9 +84,6 @@ export default { hasCommitInfo() { return this.pipeline.commit && Object.keys(this.pipeline.commit).length > 0; }, - isTriggeredByMergeRequest() { - return Boolean(this.pipeline.merge_request); - }, isMergeRequestPipeline() { return Boolean(this.pipeline.flags && this.pipeline.flags.merge_request_pipeline); }, diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_suggest_pipeline.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_suggest_pipeline.vue index de01821a292..936fdc9aff5 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_suggest_pipeline.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_suggest_pipeline.vue @@ -2,28 +2,36 @@ import { GlLink, GlSprintf, GlButton } from '@gitlab/ui'; import MrWidgetIcon from './mr_widget_icon.vue'; import Tracking from '~/tracking'; -import { s__ } from '~/locale'; +import DismissibleContainer from '~/vue_shared/components/dismissible_container.vue'; +import { + SP_TRACK_LABEL, + SP_LINK_TRACK_EVENT, + SP_SHOW_TRACK_EVENT, + SP_LINK_TRACK_VALUE, + SP_SHOW_TRACK_VALUE, + SP_HELP_CONTENT, + SP_HELP_URL, + SP_ICON_NAME, +} from '../constants'; const trackingMixin = Tracking.mixin(); -const TRACK_LABEL = 'no_pipeline_noticed'; export default { name: 'MRWidgetSuggestPipeline', - iconName: 'status_notfound', - trackLabel: TRACK_LABEL, - linkTrackValue: 30, - linkTrackEvent: 'click_link', - showTrackValue: 10, - showTrackEvent: 'click_button', - helpContent: s__( - `mrWidget|Use %{linkStart}CI pipelines to test your code%{linkEnd} by simply adding a GitLab CI configuration file to your project. It only takes a minute to make your code more secure and robust.`, - ), - helpURL: 'https://about.gitlab.com/blog/2019/07/12/guide-to-ci-cd-pipelines/', + SP_ICON_NAME, + SP_TRACK_LABEL, + SP_LINK_TRACK_EVENT, + SP_SHOW_TRACK_EVENT, + SP_LINK_TRACK_VALUE, + SP_SHOW_TRACK_VALUE, + SP_HELP_CONTENT, + SP_HELP_URL, components: { GlLink, GlSprintf, GlButton, MrWidgetIcon, + DismissibleContainer, }, mixins: [trackingMixin], props: { @@ -39,11 +47,19 @@ export default { type: String, required: true, }, + userCalloutsPath: { + type: String, + required: true, + }, + userCalloutFeatureId: { + type: String, + required: true, + }, }, computed: { tracking() { return { - label: TRACK_LABEL, + label: SP_TRACK_LABEL, property: this.humanAccess, }; }, @@ -54,9 +70,14 @@ export default { }; </script> <template> - <div class="mr-widget-body mr-pipeline-suggest gl-mb-3"> - <div class="gl-display-flex gl-align-items-center"> - <mr-widget-icon :name="$options.iconName" /> + <dismissible-container + class="mr-widget-body mr-pipeline-suggest gl-mb-3" + :path="userCalloutsPath" + :feature-id="userCalloutFeatureId" + @dismiss="$emit('dismiss')" + > + <template #title> + <mr-widget-icon :name="$options.SP_ICON_NAME" /> <div> <gl-sprintf :message=" @@ -76,18 +97,18 @@ export default { class="gl-ml-1" data-testid="add-pipeline-link" :data-track-property="humanAccess" - :data-track-value="$options.linkTrackValue" - :data-track-event="$options.linkTrackEvent" - :data-track-label="$options.trackLabel" + :data-track-value="$options.SP_LINK_TRACK_VALUE" + :data-track-event="$options.SP_LINK_TRACK_EVENT" + :data-track-label="$options.SP_TRACK_LABEL" > {{ content }} </gl-link> </template> </gl-sprintf> </div> - </div> + </template> <div class="row"> - <div class="col-md-5 order-md-last col-12 gl-mt-5 mt-md-n3 svg-content svg-225"> + <div class="col-md-5 order-md-last col-12 gl-mt-5 mt-md-n1 pt-md-1 svg-content svg-225"> <img data-testid="pipeline-image" :src="pipelineSvgPath" /> </div> <div class="col-md-7 order-md-first col-12"> @@ -96,11 +117,11 @@ export default { {{ s__('mrWidget|Are you adding technical debt or code vulnerabilities?') }} </strong> <p class="gl-mt-2"> - <gl-sprintf :message="$options.helpContent"> + <gl-sprintf :message="$options.SP_HELP_CONTENT"> <template #link="{ content }"> <gl-link data-testid="help" - :href="$options.helpURL" + :href="$options.SP_HELP_URL" target="_blank" class="font-size-inherit" >{{ content }} @@ -115,14 +136,14 @@ export default { variant="info" :href="pipelinePath" :data-track-property="humanAccess" - :data-track-value="$options.showTrackValue" - :data-track-event="$options.showTrackEvent" - :data-track-label="$options.trackLabel" + :data-track-value="$options.SP_SHOW_TRACK_VALUE" + :data-track-event="$options.SP_SHOW_TRACK_EVENT" + :data-track-label="$options.SP_TRACK_LABEL" > {{ __('Show me how to add a pipeline') }} </gl-button> </div> </div> </div> - </div> + </dismissible-container> </template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/source_branch_removal_status.vue b/app/assets/javascripts/vue_merge_request_widget/components/source_branch_removal_status.vue index a0e76b151f7..dab0540f44e 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/source_branch_removal_status.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/source_branch_removal_status.vue @@ -1,22 +1,37 @@ <script> +import { GlSprintf } from '@gitlab/ui'; import tooltip from '../../vue_shared/directives/tooltip'; import { __ } from '../../locale'; export default { + i18n: { + removesBranchText: __('%{strongStart}Deletes%{strongEnd} source branch'), + tooltipTitle: __('A user with write access to the source branch selected this option'), + }, + components: { + GlSprintf, + }, directives: { tooltip, }, - created() { - this.removesBranchText = __('<strong>Deletes</strong> source branch'); - this.tooltipTitle = __('A user with write access to the source branch selected this option'); - }, }; </script> <template> <p v-once class="mr-info-list mr-links gl-mb-0"> - <span class="status-text" v-html="removesBranchText"> </span> - <i v-tooltip :title="tooltipTitle" :aria-label="tooltipTitle" class="fa fa-question-circle"> + <span class="status-text"> + <gl-sprintf :message="$options.i18n.removesBranchText"> + <template #strong="{ content }"> + <strong>{{ content }}</strong> + </template> + </gl-sprintf> + </span> + <i + v-tooltip + :title="$options.i18n.tooltipTitle" + :aria-label="$options.i18n.tooltipTitle" + class="fa fa-question-circle" + > </i> </p> </template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/commit_message_dropdown.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/commit_message_dropdown.vue index b6722de5277..f17e409d996 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/commit_message_dropdown.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/commit_message_dropdown.vue @@ -1,10 +1,10 @@ <script> -import { GlDropdown, GlDropdownItem } from '@gitlab/ui'; +import { GlDeprecatedDropdown, GlDeprecatedDropdownItem } from '@gitlab/ui'; export default { components: { - GlDropdown, - GlDropdownItem, + GlDeprecatedDropdown, + GlDeprecatedDropdownItem, }, props: { commits: { @@ -18,20 +18,20 @@ export default { <template> <div> - <gl-dropdown + <gl-deprecated-dropdown right text="Use an existing commit message" variant="link" class="mr-commit-dropdown" > - <gl-dropdown-item + <gl-deprecated-dropdown-item v-for="commit in commits" :key="commit.short_id" class="text-nowrap text-truncate" @click="$emit('input', commit.message)" > <span class="monospace mr-2">{{ commit.short_id }}</span> {{ commit.title }} - </gl-dropdown-item> - </gl-dropdown> + </gl-deprecated-dropdown-item> + </gl-deprecated-dropdown> </div> </template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue index f02e0ac84da..12f65a4c235 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue @@ -1,6 +1,6 @@ <script> import autoMergeMixin from 'ee_else_ce/vue_merge_request_widget/mixins/auto_merge'; -import Flash from '../../../flash'; +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'; diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue index 1a6e186a371..166700dbcbf 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue @@ -1,7 +1,7 @@ <script> /* eslint-disable @gitlab/vue-require-i18n-strings */ import { GlLoadingIcon } from '@gitlab/ui'; -import Flash from '~/flash'; +import { deprecatedCreateFlash as Flash } from '~/flash'; import tooltip from '~/vue_shared/directives/tooltip'; import { s__, __ } from '~/locale'; import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue index c3cc30a1a6f..794c994bffe 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue @@ -4,7 +4,7 @@ import { escape } from 'lodash'; import simplePoll from '../../../lib/utils/simple_poll'; import eventHub from '../../event_hub'; import statusIcon from '../mr_widget_status_icon.vue'; -import Flash from '../../../flash'; +import { deprecatedCreateFlash as Flash } from '../../../flash'; import { __, sprintf } from '~/locale'; export default { diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue index cc43135f50a..930a2b68d8e 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue @@ -8,14 +8,23 @@ import simplePoll from '~/lib/utils/simple_poll'; import { __, sprintf } from '~/locale'; import MergeRequest from '../../../merge_request'; import { refreshUserMergeRequestCounts } from '~/commons/nav/user_merge_requests'; -import Flash from '../../../flash'; +import { deprecatedCreateFlash as Flash } from '../../../flash'; import statusIcon from '../mr_widget_status_icon.vue'; import eventHub from '../../event_hub'; import SquashBeforeMerge from './squash_before_merge.vue'; import CommitsHeader from './commits_header.vue'; import CommitEdit from './commit_edit.vue'; import CommitMessageDropdown from './commit_message_dropdown.vue'; -import { AUTO_MERGE_STRATEGIES } from '../../constants'; +import { AUTO_MERGE_STRATEGIES, DANGER, INFO, WARNING } from '../../constants'; + +const PIPELINE_RUNNING_STATE = 'running'; +const PIPELINE_FAILED_STATE = 'failed'; +const PIPELINE_PENDING_STATE = 'pending'; +const PIPELINE_SUCCESS_STATE = 'success'; + +const MERGE_FAILED_STATUS = 'failed'; +const MERGE_SUCCESS_STATUS = 'success'; +const MERGE_HOOK_VALIDATION_ERROR_STATUS = 'hook_validation_error'; export default { name: 'ReadyToMerge', @@ -29,6 +38,8 @@ export default { GlSprintf, GlLink, GlDeprecatedButton, + MergeTrainHelperText: () => + import('ee_component/vue_merge_request_widget/components/merge_train_helper_text.vue'), MergeImmediatelyConfirmationDialog: () => import( 'ee_component/vue_merge_request_widget/components/merge_immediately_confirmation_dialog.vue' @@ -60,35 +71,45 @@ export default { const { pipeline, isPipelineFailed, hasCI, ciStatus } = this.mr; if ((hasCI && !ciStatus) || this.hasPipelineMustSucceedConflict) { - return 'failed'; - } else if (this.isAutoMergeAvailable) { - return 'pending'; - } else if (!pipeline) { - return 'success'; - } else if (isPipelineFailed) { - return 'failed'; + return PIPELINE_FAILED_STATE; + } + + if (this.isAutoMergeAvailable) { + return PIPELINE_PENDING_STATE; + } + + if (pipeline && isPipelineFailed) { + return PIPELINE_FAILED_STATE; } - return 'success'; + return PIPELINE_SUCCESS_STATE; }, mergeButtonVariant() { - if (this.status === 'failed') { - return 'danger'; - } else if (this.status === 'pending') { - return 'info'; + if (this.status === PIPELINE_FAILED_STATE) { + return DANGER; } - return 'success'; + + if (this.status === PIPELINE_PENDING_STATE) { + return INFO; + } + + return PIPELINE_SUCCESS_STATE; }, iconClass() { + if (this.shouldRenderMergeTrainHelperText && !this.mr.preventMerge) { + return PIPELINE_RUNNING_STATE; + } + if ( - this.status === 'failed' || + this.status === PIPELINE_FAILED_STATE || !this.commitMessage.length || !this.mr.isMergeAllowed || this.mr.preventMerge ) { - return 'warning'; + return WARNING; } - return 'success'; + + return PIPELINE_SUCCESS_STATE; }, mergeButtonText() { if (this.isMergingImmediately) { @@ -167,11 +188,13 @@ export default { .merge(options) .then(res => res.data) .then(data => { - const hasError = data.status === 'failed' || data.status === 'hook_validation_error'; + const hasError = + data.status === MERGE_FAILED_STATUS || + data.status === MERGE_HOOK_VALIDATION_ERROR_STATUS; if (AUTO_MERGE_STRATEGIES.includes(data.status)) { eventHub.$emit('MRWidgetUpdateRequested'); - } else if (data.status === 'success') { + } else if (data.status === MERGE_SUCCESS_STATUS) { this.initiateMergePolling(); } else if (hasError) { eventHub.$emit('FailedToMerge', data.merge_error); @@ -269,7 +292,7 @@ export default { <template> <div> - <div class="mr-widget-body media"> + <div class="mr-widget-body media" :class="{ 'gl-pb-3': shouldRenderMergeTrainHelperText }"> <status-icon :status="iconClass" /> <div class="media-body"> <div class="mr-widget-body-controls media space-children"> @@ -358,6 +381,7 @@ export default { <div v-if="hasPipelineMustSucceedConflict" class="gl-display-flex gl-align-items-center" + data-testid="pipeline-succeed-conflict" > <gl-sprintf :message="pipelineMustSucceedConflictText" /> <gl-link @@ -379,6 +403,13 @@ export default { </div> </div> </div> + <merge-train-helper-text + v-if="shouldRenderMergeTrainHelperText" + :pipeline-id="mr.pipeline.id" + :pipeline-link="mr.pipeline.path" + :merge-train-length="mr.mergeTrainsCount" + :merge-train-when-pipeline-succeeds-docs-path="mr.mergeTrainWhenPipelineSucceedsDocsPath" + /> <template v-if="shouldShowMergeControls"> <div v-if="mr.ffOnlyEnabled" class="mr-fast-forward-message"> {{ __('Fast-forward merge without a merge commit') }} diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/squash_before_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/squash_before_merge.vue index efd58341a2d..3cf7dc3c4d1 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/squash_before_merge.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/squash_before_merge.vue @@ -38,7 +38,7 @@ export default { <div class="inline"> <label v-tooltip - :class="{ 'gl-text-gray-600': isDisabled }" + :class="{ 'gl-text-gray-400': isDisabled }" data-testid="squashLabel" :data-title="tooltipTitle" > diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/unresolved_discussions.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/unresolved_discussions.vue index d4a5fdb4b97..78e69b9ff9b 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/unresolved_discussions.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/unresolved_discussions.vue @@ -1,10 +1,13 @@ <script> +import { GlButton } from '@gitlab/ui'; import statusIcon from '../mr_widget_status_icon.vue'; +import notesEventHub from '~/notes/event_hub'; export default { name: 'UnresolvedDiscussions', components: { statusIcon, + GlButton, }, props: { mr: { @@ -12,23 +15,39 @@ export default { required: true, }, }, + methods: { + jumpToFirstUnresolvedDiscussion() { + notesEventHub.$emit('jumpToFirstUnresolvedDiscussion'); + }, + }, }; </script> <template> - <div class="mr-widget-body media"> + <div class="mr-widget-body media gl-flex-wrap"> <status-icon :show-disabled-button="true" status="warning" /> - <div class="media-body space-children"> - <span class="bold"> - {{ s__('mrWidget|There are unresolved threads. Please resolve these threads') }} - </span> - <a + <div class="media-body"> + <span class="gl-ml-3 gl-font-weight-bold gl-display-block gl-w-100">{{ + s__('mrWidget|Before this can be merged, one or more threads must be resolved.') + }}</span> + <gl-button + data-testid="jump-to-first" + class="gl-ml-3" + size="small" + icon="comment-next" + @click="jumpToFirstUnresolvedDiscussion" + > + {{ s__('mrWidget|Jump to first unresolved thread') }} + </gl-button> + <gl-button v-if="mr.createIssueToResolveDiscussionsPath" :href="mr.createIssueToResolveDiscussionsPath" - class="btn btn-default btn-sm js-create-issue" + class="js-create-issue gl-ml-3" + size="small" + icon="issue-new" > - {{ s__('mrWidget|Create an issue to resolve them later') }} - </a> + {{ s__('mrWidget|Resolve all threads in new issue') }} + </gl-button> </div> </div> </template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue index 118caac84b9..44668170fe4 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue @@ -1,8 +1,13 @@ <script> import $ from 'jquery'; -import { GlDeprecatedButton } from '@gitlab/ui'; -import { __, s__ } from '~/locale'; -import createFlash from '~/flash'; +import { GlButton } from '@gitlab/ui'; +import { __ } from '~/locale'; +import { deprecatedCreateFlash as createFlash } from '~/flash'; +import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; +import mergeRequestQueryVariablesMixin from '../../mixins/merge_request_query_variables'; +import getStateQuery from '../../queries/get_state.query.graphql'; +import workInProgressQuery from '../../queries/states/work_in_progress.query.graphql'; +import removeWipMutation from '../../queries/toggle_wip.mutation.graphql'; import StatusIcon from '../mr_widget_status_icon.vue'; import tooltip from '../../../vue_shared/directives/tooltip'; import eventHub from '../../event_hub'; @@ -11,72 +16,151 @@ export default { name: 'WorkInProgress', components: { StatusIcon, - GlDeprecatedButton, + GlButton, }, directives: { tooltip, }, + mixins: [glFeatureFlagMixin(), mergeRequestQueryVariablesMixin], + apollo: { + userPermissions: { + query: workInProgressQuery, + skip() { + return !this.glFeatures.mergeRequestWidgetGraphql; + }, + variables() { + return this.mergeRequestQueryVariables; + }, + update: data => data.project.mergeRequest.userPermissions, + }, + }, props: { mr: { type: Object, required: true }, service: { type: Object, required: true }, }, data() { return { + userPermissions: {}, isMakingRequest: false, }; }, computed: { - wipInfoTooltip() { - return s__( - 'mrWidget|When this merge request is ready, remove the WIP: prefix from the title to allow it to be merged', - ); + canUpdate() { + if (this.glFeatures.mergeRequestWidgetGraphql) { + return this.userPermissions.updateMergeRequest; + } + + return Boolean(this.mr.removeWIPPath); }, }, methods: { - handleRemoveWIP() { + removeWipMutation() { this.isMakingRequest = true; - this.service - .removeWIP() - .then(res => res.data) - .then(data => { - eventHub.$emit('UpdateWidgetData', data); + + this.$apollo + .mutate({ + mutation: removeWipMutation, + variables: { + ...this.mergeRequestQueryVariables, + wip: false, + }, + update( + store, + { + data: { + mergeRequestSetWip: { + errors, + mergeRequest: { workInProgress, title }, + }, + }, + }, + ) { + if (errors?.length) { + createFlash(__('Something went wrong. Please try again.')); + + return; + } + + const data = store.readQuery({ + query: getStateQuery, + variables: this.mergeRequestQueryVariables, + }); + data.project.mergeRequest.workInProgress = workInProgress; + data.project.mergeRequest.title = title; + store.writeQuery({ + query: getStateQuery, + data, + variables: this.mergeRequestQueryVariables, + }); + }, + optimisticResponse: { + // eslint-disable-next-line @gitlab/require-i18n-strings + __typename: 'Mutation', + mergeRequestSetWip: { + __typename: 'MergeRequestSetWipPayload', + errors: [], + mergeRequest: { + __typename: 'MergeRequest', + title: this.mr.title, + workInProgress: false, + }, + }, + }, + }) + .then(({ data: { mergeRequestSetWip: { mergeRequest: { title } } } }) => { createFlash(__('The merge request can now be merged.'), 'notice'); - $('.merge-request .detail-page-description .title').text(this.mr.title); + $('.merge-request .detail-page-description .title').text(title); }) - .catch(() => { + .catch(() => createFlash(__('Something went wrong. Please try again.'))) + .finally(() => { this.isMakingRequest = false; - createFlash(__('Something went wrong. Please try again.')); }); }, + handleRemoveWIP() { + if (this.glFeatures.mergeRequestWidgetGraphql) { + this.removeWipMutation(); + } else { + this.isMakingRequest = true; + this.service + .removeWIP() + .then(res => res.data) + .then(data => { + eventHub.$emit('UpdateWidgetData', data); + createFlash(__('The merge request can now be merged.'), 'notice'); + $('.merge-request .detail-page-description .title').text(this.mr.title); + }) + .catch(() => { + this.isMakingRequest = false; + createFlash(__('Something went wrong. Please try again.')); + }); + } + }, }, }; </script> <template> <div class="mr-widget-body media"> - <status-icon :show-disabled-button="Boolean(mr.removeWIPPath)" status="warning" /> - <div class="media-body space-children"> - <span class="bold"> - {{ __('This is a Work in Progress') }} - <i - v-tooltip - class="fa fa-question-circle" - :title="wipInfoTooltip" - :aria-label="wipInfoTooltip" - > - </i> - </span> - <gl-deprecated-button - v-if="mr.removeWIPPath" - size="sm" - variant="default" + <status-icon :show-disabled-button="canUpdate" status="warning" /> + <div class="media-body"> + <div class="gl-ml-3 float-left"> + <span class="gl-font-weight-bold"> + {{ __('This merge request is still a work in progress.') }} + </span> + <span class="gl-display-block text-muted">{{ + __("Draft merge requests can't be merged.") + }}</span> + </div> + <gl-button + v-if="canUpdate" + size="small" :disabled="isMakingRequest" :loading="isMakingRequest" - class="js-remove-wip" + class="js-remove-wip gl-ml-3" @click="handleRemoveWIP" > - {{ s__('mrWidget|Resolve WIP status') }} - </gl-deprecated-button> + {{ s__('mrWidget|Mark as ready') }} + </gl-button> </div> </div> </template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/terraform/mr_widget_terraform_container.vue b/app/assets/javascripts/vue_merge_request_widget/components/terraform/mr_widget_terraform_container.vue index f6e21dc1ec1..c7d9453a5c9 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/terraform/mr_widget_terraform_container.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/terraform/mr_widget_terraform_container.vue @@ -1,6 +1,6 @@ <script> -import { n__ } from '~/locale'; import { GlSkeletonLoading, GlSprintf } from '@gitlab/ui'; +import { n__ } from '~/locale'; import axios from '~/lib/utils/axios_utils'; import MrWidgetExpanableSection from '../mr_widget_expandable_section.vue'; import Poll from '~/lib/utils/poll'; diff --git a/app/assets/javascripts/vue_merge_request_widget/components/terraform/terraform_plan.vue b/app/assets/javascripts/vue_merge_request_widget/components/terraform/terraform_plan.vue index dc16d46dd8e..b74e036d9d9 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/terraform/terraform_plan.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/terraform/terraform_plan.vue @@ -1,6 +1,6 @@ <script> -import { s__ } from '~/locale'; import { GlIcon, GlLink, GlSprintf } from '@gitlab/ui'; +import { s__ } from '~/locale'; export default { name: 'TerraformPlan', diff --git a/app/assets/javascripts/vue_merge_request_widget/constants.js b/app/assets/javascripts/vue_merge_request_widget/constants.js index 1002bb728a0..77dfbf9d385 100644 --- a/app/assets/javascripts/vue_merge_request_widget/constants.js +++ b/app/assets/javascripts/vue_merge_request_widget/constants.js @@ -1,6 +1,9 @@ +import { s__ } from '~/locale'; + export const SUCCESS = 'success'; export const WARNING = 'warning'; export const DANGER = 'danger'; +export const INFO = 'info'; export const WARNING_MESSAGE_CLASS = 'warning_message'; export const DANGER_MESSAGE_CLASS = 'danger_message'; @@ -10,3 +13,15 @@ export const MTWPS_MERGE_STRATEGY = 'add_to_merge_train_when_pipeline_succeeds'; export const MT_MERGE_STRATEGY = 'merge_train'; export const AUTO_MERGE_STRATEGIES = [MWPS_MERGE_STRATEGY, MTWPS_MERGE_STRATEGY, MT_MERGE_STRATEGY]; + +// SP - "Suggest Pipelines" +export const SP_TRACK_LABEL = 'no_pipeline_noticed'; +export const SP_LINK_TRACK_EVENT = 'click_link'; +export const SP_SHOW_TRACK_EVENT = 'click_button'; +export const SP_LINK_TRACK_VALUE = 30; +export const SP_SHOW_TRACK_VALUE = 10; +export const SP_HELP_CONTENT = s__( + `mrWidget|Use %{linkStart}CI pipelines to test your code%{linkEnd} by simply adding a GitLab CI configuration file to your project. It only takes a minute to make your code more secure and robust.`, +); +export const SP_HELP_URL = 'https://about.gitlab.com/blog/2019/07/12/guide-to-ci-cd-pipelines/'; +export const SP_ICON_NAME = 'status_notfound'; diff --git a/app/assets/javascripts/vue_merge_request_widget/index.js b/app/assets/javascripts/vue_merge_request_widget/index.js index 068829912bf..87e56dfcbdf 100644 --- a/app/assets/javascripts/vue_merge_request_widget/index.js +++ b/app/assets/javascripts/vue_merge_request_widget/index.js @@ -1,7 +1,7 @@ import Vue from 'vue'; import MrWidgetOptions from 'ee_else_ce/vue_merge_request_widget/mr_widget_options.vue'; -import Translate from '../vue_shared/translate'; import VueApollo from 'vue-apollo'; +import Translate from '../vue_shared/translate'; import createDefaultClient from '~/lib/graphql'; Vue.use(Translate); diff --git a/app/assets/javascripts/vue_merge_request_widget/mixins/merge_request_query_variables.js b/app/assets/javascripts/vue_merge_request_widget/mixins/merge_request_query_variables.js new file mode 100644 index 00000000000..3a121908f36 --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/mixins/merge_request_query_variables.js @@ -0,0 +1,10 @@ +export default { + computed: { + mergeRequestQueryVariables() { + return { + projectPath: this.mr.targetProjectFullPath, + iid: `${this.mr.iid}`, + }; + }, + }, +}; diff --git a/app/assets/javascripts/vue_merge_request_widget/mixins/ready_to_merge.js b/app/assets/javascripts/vue_merge_request_widget/mixins/ready_to_merge.js index 319b6c333f4..dc8a6b56d58 100644 --- a/app/assets/javascripts/vue_merge_request_widget/mixins/ready_to_merge.js +++ b/app/assets/javascripts/vue_merge_request_widget/mixins/ready_to_merge.js @@ -32,5 +32,8 @@ export default { isMergeImmediatelyDangerous() { return false; }, + shouldRenderMergeTrainHelperText() { + return false; + }, }, }; diff --git a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue index cff85fe232d..36a883869f1 100644 --- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue +++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue @@ -7,7 +7,8 @@ import stateMaps from 'ee_else_ce/vue_merge_request_widget/stores/state_maps'; import { sprintf, s__, __ } from '~/locale'; import Project from '~/pages/projects/project'; import SmartInterval from '~/smart_interval'; -import createFlash from '../flash'; +import { deprecatedCreateFlash as createFlash } from '../flash'; +import mergeRequestQueryVariablesMixin from './mixins/merge_request_query_variables'; import Loading from './components/loading.vue'; import WidgetHeader from './components/mr_widget_header.vue'; import WidgetSuggestPipeline from './components/mr_widget_suggest_pipeline.vue'; @@ -42,6 +43,7 @@ import GroupedCodequalityReportsApp from '../reports/codequality_report/grouped_ import GroupedTestReportsApp from '../reports/components/grouped_test_reports_app.vue'; import { setFaviconOverlay } from '../lib/utils/common_utils'; import GroupedAccessibilityReportsApp from '../reports/accessibility_report/grouped_accessibility_reports_app.vue'; +import getStateQuery from './queries/get_state.query.graphql'; export default { el: '#js-vue-mr-widget', @@ -83,6 +85,27 @@ export default { GroupedAccessibilityReportsApp, MrWidgetApprovals, }, + apollo: { + state: { + query: getStateQuery, + manual: true, + pollInterval: 10 * 1000, + skip() { + return !this.mr || !window.gon?.features?.mergeRequestWidgetGraphql; + }, + variables() { + return this.mergeRequestQueryVariables; + }, + result({ + data: { + project: { mergeRequest }, + }, + }) { + this.mr.setGraphqlData(mergeRequest); + }, + }, + }, + mixins: [mergeRequestQueryVariablesMixin], props: { mrData: { type: Object, @@ -116,7 +139,12 @@ export default { return this.mr.hasCI || this.hasPipelineMustSucceedConflict; }, shouldSuggestPipelines() { - return gon.features?.suggestPipeline && !this.mr.hasCI && this.mr.mergeRequestAddCiConfigPath; + return ( + gon.features?.suggestPipeline && + !this.mr.hasCI && + this.mr.mergeRequestAddCiConfigPath && + !this.mr.isDismissedSuggestPipeline + ); }, shouldRenderCodeQuality() { return this.mr?.codeclimate?.head_path; @@ -374,6 +402,9 @@ export default { this.stopPolling(); }); }, + dismissSuggestPipelines() { + this.mr.isDismissedSuggestPipeline = true; + }, }, }; </script> @@ -382,10 +413,14 @@ export default { <mr-widget-header :mr="mr" /> <mr-widget-suggest-pipeline v-if="shouldSuggestPipelines" + data-testid="mr-suggest-pipeline" class="mr-widget-workflow" :pipeline-path="mr.mergeRequestAddCiConfigPath" :pipeline-svg-path="mr.pipelinesEmptySvgPath" :human-access="mr.humanAccess.toLowerCase()" + :user-callouts-path="mr.userCalloutsPath" + :user-callout-feature-id="mr.suggestPipelineFeatureId" + @dismiss="dismissSuggestPipelines" /> <mr-widget-pipeline-container v-if="shouldRenderPipelines" diff --git a/app/assets/javascripts/vue_merge_request_widget/queries/get_state.query.graphql b/app/assets/javascripts/vue_merge_request_widget/queries/get_state.query.graphql new file mode 100644 index 00000000000..488397e7735 --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/queries/get_state.query.graphql @@ -0,0 +1,8 @@ +query getState($projectPath: ID!, $iid: String!) { + project(fullPath: $projectPath) { + mergeRequest(iid: $iid) { + title + workInProgress + } + } +} diff --git a/app/assets/javascripts/vue_merge_request_widget/queries/states/work_in_progress.query.graphql b/app/assets/javascripts/vue_merge_request_widget/queries/states/work_in_progress.query.graphql new file mode 100644 index 00000000000..73e205ebf2b --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/queries/states/work_in_progress.query.graphql @@ -0,0 +1,9 @@ +query workInProgressQuery($projectPath: ID!, $iid: String!) { + project(fullPath: $projectPath) { + mergeRequest(iid: $iid) { + userPermissions { + updateMergeRequest + } + } + } +} diff --git a/app/assets/javascripts/vue_merge_request_widget/queries/toggle_wip.mutation.graphql b/app/assets/javascripts/vue_merge_request_widget/queries/toggle_wip.mutation.graphql new file mode 100644 index 00000000000..37abe5ddf3c --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/queries/toggle_wip.mutation.graphql @@ -0,0 +1,9 @@ +mutation toggleWIPStatus($projectPath: ID!, $iid: String!, $wip: Boolean!) { + mergeRequestSetWip(input: { projectPath: $projectPath, iid: $iid, wip: $wip }) { + mergeRequest { + title + workInProgress + } + errors + } +} diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/artifacts_list/actions.js b/app/assets/javascripts/vue_merge_request_widget/stores/artifacts_list/actions.js index 3648db795f5..419793977f0 100644 --- a/app/assets/javascripts/vue_merge_request_widget/stores/artifacts_list/actions.js +++ b/app/assets/javascripts/vue_merge_request_widget/stores/artifacts_list/actions.js @@ -69,6 +69,3 @@ export const receiveArtifactsSuccess = ({ commit }, response) => { }; export const receiveArtifactsError = ({ commit }) => commit(types.RECEIVE_ARTIFACTS_ERROR); - -// prevent babel-plugin-rewire from generating an invalid default during karma tests -export default () => {}; diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/artifacts_list/getters.js b/app/assets/javascripts/vue_merge_request_widget/stores/artifacts_list/getters.js index 8921637b93b..5215d210e1c 100644 --- a/app/assets/javascripts/vue_merge_request_widget/stores/artifacts_list/getters.js +++ b/app/assets/javascripts/vue_merge_request_widget/stores/artifacts_list/getters.js @@ -1,5 +1,6 @@ import { s__, n__ } from '~/locale'; +// eslint-disable-next-line import/prefer-default-export export const title = state => { if (state.isLoading) { return s__('BuildArtifacts|Loading artifacts'); @@ -11,6 +12,3 @@ export const title = state => { return n__('View exposed artifact', 'View %d exposed artifacts', state.artifacts.length); }; - -// prevent babel-plugin-rewire from generating an invalid default during karma tests -export default () => {}; diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js b/app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js index 44e8167d6a3..3bd512c89bf 100644 --- a/app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js +++ b/app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js @@ -1,21 +1,21 @@ import { stateKey } from './state_maps'; -export default function deviseState(data) { - if (data.project_archived) { +export default function deviseState() { + if (this.projectArchived) { return stateKey.archived; - } else if (data.branch_missing) { + } else if (this.branchMissing) { return stateKey.missingBranch; - } else if (!data.commits_count) { + } else if (!this.commitsCount) { return stateKey.nothingToMerge; } else if (this.mergeStatus === 'unchecked' || this.mergeStatus === 'checking') { return stateKey.checking; - } else if (data.has_conflicts) { + } else if (this.hasConflicts) { return stateKey.conflicts; } else if (this.shouldBeRebased) { return stateKey.rebase; } else if (this.onlyAllowMergeIfPipelineSucceeds && this.isPipelineFailed) { return stateKey.pipelineFailed; - } else if (data.work_in_progress) { + } else if (this.workInProgress) { return stateKey.workInProgress; } else if (this.hasMergeableDiscussionsState) { return stateKey.unresolvedDiscussions; diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js index 8b9799d9775..8c98ba1b023 100644 --- a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js +++ b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js @@ -60,6 +60,9 @@ export default class MergeRequestStore { this.rebaseInProgress = data.rebase_in_progress; this.mergeRequestDiffsPath = data.diffs_path; this.approvalsWidgetType = data.approvals_widget_type; + this.projectArchived = data.project_archived; + this.branchMissing = data.branch_missing; + this.hasConflicts = data.has_conflicts; if (data.issues_links) { const links = data.issues_links; @@ -90,7 +93,8 @@ export default class MergeRequestStore { this.ffOnlyEnabled = data.ff_only_enabled; this.shouldBeRebased = Boolean(data.should_be_rebased); this.isRemovingSourceBranch = this.isRemovingSourceBranch || false; - this.isOpen = data.state === 'opened'; + this.mergeRequestState = data.state; + this.isOpen = this.mergeRequestState === 'opened'; this.hasMergeableDiscussionsState = data.mergeable_discussions_state === false; this.isSHAMismatch = this.sha !== data.diff_head_sha; this.latestSHA = data.diff_head_sha; @@ -133,6 +137,10 @@ export default class MergeRequestStore { this.mergeCommitPath = data.merge_commit_path; this.canPushToSourceBranch = data.can_push_to_source_branch; + if (data.work_in_progress !== undefined) { + this.workInProgress = data.work_in_progress; + } + const currentUser = data.current_user; this.cherryPickInForkPath = currentUser.cherry_pick_in_fork_path; @@ -143,19 +151,25 @@ export default class MergeRequestStore { this.canCherryPickInCurrentMR = currentUser.can_cherry_pick_on_current_merge_request || false; this.canRevertInCurrentMR = currentUser.can_revert_on_current_merge_request || false; - this.setState(data); + this.setState(); + } + + setGraphqlData(data) { + this.workInProgress = data.workInProgress; + + this.setState(); } - setState(data) { + setState() { if (this.mergeOngoing) { this.state = 'merging'; return; } if (this.isOpen) { - this.state = getStateKey.call(this, data); + this.state = getStateKey.call(this); } else { - switch (data.state) { + switch (this.mergeRequestState) { case 'merged': this.state = 'merged'; break; @@ -194,6 +208,9 @@ export default class MergeRequestStore { this.pipelinesEmptySvgPath = data.pipelines_empty_svg_path; this.humanAccess = data.human_access; this.newPipelinePath = data.new_project_pipeline_path; + this.userCalloutsPath = data.user_callouts_path; + this.suggestPipelineFeatureId = data.suggest_pipeline_feature_id; + this.isDismissedSuggestPipeline = data.is_dismissed_suggest_pipeline; // codeclimate const blobPath = data.blob_path || {}; |