Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-10-01 15:10:14 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2020-10-01 15:10:14 +0300
commita27b8a5c104f492e4b0abac4c84385a615c4f6ba (patch)
tree3ec1a2cc06312b1999ce405905ab7881f68927d2 /app
parent635d82b15d83a88a2d073fbf3bb56f6ff626020f (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/commit/pipelines/pipelines_table.vue124
-rw-r--r--app/assets/javascripts/diffs/components/compare_dropdown_layout.vue2
-rw-r--r--app/assets/javascripts/diffs/components/compare_versions.vue1
-rw-r--r--app/assets/javascripts/diffs/components/diff_file_header.vue9
-rw-r--r--app/assets/javascripts/main.js10
-rw-r--r--app/assets/javascripts/merge_request.js74
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table.vue4
-rw-r--r--app/assets/javascripts/projects/default_project_templates.js4
-rw-r--r--app/assets/javascripts/sidebar/components/reviewers/collapsed_reviewer.vue24
-rw-r--r--app/assets/javascripts/sidebar/components/reviewers/collapsed_reviewer_list.vue107
-rw-r--r--app/assets/javascripts/sidebar/components/reviewers/reviewer_avatar.vue43
-rw-r--r--app/assets/javascripts/sidebar/components/reviewers/reviewer_avatar_link.vue84
-rw-r--r--app/assets/javascripts/sidebar/components/reviewers/reviewer_title.vue64
-rw-r--r--app/assets/javascripts/sidebar/components/reviewers/reviewers.vue72
-rw-r--r--app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers.vue107
-rw-r--r--app/assets/javascripts/sidebar/components/reviewers/uncollapsed_reviewer_list.vue103
-rw-r--r--app/assets/javascripts/sidebar/mount_sidebar.js32
-rw-r--r--app/assets/javascripts/tooltips/index.js2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view.vue7
-rw-r--r--app/assets/stylesheets/framework/buttons.scss6
-rw-r--r--app/assets/stylesheets/framework/variables.scss1
-rw-r--r--app/assets/stylesheets/pages/issuable.scss3
-rw-r--r--app/assets/stylesheets/pages/pipelines.scss4
-rw-r--r--app/controllers/concerns/multiple_boards_actions.rb6
-rw-r--r--app/controllers/projects/merge_requests/application_controller.rb1
-rw-r--r--app/helpers/issuables_helper.rb6
-rw-r--r--app/models/commit_status.rb9
-rw-r--r--app/models/merge_request.rb2
-rw-r--r--app/presenters/merge_request_presenter.rb2
-rw-r--r--app/serializers/merge_request_basic_entity.rb1
-rw-r--r--app/services/boards/create_service.rb18
-rw-r--r--app/services/ci/pipelines/create_artifact_service.rb1
-rw-r--r--app/views/admin/groups/_form.html.haml2
-rw-r--r--app/views/admin/users/_modals.html.haml2
-rw-r--r--app/views/layouts/_head.html.haml2
-rw-r--r--app/views/projects/merge_requests/_mr_title.html.haml9
-rw-r--r--app/views/shared/issuable/_close_reopen_button.html.haml7
-rw-r--r--app/views/shared/issuable/_close_reopen_draft_report_toggle.html.haml37
-rw-r--r--app/views/shared/issuable/_sidebar.html.haml5
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]