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/assets | |
parent | 635d82b15d83a88a2d073fbf3bb56f6ff626020f (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app/assets')
24 files changed, 792 insertions, 99 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; |