diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-10-01 15:10:14 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-10-01 15:10:14 +0300 |
commit | a27b8a5c104f492e4b0abac4c84385a615c4f6ba (patch) | |
tree | 3ec1a2cc06312b1999ce405905ab7881f68927d2 /app | |
parent | 635d82b15d83a88a2d073fbf3bb56f6ff626020f (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
40 files changed, 880 insertions, 121 deletions
diff --git a/app/assets/javascripts/commit/pipelines/pipelines_table.vue b/app/assets/javascripts/commit/pipelines/pipelines_table.vue index 2f4118c1717..188d958ba86 100644 --- a/app/assets/javascripts/commit/pipelines/pipelines_table.vue +++ b/app/assets/javascripts/commit/pipelines/pipelines_table.vue @@ -1,6 +1,5 @@ <script> import { GlButton, GlLoadingIcon, GlModal, GlLink } from '@gitlab/ui'; -import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils'; import PipelinesService from '~/pipelines/services/pipelines_service'; import PipelineStore from '~/pipelines/stores/pipelines_store'; import pipelinesMixin from '~/pipelines/mixins/pipelines'; @@ -126,16 +125,6 @@ export default { (latest.flags.detached_merge_request_pipeline || latest.flags.merge_request_pipeline) ); }, - /** - * When we are on Desktop and the button is visible - * we need to add a negative margin to the table - * to make it inline with the button - * - * @returns {Boolean} - */ - shouldAddNegativeMargin() { - return this.canRenderPipelineButton && bp.isDesktop(); - }, }, created() { this.service = new PipelinesService(this.endpoint); @@ -205,65 +194,76 @@ export default { /> <div v-else-if="shouldRenderTable" class="table-holder"> - <div v-if="canRenderPipelineButton" class="nav justify-content-end"> - <gl-button - variant="success" - class="js-run-mr-pipeline gl-mt-3 btn-wide-on-xs" - :disabled="state.isRunningMergeRequestPipeline" - @click="tryRunPipeline" - > - <gl-loading-icon v-if="state.isRunningMergeRequestPipeline" inline /> - {{ s__('Pipelines|Run Pipeline') }} - </gl-button> - - <gl-modal - :id="modalId" - ref="modal" - :modal-id="modalId" - :title="s__('Pipelines|Are you sure you want to run this pipeline?')" - :ok-title="s__('Pipelines|Run Pipeline')" - ok-variant="danger" - @ok="onClickRunPipeline" - > - <p> - {{ - s__( - 'Pipelines|This pipeline will run code originating from a forked project merge request. This means that the code can potentially have security considerations like exposing CI variables.', - ) - }} - </p> - <p> - {{ - s__( - "Pipelines|It is recommended the code is reviewed thoroughly before running this pipeline with the parent project's CI resource.", - ) - }} - </p> - <p> - {{ - s__( - 'Pipelines|If you are unsure, please ask a project maintainer to review it for you.', - ) - }} - </p> - <gl-link - href="/help/ci/merge_request_pipelines/index.html#run-pipelines-in-the-parent-project-for-merge-requests-from-a-forked-project" - target="_blank" - > - {{ s__('Pipelines|More Information') }} - </gl-link> - </gl-modal> - </div> + <gl-button + v-if="canRenderPipelineButton" + block + class="gl-mt-3 gl-mb-0 gl-display-md-none" + variant="success" + data-testid="run_pipeline_button_mobile" + :loading="state.isRunningMergeRequestPipeline" + @click="tryRunPipeline" + > + {{ s__('Pipelines|Run Pipeline') }} + </gl-button> <pipelines-table-component :pipelines="state.pipelines" :update-graph-dropdown="updateGraphDropdown" :auto-devops-help-path="autoDevopsHelpPath" :view-type="viewType" - :class="{ 'negative-margin-top': shouldAddNegativeMargin }" - /> + > + <template #table-header-actions> + <div v-if="canRenderPipelineButton" class="gl-text-right"> + <gl-button + variant="success" + data-testid="run_pipeline_button" + :loading="state.isRunningMergeRequestPipeline" + @click="tryRunPipeline" + > + {{ s__('Pipelines|Run Pipeline') }} + </gl-button> + </div> + </template> + </pipelines-table-component> </div> + <gl-modal + v-if="canRenderPipelineButton" + :id="modalId" + ref="modal" + :modal-id="modalId" + :title="s__('Pipelines|Are you sure you want to run this pipeline?')" + :ok-title="s__('Pipelines|Run Pipeline')" + ok-variant="danger" + @ok="onClickRunPipeline" + > + <p> + {{ + s__( + 'Pipelines|This pipeline will run code originating from a forked project merge request. This means that the code can potentially have security considerations like exposing CI variables.', + ) + }} + </p> + <p> + {{ + s__( + "Pipelines|It is recommended the code is reviewed thoroughly before running this pipeline with the parent project's CI resource.", + ) + }} + </p> + <p> + {{ + s__('Pipelines|If you are unsure, please ask a project maintainer to review it for you.') + }} + </p> + <gl-link + href="/help/ci/merge_request_pipelines/index.html#run-pipelines-in-the-parent-project-for-merge-requests-from-a-forked-project" + target="_blank" + > + {{ s__('Pipelines|More Information') }} + </gl-link> + </gl-modal> + <table-pagination v-if="shouldRenderPagination" :change="onChangePage" diff --git a/app/assets/javascripts/diffs/components/compare_dropdown_layout.vue b/app/assets/javascripts/diffs/components/compare_dropdown_layout.vue index 8263e938e69..adef5d94624 100644 --- a/app/assets/javascripts/diffs/components/compare_dropdown_layout.vue +++ b/app/assets/javascripts/diffs/components/compare_dropdown_layout.vue @@ -32,7 +32,7 @@ export default { <gl-icon :size="12" name="angle-down" class="position-absolute" /> </a> <div class="dropdown-menu dropdown-select dropdown-menu-selectable"> - <div class="dropdown-content"> + <div class="dropdown-content" data-qa-selector="dropdown_content"> <ul> <li v-for="version in versions" :key="version.id"> <a :class="{ 'is-active': version.selected }" :href="version.href"> diff --git a/app/assets/javascripts/diffs/components/compare_versions.vue b/app/assets/javascripts/diffs/components/compare_versions.vue index b94874c5644..b1ebd8e6ebc 100644 --- a/app/assets/javascripts/diffs/components/compare_versions.vue +++ b/app/assets/javascripts/diffs/components/compare_versions.vue @@ -100,6 +100,7 @@ export default { <compare-dropdown-layout :versions="diffCompareDropdownTargetVersions" class="mr-version-compare-dropdown" + data-qa-selector="target_version_dropdown" /> </template> <template #source> diff --git a/app/assets/javascripts/diffs/components/diff_file_header.vue b/app/assets/javascripts/diffs/components/diff_file_header.vue index e403fc1f432..561ba0bd316 100644 --- a/app/assets/javascripts/diffs/components/diff_file_header.vue +++ b/app/assets/javascripts/diffs/components/diff_file_header.vue @@ -245,7 +245,14 @@ export default { ></strong> </span> - <strong v-else v-gl-tooltip :title="filePath" class="file-title-name" data-container="body"> + <strong + v-else + v-gl-tooltip + :title="filePath" + class="file-title-name" + data-container="body" + data-qa-selector="file_name_content" + > {{ filePath }} </strong> </a> diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index 95da3ce5d7e..d60f949c49d 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -39,6 +39,8 @@ import initPersistentUserCallouts from './persistent_user_callouts'; import { initUserTracking, initDefaultTrackers } from './tracking'; import { __ } from './locale'; +import * as tooltips from '~/tooltips'; + import 'ee_else_ce/main_ee'; applyGitLabUIConfig(); @@ -77,7 +79,7 @@ document.addEventListener('beforeunload', () => { // Unbind scroll events $(document).off('scroll'); // Close any open tooltips - $('.has-tooltip, [data-toggle="tooltip"]').tooltip('dispose'); + tooltips.dispose(document.querySelectorAll('.has-tooltip, [data-toggle="tooltip"]')); // Close any open popover $('[data-toggle="popover"]').popover('dispose'); }); @@ -133,8 +135,10 @@ function deferredInitialisation() { addSelectOnFocusBehaviour('.js-select-on-focus'); $('.remove-row').on('ajax:success', function removeRowAjaxSuccessCallback() { + tooltips.dispose(this); + + // eslint-disable-next-line no-jquery/no-fade $(this) - .tooltip('dispose') .closest('li') .fadeOut(); }); @@ -154,7 +158,7 @@ function deferredInitialisation() { const delay = glTooltipDelay ? JSON.parse(glTooltipDelay) : 0; // Initialize tooltips - $body.tooltip({ + tooltips.initTooltips({ selector: '.has-tooltip, [data-toggle="tooltip"]', trigger: 'hover', boundary: 'viewport', diff --git a/app/assets/javascripts/merge_request.js b/app/assets/javascripts/merge_request.js index 79a4c3700ef..fe4e2cee69f 100644 --- a/app/assets/javascripts/merge_request.js +++ b/app/assets/javascripts/merge_request.js @@ -3,11 +3,12 @@ import $ from 'jquery'; import axios from './lib/utils/axios_utils'; import { __ } from '~/locale'; +import eventHub from '~/vue_merge_request_widget/event_hub'; import { deprecatedCreateFlash as createFlash } from '~/flash'; import TaskList from './task_list'; import MergeRequestTabs from './merge_request_tabs'; -import IssuablesHelper from './helpers/issuables_helper'; import { addDelimiter } from './lib/utils/text_utility'; +import { getParameterValues, setUrlParams } from './lib/utils/url_utility'; function MergeRequest(opts) { // Initialize MergeRequest behavior @@ -23,7 +24,6 @@ function MergeRequest(opts) { this.initTabs(); this.initMRBtnListeners(); this.initCommitMessageListeners(); - this.closeReopenReportToggle = IssuablesHelper.initCloseReopenReport(); if ($('.description.js-task-list-container').length) { this.taskList = new TaskList({ @@ -66,13 +66,38 @@ MergeRequest.prototype.showAllCommits = function() { MergeRequest.prototype.initMRBtnListeners = function() { const _this = this; + const draftToggles = document.querySelectorAll('.js-draft-toggle-button'); - $('.report-abuse-link').on('click', e => { - // this is needed because of the implementation of - // the dropdown toggle and Report Abuse needing to be - // linked to another page. - e.stopPropagation(); - }); + if (draftToggles.length) { + draftToggles.forEach(draftToggle => { + draftToggle.addEventListener('click', e => { + e.preventDefault(); + e.stopImmediatePropagation(); + + const url = draftToggle.href; + const wipEvent = getParameterValues('merge_request[wip_event]', url)[0]; + const mobileDropdown = draftToggle.closest('.dropdown.show'); + + if (mobileDropdown) { + $(mobileDropdown.firstElementChild).dropdown('toggle'); + } + + draftToggle.setAttribute('disabled', 'disabled'); + + axios + .put(draftToggle.href, null, { params: { format: 'json' } }) + .then(({ data }) => { + draftToggle.removeAttribute('disabled'); + eventHub.$emit('MRWidgetUpdateRequested'); + MergeRequest.toggleDraftStatus(data.title, wipEvent === 'unwip'); + }) + .catch(() => { + draftToggle.removeAttribute('disabled'); + createFlash(__('Something went wrong. Please try again.')); + }); + }); + }); + } return $('.btn-close, .btn-reopen').on('click', function(e) { const $this = $(this); @@ -89,8 +114,6 @@ MergeRequest.prototype.initMRBtnListeners = function() { return; } - if (this.closeReopenReportToggle) this.closeReopenReportToggle.setDisable(); - if (shouldSubmit) { if ($this.hasClass('btn-comment-and-close') || $this.hasClass('btn-comment-and-reopen')) { e.preventDefault(); @@ -151,14 +174,35 @@ MergeRequest.hideCloseButton = function() { const closeDropdownItem = el.querySelector('li.close-item'); if (closeDropdownItem) { closeDropdownItem.classList.add('hidden'); - // Selects the next dropdown item - el.querySelector('li.report-item').click(); - } else { - // No dropdown just hide the Close button - el.querySelector('.btn-close').classList.add('hidden'); } // Dropdown for mobile screen el.querySelector('li.js-close-item').classList.add('hidden'); }; +MergeRequest.toggleDraftStatus = function(title, isReady) { + if (isReady) { + createFlash(__('The merge request can now be merged.'), 'notice'); + } + const titleEl = document.querySelector('.merge-request .detail-page-description .title'); + + if (titleEl) { + titleEl.textContent = title; + } + + const draftToggles = document.querySelectorAll('.js-draft-toggle-button'); + + if (draftToggles.length) { + draftToggles.forEach(el => { + const draftToggle = el; + const url = setUrlParams( + { 'merge_request[wip_event]': isReady ? 'wip' : 'unwip' }, + draftToggle.href, + ); + + draftToggle.setAttribute('href', url); + draftToggle.textContent = isReady ? __('Mark as draft') : __('Mark as ready'); + }); + } +}; + export default MergeRequest; diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table.vue index b8112149778..6c60594efca 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table.vue @@ -91,6 +91,10 @@ export default { <div class="table-section section-15 js-pipeline-stages pipeline-stages" role="rowheader"> {{ s__('Pipeline|Stages') }} </div> + <div class="table-section section-15" role="rowheader"></div> + <div class="table-section section-20" role="rowheader"> + <slot name="table-header-actions"></slot> + </div> </div> <pipelines-table-row-component v-for="model in pipelines" diff --git a/app/assets/javascripts/projects/default_project_templates.js b/app/assets/javascripts/projects/default_project_templates.js index 2d321ead33e..a6019e9c01b 100644 --- a/app/assets/javascripts/projects/default_project_templates.js +++ b/app/assets/javascripts/projects/default_project_templates.js @@ -57,6 +57,10 @@ export default { text: s__('ProjectTemplates|Static Site Editor/Middleman'), icon: '.template-option .icon-sse_middleman', }, + gitpod_spring_petclinic: { + text: s__('ProjectTemplates|Gitpod/Spring Petclinic'), + icon: '.template-option .icon-gitpod_spring_petclinic', + }, nfhugo: { text: s__('ProjectTemplates|Netlify/Hugo'), icon: '.template-option .icon-nfhugo', diff --git a/app/assets/javascripts/sidebar/components/reviewers/collapsed_reviewer.vue b/app/assets/javascripts/sidebar/components/reviewers/collapsed_reviewer.vue new file mode 100644 index 00000000000..6de926e0ff9 --- /dev/null +++ b/app/assets/javascripts/sidebar/components/reviewers/collapsed_reviewer.vue @@ -0,0 +1,24 @@ +<script> +// NOTE! For the first iteration, we are simply copying the implementation of Assignees +// It will soon be overhauled in Issue https://gitlab.com/gitlab-org/gitlab/-/issues/233736 +import ReviewerAvatar from './reviewer_avatar.vue'; + +export default { + components: { + ReviewerAvatar, + }, + props: { + user: { + type: Object, + required: true, + }, + }, +}; +</script> + +<template> + <button type="button" class="btn-link"> + <reviewer-avatar :user="user" :img-size="24" /> + <span class="author"> {{ user.name }} </span> + </button> +</template> diff --git a/app/assets/javascripts/sidebar/components/reviewers/collapsed_reviewer_list.vue b/app/assets/javascripts/sidebar/components/reviewers/collapsed_reviewer_list.vue new file mode 100644 index 00000000000..45707c18f7b --- /dev/null +++ b/app/assets/javascripts/sidebar/components/reviewers/collapsed_reviewer_list.vue @@ -0,0 +1,107 @@ +<script> +// NOTE! For the first iteration, we are simply copying the implementation of Assignees +// It will soon be overhauled in Issue https://gitlab.com/gitlab-org/gitlab/-/issues/233736 +import { GlIcon, GlTooltipDirective } from '@gitlab/ui'; +import { __, sprintf } from '~/locale'; +import CollapsedReviewer from './collapsed_reviewer.vue'; + +const DEFAULT_MAX_COUNTER = 99; +const DEFAULT_RENDER_COUNT = 5; + +export default { + directives: { + GlTooltip: GlTooltipDirective, + }, + components: { + CollapsedReviewer, + GlIcon, + }, + props: { + users: { + type: Array, + required: true, + }, + }, + computed: { + hasNoUsers() { + return !this.users.length; + }, + hasMoreThanOneReviewer() { + return this.users.length > 1; + }, + hasMoreThanTwoReviewers() { + return this.users.length > 2; + }, + allReviewersCanMerge() { + return this.users.every(user => user.can_merge); + }, + sidebarAvatarCounter() { + if (this.users.length > DEFAULT_MAX_COUNTER) { + return `${DEFAULT_MAX_COUNTER}+`; + } + + return `+${this.users.length - 1}`; + }, + collapsedUsers() { + const collapsedLength = this.hasMoreThanTwoReviewers ? 1 : this.users.length; + + return this.users.slice(0, collapsedLength); + }, + tooltipTitleMergeStatus() { + const mergeLength = this.users.filter(u => u.can_merge).length; + + if (mergeLength === this.users.length) { + return ''; + } else if (mergeLength > 0) { + return sprintf(__('%{mergeLength}/%{usersLength} can merge'), { + mergeLength, + usersLength: this.users.length, + }); + } + + return this.users.length === 1 ? __('cannot merge') : __('no one can merge'); + }, + tooltipTitle() { + const maxRender = Math.min(DEFAULT_RENDER_COUNT, this.users.length); + const renderUsers = this.users.slice(0, maxRender); + const names = renderUsers.map(u => u.name); + + if (!this.users.length) { + return __('Reviewer(s)'); + } + + if (this.users.length > names.length) { + names.push(sprintf(__('+ %{amount} more'), { amount: this.users.length - names.length })); + } + + const text = names.join(', '); + + return this.tooltipTitleMergeStatus ? `${text} (${this.tooltipTitleMergeStatus})` : text; + }, + + tooltipOptions() { + return { container: 'body', placement: 'left', boundary: 'viewport' }; + }, + }, +}; +</script> + +<template> + <div + v-gl-tooltip="tooltipOptions" + :class="{ 'multiple-users': hasMoreThanOneReviewer }" + :title="tooltipTitle" + class="sidebar-collapsed-icon sidebar-collapsed-user" + > + <gl-icon v-if="hasNoUsers" name="user" :aria-label="__('None')" /> + <collapsed-reviewer v-for="user in collapsedUsers" :key="user.id" :user="user" /> + <button v-if="hasMoreThanTwoReviewers" class="btn-link" type="button"> + <span class="avatar-counter sidebar-avatar-counter"> {{ sidebarAvatarCounter }} </span> + <i + v-if="!allReviewersCanMerge" + aria-hidden="true" + class="fa fa-exclamation-triangle merge-icon" + ></i> + </button> + </div> +</template> diff --git a/app/assets/javascripts/sidebar/components/reviewers/reviewer_avatar.vue b/app/assets/javascripts/sidebar/components/reviewers/reviewer_avatar.vue new file mode 100644 index 00000000000..9fa3fa38eac --- /dev/null +++ b/app/assets/javascripts/sidebar/components/reviewers/reviewer_avatar.vue @@ -0,0 +1,43 @@ +<script> +// NOTE! For the first iteration, we are simply copying the implementation of Assignees +// It will soon be overhauled in Issue https://gitlab.com/gitlab-org/gitlab/-/issues/233736 +import { __, sprintf } from '~/locale'; + +export default { + props: { + user: { + type: Object, + required: true, + }, + imgSize: { + type: Number, + required: true, + }, + }, + computed: { + reviewerAlt() { + return sprintf(__("%{userName}'s avatar"), { userName: this.user.name }); + }, + avatarUrl() { + return this.user.avatar || this.user.avatar_url || gon.default_avatar_url; + }, + hasMergeIcon() { + return !this.user.can_merge; + }, + }, +}; +</script> + +<template> + <span class="position-relative"> + <img + :alt="reviewerAlt" + :src="avatarUrl" + :width="imgSize" + :class="`s${imgSize}`" + class="avatar avatar-inline m-0" + data-qa-selector="avatar_image" + /> + <i v-if="hasMergeIcon" aria-hidden="true" class="fa fa-exclamation-triangle merge-icon"></i> + </span> +</template> diff --git a/app/assets/javascripts/sidebar/components/reviewers/reviewer_avatar_link.vue b/app/assets/javascripts/sidebar/components/reviewers/reviewer_avatar_link.vue new file mode 100644 index 00000000000..b1b04564a62 --- /dev/null +++ b/app/assets/javascripts/sidebar/components/reviewers/reviewer_avatar_link.vue @@ -0,0 +1,84 @@ +<script> +// NOTE! For the first iteration, we are simply copying the implementation of Assignees +// It will soon be overhauled in Issue https://gitlab.com/gitlab-org/gitlab/-/issues/233736 +import { GlTooltipDirective, GlLink } from '@gitlab/ui'; +import { __, sprintf } from '~/locale'; +import ReviewerAvatar from './reviewer_avatar.vue'; + +export default { + components: { + ReviewerAvatar, + GlLink, + }, + directives: { + GlTooltip: GlTooltipDirective, + }, + props: { + user: { + type: Object, + required: true, + }, + rootPath: { + type: String, + required: true, + }, + tooltipPlacement: { + type: String, + default: 'bottom', + required: false, + }, + tooltipHasName: { + type: Boolean, + default: true, + required: false, + }, + issuableType: { + type: String, + default: 'issue', + required: false, + }, + }, + computed: { + cannotMerge() { + return this.issuableType === 'merge_request' && !this.user.can_merge; + }, + tooltipTitle() { + if (this.cannotMerge && this.tooltipHasName) { + return sprintf(__('%{userName} (cannot merge)'), { userName: this.user.name }); + } else if (this.cannotMerge) { + return __('Cannot merge'); + } else if (this.tooltipHasName) { + return this.user.name; + } + + return ''; + }, + tooltipOption() { + return { + container: 'body', + placement: this.tooltipPlacement, + boundary: 'viewport', + }; + }, + reviewerUrl() { + return this.user.web_url; + }, + }, +}; +</script> + +<template> + <!-- must be `d-inline-block` or parent flex-basis causes width issues --> + <gl-link + v-gl-tooltip="tooltipOption" + :href="reviewerUrl" + :title="tooltipTitle" + class="d-inline-block" + > + <!-- use d-flex so that slot can be appropriately styled --> + <span class="d-flex"> + <reviewer-avatar :user="user" :img-size="32" :issuable-type="issuableType" /> + <slot :user="user"></slot> + </span> + </gl-link> +</template> diff --git a/app/assets/javascripts/sidebar/components/reviewers/reviewer_title.vue b/app/assets/javascripts/sidebar/components/reviewers/reviewer_title.vue new file mode 100644 index 00000000000..437f28907fd --- /dev/null +++ b/app/assets/javascripts/sidebar/components/reviewers/reviewer_title.vue @@ -0,0 +1,64 @@ +<script> +// NOTE! For the first iteration, we are simply copying the implementation of Assignees +// It will soon be overhauled in Issue https://gitlab.com/gitlab-org/gitlab/-/issues/233736 +import { GlLoadingIcon } from '@gitlab/ui'; +import { n__ } from '~/locale'; + +export default { + name: 'ReviewerTitle', + components: { + GlLoadingIcon, + }, + props: { + loading: { + type: Boolean, + required: false, + default: false, + }, + numberOfReviewers: { + type: Number, + required: true, + }, + editable: { + type: Boolean, + required: true, + }, + showToggle: { + type: Boolean, + required: false, + default: false, + }, + }, + computed: { + reviewerTitle() { + const reviewers = this.numberOfReviewers; + return n__('Reviewer', `%d Reviewers`, reviewers); + }, + }, +}; +</script> +<template> + <div class="title hide-collapsed"> + {{ reviewerTitle }} + <gl-loading-icon v-if="loading" inline class="align-bottom" /> + <a + v-if="editable" + class="js-sidebar-dropdown-toggle edit-link float-right" + href="#" + data-track-event="click_edit_button" + data-track-label="right_sidebar" + data-track-property="reviewer" + > + {{ __('Edit') }} + </a> + <a + v-if="showToggle" + :aria-label="__('Toggle sidebar')" + class="gutter-toggle float-right js-sidebar-toggle" + href="#" + role="button" + > + <i aria-hidden="true" data-hidden="true" class="fa fa-angle-double-right"></i> + </a> + </div> +</template> diff --git a/app/assets/javascripts/sidebar/components/reviewers/reviewers.vue b/app/assets/javascripts/sidebar/components/reviewers/reviewers.vue new file mode 100644 index 00000000000..6a3d88f6385 --- /dev/null +++ b/app/assets/javascripts/sidebar/components/reviewers/reviewers.vue @@ -0,0 +1,72 @@ +<script> +// NOTE! For the first iteration, we are simply copying the implementation of Assignees +// It will soon be overhauled in Issue https://gitlab.com/gitlab-org/gitlab/-/issues/233736 +import CollapsedReviewerList from './collapsed_reviewer_list.vue'; +import UncollapsedReviewerList from './uncollapsed_reviewer_list.vue'; + +export default { + // name: 'Reviewers' is a false positive: https://gitlab.com/gitlab-org/frontend/eslint-plugin-i18n/issues/26#possible-false-positives + // eslint-disable-next-line @gitlab/require-i18n-strings + name: 'Reviewers', + components: { + CollapsedReviewerList, + UncollapsedReviewerList, + }, + props: { + rootPath: { + type: String, + required: true, + }, + users: { + type: Array, + required: true, + }, + editable: { + type: Boolean, + required: true, + }, + issuableType: { + type: String, + required: false, + default: 'issue', + }, + }, + computed: { + hasNoUsers() { + return !this.users.length; + }, + sortedReviewers() { + const canMergeUsers = this.users.filter(user => user.can_merge); + const canNotMergeUsers = this.users.filter(user => !user.can_merge); + + return [...canMergeUsers, ...canNotMergeUsers]; + }, + }, + methods: { + assignSelf() { + this.$emit('assign-self'); + }, + }, +}; +</script> + +<template> + <div> + <collapsed-reviewer-list :users="sortedReviewers" :issuable-type="issuableType" /> + + <div class="value hide-collapsed"> + <template v-if="hasNoUsers"> + <span class="assign-yourself no-value qa-assign-yourself"> + {{ __('None') }} + </span> + </template> + + <uncollapsed-reviewer-list + v-else + :users="sortedReviewers" + :root-path="rootPath" + :issuable-type="issuableType" + /> + </div> + </div> +</template> diff --git a/app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers.vue b/app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers.vue new file mode 100644 index 00000000000..5d8a2e6fa65 --- /dev/null +++ b/app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers.vue @@ -0,0 +1,107 @@ +<script> +// NOTE! For the first iteration, we are simply copying the implementation of Assignees +// It will soon be overhauled in Issue https://gitlab.com/gitlab-org/gitlab/-/issues/233736 +import { deprecatedCreateFlash as Flash } from '~/flash'; +import eventHub from '~/sidebar/event_hub'; +import Store from '~/sidebar/stores/sidebar_store'; +import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; +import ReviewerTitle from './reviewer_title.vue'; +import Reviewers from './reviewers.vue'; +import { __ } from '~/locale'; + +export default { + name: 'SidebarReviewers', + components: { + ReviewerTitle, + Reviewers, + }, + mixins: [glFeatureFlagsMixin()], + props: { + mediator: { + type: Object, + required: true, + }, + field: { + type: String, + required: true, + }, + signedIn: { + type: Boolean, + required: false, + default: false, + }, + issuableType: { + type: String, + required: false, + default: 'issue', + }, + issuableIid: { + type: String, + required: true, + }, + projectPath: { + type: String, + required: true, + }, + }, + data() { + return { + store: new Store(), + loading: false, + }; + }, + created() { + this.removeReviewer = this.store.removeReviewer.bind(this.store); + this.addReviewer = this.store.addReviewer.bind(this.store); + this.removeAllReviewers = this.store.removeAllReviewers.bind(this.store); + + // Get events from deprecatedJQueryDropdown + eventHub.$on('sidebar.removeReviewer', this.removeReviewer); + eventHub.$on('sidebar.addReviewer', this.addReviewer); + eventHub.$on('sidebar.removeAllReviewers', this.removeAllReviewers); + eventHub.$on('sidebar.saveReviewers', this.saveReviewers); + }, + beforeDestroy() { + eventHub.$off('sidebar.removeReviewer', this.removeReviewer); + eventHub.$off('sidebar.addReviewer', this.addReviewer); + eventHub.$off('sidebar.removeAllReviewers', this.removeAllReviewers); + eventHub.$off('sidebar.saveReviewers', this.saveReviewers); + }, + methods: { + saveReviewers() { + this.loading = true; + + this.mediator + .saveReviewers(this.field) + .then(() => { + this.loading = false; + // Uncomment once this issue has been addressed > https://gitlab.com/gitlab-org/gitlab/-/issues/237922 + // refreshUserMergeRequestCounts(); + }) + .catch(() => { + this.loading = false; + return new Flash(__('Error occurred when saving reviewers')); + }); + }, + }, +}; +</script> + +<template> + <div> + <reviewer-title + :number-of-reviewers="store.reviewers.length" + :loading="loading || store.isFetching.reviewers" + :editable="store.editable" + :show-toggle="!signedIn" + /> + <reviewers + v-if="!store.isFetching.reviewers" + :root-path="store.rootPath" + :users="store.reviewers" + :editable="store.editable" + :issuable-type="issuableType" + class="value" + /> + </div> +</template> diff --git a/app/assets/javascripts/sidebar/components/reviewers/uncollapsed_reviewer_list.vue b/app/assets/javascripts/sidebar/components/reviewers/uncollapsed_reviewer_list.vue new file mode 100644 index 00000000000..2ae4a114b36 --- /dev/null +++ b/app/assets/javascripts/sidebar/components/reviewers/uncollapsed_reviewer_list.vue @@ -0,0 +1,103 @@ +<script> +// NOTE! For the first iteration, we are simply copying the implementation of Assignees +// It will soon be overhauled in Issue https://gitlab.com/gitlab-org/gitlab/-/issues/233736 +import { __, sprintf } from '~/locale'; +import ReviewerAvatarLink from './reviewer_avatar_link.vue'; + +const DEFAULT_RENDER_COUNT = 5; + +export default { + components: { + ReviewerAvatarLink, + }, + props: { + users: { + type: Array, + required: true, + }, + rootPath: { + type: String, + required: true, + }, + issuableType: { + type: String, + required: false, + default: 'issue', + }, + }, + data() { + return { + showLess: true, + }; + }, + computed: { + firstUser() { + return this.users[0]; + }, + hasOneUser() { + return this.users.length === 1; + }, + hiddenReviewersLabel() { + const { numberOfHiddenReviewers } = this; + return sprintf(__('+ %{numberOfHiddenReviewers} more'), { numberOfHiddenReviewers }); + }, + renderShowMoreSection() { + return this.users.length > DEFAULT_RENDER_COUNT; + }, + numberOfHiddenReviewers() { + return this.users.length - DEFAULT_RENDER_COUNT; + }, + uncollapsedUsers() { + const uncollapsedLength = this.showLess + ? Math.min(this.users.length, DEFAULT_RENDER_COUNT) + : this.users.length; + return this.showLess ? this.users.slice(0, uncollapsedLength) : this.users; + }, + username() { + return `@${this.firstUser.username}`; + }, + }, + methods: { + toggleShowLess() { + this.showLess = !this.showLess; + }, + }, +}; +</script> + +<template> + <reviewer-avatar-link + v-if="hasOneUser" + #default="{ user }" + tooltip-placement="left" + :tooltip-has-name="false" + :user="firstUser" + :root-path="rootPath" + :issuable-type="issuableType" + > + <div class="ml-2"> + <span class="author"> {{ user.name }} </span> + <span class="username"> {{ username }} </span> + </div> + </reviewer-avatar-link> + <div v-else> + <div class="user-list"> + <div v-for="user in uncollapsedUsers" :key="user.id" class="user-item"> + <reviewer-avatar-link :user="user" :root-path="rootPath" :issuable-type="issuableType" /> + </div> + </div> + <div v-if="renderShowMoreSection" class="user-list-more"> + <button + type="button" + class="btn-link" + data-qa-selector="more_reviewers_link" + @click="toggleShowLess" + > + <template v-if="showLess"> + {{ hiddenReviewersLabel }} + </template> + <template v-else>{{ __('- show less') }}</template> + </button> + </div> + </div> +</template> diff --git a/app/assets/javascripts/sidebar/mount_sidebar.js b/app/assets/javascripts/sidebar/mount_sidebar.js index 047d9a4d0e8..ffc7f2c07ba 100644 --- a/app/assets/javascripts/sidebar/mount_sidebar.js +++ b/app/assets/javascripts/sidebar/mount_sidebar.js @@ -5,6 +5,7 @@ import Vuex from 'vuex'; import SidebarTimeTracking from './components/time_tracking/sidebar_time_tracking.vue'; import SidebarAssignees from './components/assignees/sidebar_assignees.vue'; import SidebarLabels from './components/labels/sidebar_labels.vue'; +import SidebarReviewers from './components/reviewers/sidebar_reviewers.vue'; import ConfidentialIssueSidebar from './components/confidential/confidential_issue_sidebar.vue'; import SidebarMoveIssue from './lib/sidebar_move_issue'; import IssuableLockForm from './components/lock/issuable_lock_form.vue'; @@ -56,6 +57,36 @@ function mountAssigneesComponent(mediator) { }); } +function mountReviewersComponent(mediator) { + const el = document.getElementById('js-vue-sidebar-reviewers'); + const apolloProvider = new VueApollo({ + defaultClient: createDefaultClient(), + }); + + if (!el) return; + + const { iid, fullPath } = getSidebarOptions(); + // eslint-disable-next-line no-new + new Vue({ + el, + apolloProvider, + components: { + SidebarReviewers, + }, + render: createElement => + createElement('sidebar-reviewers', { + props: { + mediator, + issuableIid: String(iid), + projectPath: fullPath, + field: el.dataset.field, + signedIn: el.hasAttribute('data-signed-in'), + issuableType: isInIssuePage() ? 'issue' : 'merge_request', + }, + }), + }); +} + export function mountSidebarLabels() { const el = document.querySelector('.js-sidebar-labels'); @@ -245,6 +276,7 @@ function mountSeverityComponent() { export function mountSidebar(mediator) { mountAssigneesComponent(mediator); + mountReviewersComponent(mediator); mountConfidentialComponent(mediator); mountLockComponent(); mountParticipantsComponent(mediator); diff --git a/app/assets/javascripts/tooltips/index.js b/app/assets/javascripts/tooltips/index.js index cfbd88d6c40..debb36dc53f 100644 --- a/app/assets/javascripts/tooltips/index.js +++ b/app/assets/javascripts/tooltips/index.js @@ -58,6 +58,8 @@ const applyToElements = (elements, handler) => toArray(elements).forEach(handler const invokeBootstrapApi = (elements, method) => { if (isFunction(elements.tooltip)) { + elements.tooltip(method); + } else { jQuery(elements).tooltip(method); } }; 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 61cc950f058..be9d37e4531 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 @@ -3,6 +3,7 @@ import $ from 'jquery'; import { GlButton } from '@gitlab/ui'; import { __ } from '~/locale'; import { deprecatedCreateFlash as createFlash } from '~/flash'; +import MergeRequest from '~/merge_request'; 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'; @@ -128,8 +129,7 @@ export default { .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); + MergeRequest.toggleDraftStatus(this.mr.title, true); }) .catch(() => { this.isMakingRequest = false; diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view.vue index 01d3ab1033c..c8dee81d746 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view.vue @@ -1,6 +1,7 @@ <script> import { mapState, mapGetters, mapActions } from 'vuex'; import { GlLoadingIcon, GlButton, GlSearchBoxByType, GlLink } from '@gitlab/ui'; +import fuzzaldrinPlus from 'fuzzaldrin-plus'; import { UP_KEY_CODE, DOWN_KEY_CODE, ENTER_KEY_CODE, ESC_KEY_CODE } from '~/lib/utils/keycodes'; import SmartVirtualList from '~/vue_shared/components/smart_virtual_list.vue'; @@ -39,9 +40,9 @@ export default { ...mapGetters(['selectedLabelsList', 'isDropdownVariantSidebar', 'isDropdownVariantEmbedded']), visibleLabels() { if (this.searchKey) { - return this.labels.filter(label => - label.title.toLowerCase().includes(this.searchKey.toLowerCase()), - ); + return fuzzaldrinPlus.filter(this.labels, this.searchKey, { + key: ['title'], + }); } return this.labels; }, diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss index a9c1652d00d..a8cc685d880 100644 --- a/app/assets/stylesheets/framework/buttons.scss +++ b/app/assets/stylesheets/framework/buttons.scss @@ -417,12 +417,6 @@ } } -@include media-breakpoint-down(xs) { - .btn-wide-on-xs { - width: 100%; - } -} - .btn-blank { padding: 0; background: transparent; diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 4dcc6cdb649..66267d8a8bc 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -819,7 +819,6 @@ $pipeline-dropdown-line-height: 20px; $pipeline-dropdown-status-icon-size: 18px; $ci-action-dropdown-button-size: 24px; $ci-action-dropdown-svg-size: 12px; -$pipelines-table-header-height: 40px; /* CI variable lists diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index 305022058a7..7097c2b10c4 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -117,7 +117,8 @@ } } -.assignee { +.assignee, +.reviewer { .merge-icon { color: $orange-400; position: absolute; diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index 9ae54aa9cdf..d382fc6241f 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -26,10 +26,6 @@ } .pipelines { - .negative-margin-top { - margin-top: -$pipelines-table-header-height; - } - .stage { max-width: 90px; width: 90px; diff --git a/app/controllers/concerns/multiple_boards_actions.rb b/app/controllers/concerns/multiple_boards_actions.rb index 95a6800f55c..370b8c72bfe 100644 --- a/app/controllers/concerns/multiple_boards_actions.rb +++ b/app/controllers/concerns/multiple_boards_actions.rb @@ -21,11 +21,13 @@ module MultipleBoardsActions end def create - board = Boards::CreateService.new(parent, current_user, board_params).execute + response = Boards::CreateService.new(parent, current_user, board_params).execute respond_to do |format| format.json do - if board.persisted? + board = response.payload + + if response.success? extra_json = { board_path: board_path(board) } render json: serialize_as_json(board).merge(extra_json) else diff --git a/app/controllers/projects/merge_requests/application_controller.rb b/app/controllers/projects/merge_requests/application_controller.rb index 921da788ad2..7b4024ed79c 100644 --- a/app/controllers/projects/merge_requests/application_controller.rb +++ b/app/controllers/projects/merge_requests/application_controller.rb @@ -35,6 +35,7 @@ class Projects::MergeRequests::ApplicationController < Projects::ApplicationCont :source_branch, :source_project_id, :state_event, + :wip_event, :squash, :target_branch, :target_project_id, diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb index e3959542865..1bd68d3116a 100644 --- a/app/helpers/issuables_helper.rb +++ b/app/helpers/issuables_helper.rb @@ -342,6 +342,12 @@ module IssuablesHelper issuable.closed? ^ should_inverse ? reopen_issuable_path(issuable) : close_issuable_path(issuable) end + def toggle_draft_issuable_path(issuable) + wip_event = issuable.work_in_progress? ? 'unwip' : 'wip' + + issuable_path(issuable, { merge_request: { wip_event: wip_event } }) + end + def issuable_path(issuable, *options) polymorphic_path(issuable, *options) end diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index 2f0596c93cc..b0169d6290a 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -204,8 +204,13 @@ class CommitStatus < ApplicationRecord # 'rspec:linux: 1/10' => 'rspec:linux' common_name = name.to_s.gsub(%r{\d+[\s:\/\\]+\d+\s*}, '') - # 'rspec:linux: [aws, max memory]' => 'rspec:linux' - common_name.gsub!(%r{: \[.*, .*\]\s*\z}, '') + if ::Gitlab::Ci::Features.one_dimensional_matrix_enabled? + # 'rspec:linux: [aws, max memory]' => 'rspec:linux', 'rspec:linux: [aws]' => 'rspec:linux' + common_name.gsub!(%r{: \[.*\]\s*\z}, '') + else + # 'rspec:linux: [aws, max memory]' => 'rspec:linux', 'rspec:linux: [aws]' => 'rspec:linux: [aws]' + common_name.gsub!(%r{: \[.*, .*\]\s*\z}, '') + end common_name.strip! common_name diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 1965c5dacc7..16a38cc64b1 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -1384,8 +1384,6 @@ class MergeRequest < ApplicationRecord end def has_coverage_reports? - return false unless Feature.enabled?(:coverage_report_view, project, default_enabled: true) - actual_head_pipeline&.has_coverage_reports? end diff --git a/app/presenters/merge_request_presenter.rb b/app/presenters/merge_request_presenter.rb index 1ff02412994..a22138011ae 100644 --- a/app/presenters/merge_request_presenter.rb +++ b/app/presenters/merge_request_presenter.rb @@ -37,7 +37,7 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated end def remove_wip_path - if work_in_progress? && can?(current_user, :update_merge_request, merge_request.project) + if can?(current_user, :update_merge_request, merge_request.project) remove_wip_project_merge_request_path(project, merge_request) end end diff --git a/app/serializers/merge_request_basic_entity.rb b/app/serializers/merge_request_basic_entity.rb index ef1177e9967..9e2bce53c8a 100644 --- a/app/serializers/merge_request_basic_entity.rb +++ b/app/serializers/merge_request_basic_entity.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true class MergeRequestBasicEntity < Grape::Entity + expose :title expose :public_merge_status, as: :merge_status expose :merge_error expose :state diff --git a/app/services/boards/create_service.rb b/app/services/boards/create_service.rb index 1a5dc790c41..2ccaea64d14 100644 --- a/app/services/boards/create_service.rb +++ b/app/services/boards/create_service.rb @@ -3,7 +3,11 @@ module Boards class CreateService < Boards::BaseService def execute - create_board! if can_create_board? + unless can_create_board? + return ServiceResponse.error(message: "You don't have the permission to create a board for this resource.") + end + + create_board! end private @@ -15,12 +19,16 @@ module Boards def create_board! board = parent.boards.create(params) - if board.persisted? - board.lists.create(list_type: :backlog) - board.lists.create(list_type: :closed) + unless board.persisted? + return ServiceResponse.error(message: "There was an error when creating a board.", payload: board) + end + + board.tap do |created_board| + created_board.lists.create(list_type: :backlog) + created_board.lists.create(list_type: :closed) end - board + ServiceResponse.success(payload: board) end end end diff --git a/app/services/ci/pipelines/create_artifact_service.rb b/app/services/ci/pipelines/create_artifact_service.rb index b7d334e436d..bfaf317241a 100644 --- a/app/services/ci/pipelines/create_artifact_service.rb +++ b/app/services/ci/pipelines/create_artifact_service.rb @@ -3,7 +3,6 @@ module Ci module Pipelines class CreateArtifactService def execute(pipeline) - return unless ::Gitlab::Ci::Features.coverage_report_view?(pipeline.project) return unless pipeline.can_generate_coverage_reports? return if pipeline.has_coverage_reports? diff --git a/app/views/admin/groups/_form.html.haml b/app/views/admin/groups/_form.html.haml index 041b0661d37..1feb2ad16ad 100644 --- a/app/views/admin/groups/_form.html.haml +++ b/app/views/admin/groups/_form.html.haml @@ -36,5 +36,3 @@ .form-actions = f.submit _('Save changes'), class: "btn btn-success", data: { qa_selector: 'save_changes_button' } = link_to _('Cancel'), admin_group_path(@group), class: "btn btn-cancel" - -= render_if_exists 'ldap_group_links/ldap_syncrhonizations', group: @group diff --git a/app/views/admin/users/_modals.html.haml b/app/views/admin/users/_modals.html.haml index 6cf6dc116e3..a8e5d962e5b 100644 --- a/app/views/admin/users/_modals.html.haml +++ b/app/views/admin/users/_modals.html.haml @@ -25,6 +25,6 @@ 'secondary-action': s_('AdminUsers|Block user') } } = s_('AdminUsers|You are about to permanently delete the user %{username}. This will delete all of the issues, merge requests, and groups linked to them. To avoid data loss, - consider using the %{strong_start}block user%{strong_end} feature instead. Once you %{strong_start}Delete user%{strong_end}, + consider using the %{strongStart}block user%{strongEnd} feature instead. Once you %{strongStart}Delete user%{strongEnd}, it cannot be undone or recovered.') diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml index e30f76593ac..cbc2a79d5bf 100644 --- a/app/views/layouts/_head.html.haml +++ b/app/views/layouts/_head.html.haml @@ -75,12 +75,12 @@ = javascript_include_tag locale_path unless I18n.locale == :en = webpack_bundle_tag "sentry" if Gitlab.config.sentry.enabled + = webpack_bundle_tag 'performance_bar' if performance_bar_enabled? - if content_for?(:page_specific_javascripts) = yield :page_specific_javascripts = webpack_controller_bundle_tags - = webpack_bundle_tag 'performance_bar' if performance_bar_enabled? = webpack_bundle_tag "chrome_84_icon_fix" if browser.chrome?([">=84", "<84.0.4147.125"]) || browser.edge?([">=84", "<84.0.522.59"]) = yield :project_javascripts diff --git a/app/views/projects/merge_requests/_mr_title.html.haml b/app/views/projects/merge_requests/_mr_title.html.haml index 454a0694355..b56e2c3f985 100644 --- a/app/views/projects/merge_requests/_mr_title.html.haml +++ b/app/views/projects/merge_requests/_mr_title.html.haml @@ -32,16 +32,19 @@ %ul - if can_update_merge_request %li= link_to 'Edit', edit_namespace_project_merge_request_path(@project.namespace, @project, @merge_request) - - unless current_user == @merge_request.author - %li= link_to 'Report abuse', new_abuse_report_path(user_id: @merge_request.author.id, ref_url: merge_request_url(@merge_request)) - if can_update_merge_request + - unless @merge_request.closed? + %li + = link_to @merge_request.work_in_progress? ? _('Mark as ready') : _('Mark as draft'), toggle_draft_issuable_path(@merge_request), method: :put, class: "js-draft-toggle-button" %li{ class: [merge_request_button_visibility(@merge_request, true), 'js-close-item'] } = link_to 'Close', merge_request_path(@merge_request, merge_request: { state_event: :close }), method: :put, title: 'Close merge request' - if can_reopen_merge_request %li{ class: merge_request_button_visibility(@merge_request, false) } = link_to 'Reopen', merge_request_path(@merge_request, merge_request: { state_event: :reopen }), method: :put, class: 'reopen-mr-link', title: 'Reopen merge request' + - unless current_user == @merge_request.author + %li= link_to 'Report abuse', new_abuse_report_path(user_id: @merge_request.author.id, ref_url: merge_request_url(@merge_request)) - if can_update_merge_request - = link_to 'Edit', edit_project_merge_request_path(@project, @merge_request), class: "d-none d-sm-none d-md-block btn btn-grouped js-issuable-edit qa-edit-button" + = link_to 'Edit', edit_project_merge_request_path(@project, @merge_request), class: "d-none d-sm-none d-md-block btn gl-button btn-grouped js-issuable-edit qa-edit-button" = render 'shared/issuable/close_reopen_button', issuable: @merge_request, can_update: can_update_merge_request, can_reopen: can_reopen_merge_request diff --git a/app/views/shared/issuable/_close_reopen_button.html.haml b/app/views/shared/issuable/_close_reopen_button.html.haml index 59d0c46b92f..9fb64ff19a9 100644 --- a/app/views/shared/issuable/_close_reopen_button.html.haml +++ b/app/views/shared/issuable/_close_reopen_button.html.haml @@ -5,7 +5,7 @@ - if defined? warn_before_close - add_blocked_class = warn_before_close -- if is_current_user +- if is_current_user && !issuable.is_a?(MergeRequest) - if can_update %button{ class: "d-none d-sm-none d-md-block btn btn-grouped btn-close js-btn-issue-action #{issuable_button_visibility(issuable, true)} #{(add_blocked_class ? 'btn-issue-blocked' : '')}", data: { remote: 'true', endpoint: close_issuable_path(issuable), qa_selector: 'close_issue_button' } } @@ -16,7 +16,10 @@ = _("Reopen %{display_issuable_type}") % { display_issuable_type: display_issuable_type } - else - if can_update && !are_close_and_open_buttons_hidden - = render 'shared/issuable/close_reopen_report_toggle', issuable: issuable, warn_before_close: add_blocked_class + - if issuable.is_a?(MergeRequest) + = render 'shared/issuable/close_reopen_draft_report_toggle', issuable: issuable + - else + = render 'shared/issuable/close_reopen_report_toggle', issuable: issuable, warn_before_close: add_blocked_class - else = link_to _('Report abuse'), new_abuse_report_path(user_id: issuable.author.id, ref_url: issuable_url(issuable)), class: 'd-none d-sm-none d-md-block btn btn-grouped btn-close-color', title: _('Report abuse') diff --git a/app/views/shared/issuable/_close_reopen_draft_report_toggle.html.haml b/app/views/shared/issuable/_close_reopen_draft_report_toggle.html.haml new file mode 100644 index 00000000000..bdb53dfe323 --- /dev/null +++ b/app/views/shared/issuable/_close_reopen_draft_report_toggle.html.haml @@ -0,0 +1,37 @@ +- display_issuable_type = issuable_display_type(issuable) +- button_action_class = issuable.closed? ? 'btn-default' : 'btn-warning btn-warning-secondary' +- button_class = "btn gl-button #{!issuable.closed? && 'js-draft-toggle-button'}" +- toggle_class = "btn gl-button dropdown-toggle" + +.float-left.btn-group.gl-ml-3.issuable-close-dropdown.d-none.d-md-inline-flex.js-issuable-close-dropdown + = link_to issuable.closed? ? reopen_issuable_path(issuable) : toggle_draft_issuable_path(issuable), method: :put, class: "#{button_class} #{button_action_class}" do + - if issuable.closed? + = _('Reopen') + = display_issuable_type + - else + = issuable.work_in_progress? ? _('Mark as ready') : _('Mark as draft') + + - if !issuable.closed? || !issuable_author_is_current_user(issuable) + = button_tag type: 'button', class: "#{toggle_class} #{button_action_class}", data: { 'toggle' => 'dropdown' } do + %span.sr-only= _('Toggle dropdown') + = sprite_icon "angle-down", size: 12 + + %ul.js-issuable-close-menu.dropdown-menu.dropdown-menu-right + - if issuable.open? + %li + = link_to close_issuable_path(issuable), method: :put do + .description + %strong.title + = _('Close') + = display_issuable_type + + - unless issuable_author_is_current_user(issuable) + - unless issuable.closed? + %li.divider.droplab-item-ignore + + %li.report-item + %a.report-abuse-link{ href: new_abuse_report_path(user_id: issuable.author.id, ref_url: issuable_url(issuable)) } + .description + %strong.title= _('Report abuse') + %p.text + = _('Report %{display_issuable_type} that are abusive, inappropriate or spam.') % { display_issuable_type: display_issuable_type.pluralize } diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index 68aa66074b7..014ada03686 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -5,6 +5,7 @@ - signed_in = !!issuable_sidebar.dig(:current_user, :id) - can_edit_issuable = issuable_sidebar.dig(:current_user, :can_edit) - add_page_startup_api_call "#{issuable_sidebar[:issuable_json_path]}?serializer=sidebar_extras" +- reviewers = local_assigns.fetch(:reviewers, nil) - if Feature.enabled?(:vue_issuable_sidebar, @project.group) %aside#js-vue-issuable-sidebar{ data: { signed_in: signed_in, @@ -28,6 +29,10 @@ .block.assignee.qa-assignee-block = render "shared/issuable/sidebar_assignees", issuable_sidebar: issuable_sidebar, assignees: assignees + - if Feature.enabled?(:merge_request_reviewers, @project) && reviewers + .block.reviewer.qa-reviewer-block + = render "shared/issuable/sidebar_reviewers", issuable_sidebar: issuable_sidebar, reviewers: reviewers + = render_if_exists 'shared/issuable/sidebar_item_epic', issuable_sidebar: issuable_sidebar - if issuable_sidebar[:supports_milestone] |