diff options
Diffstat (limited to 'app')
15 files changed, 261 insertions, 25 deletions
diff --git a/app/assets/javascripts/attention_requests/components/navigation_popover.vue b/app/assets/javascripts/attention_requests/components/navigation_popover.vue new file mode 100644 index 00000000000..1542bc9a7e9 --- /dev/null +++ b/app/assets/javascripts/attention_requests/components/navigation_popover.vue @@ -0,0 +1,120 @@ +<script> +import { GlPopover, GlSprintf, GlButton, GlLink, GlIcon } from '@gitlab/ui'; +import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils'; +import { helpPagePath } from '~/helpers/help_page_helper'; +import UserCalloutDismisser from '~/vue_shared/components/user_callout_dismisser.vue'; + +export default { + components: { + GlPopover, + GlSprintf, + GlButton, + GlLink, + GlIcon, + UserCalloutDismisser, + }, + inject: { + message: { + default: '', + }, + observerElSelector: { + default: '', + }, + observerElToggledClass: { + default: '', + }, + featureName: { + default: '', + }, + popoverTarget: { + default: '', + }, + showAttentionIcon: { + default: false, + }, + delay: { + default: 0, + }, + popoverCssClass: { + default: '', + }, + }, + data() { + return { + showPopover: false, + popoverPlacement: this.popoverPosition(), + }; + }, + mounted() { + this.observeEl = document.querySelector(this.observerElSelector); + this.observer = new MutationObserver(this.callback); + this.observer.observe(this.observeEl, { + attributes: true, + }); + this.callback(); + + window.addEventListener('resize', () => { + this.popoverPlacement = this.popoverPosition(); + }); + }, + beforeDestroy() { + this.observer.disconnect(); + }, + methods: { + callback() { + if (this.showPopover) { + this.$root.$emit('bv::hide::popover'); + } + + setTimeout(() => this.toggleShowPopover(), this.delay); + }, + toggleShowPopover() { + this.showPopover = this.observeEl.classList.contains(this.observerElToggledClass); + }, + getPopoverTarget() { + return document.querySelector(this.popoverTarget); + }, + popoverPosition() { + if (bp.isDesktop()) { + return 'left'; + } + + return 'bottom'; + }, + }, + docsPage: helpPagePath('development/code_review.html'), +}; +</script> + +<template> + <user-callout-dismisser :feature-name="featureName"> + <template #default="{ shouldShowCallout, dismiss }"> + <gl-popover + v-if="shouldShowCallout" + :show-close-button="false" + :target="() => getPopoverTarget()" + :show="showPopover" + :delay="0" + triggers="manual" + :placement="popoverPlacement" + boundary="window" + no-fade + :css-classes="[popoverCssClass]" + > + <p v-for="(m, index) in message" :key="index" class="gl-mb-5"> + <gl-sprintf :message="m"> + <template #strong="{ content }"> + <strong><gl-icon v-if="showAttentionIcon" name="attention" /> {{ content }}</strong> + </template> + </gl-sprintf> + </p> + <div class="gl-display-flex gl-align-items-center"> + <gl-button size="small" variant="confirm" class="gl-mr-5" @click.prevent.stop="dismiss"> + {{ __('Got it!') }} + </gl-button> + <gl-link :href="$options.docsPage" target="_blank">{{ __('Learn more') }}</gl-link> + </div> + </gl-popover> + </template> + </user-callout-dismisser> +</template> diff --git a/app/assets/javascripts/attention_requests/index.js b/app/assets/javascripts/attention_requests/index.js new file mode 100644 index 00000000000..2a142ab46e5 --- /dev/null +++ b/app/assets/javascripts/attention_requests/index.js @@ -0,0 +1,73 @@ +import Vue from 'vue'; +import VueApollo from 'vue-apollo'; +import { __ } from '~/locale'; +import createDefaultClient from '~/lib/graphql'; +import NavigationPopover from './components/navigation_popover.vue'; + +Vue.use(VueApollo); + +const apolloProvider = new VueApollo({ + defaultClient: createDefaultClient(), +}); + +export const initTopNavPopover = () => { + const el = document.getElementById('js-need-attention-nav-onboarding'); + + if (!el) return; + + // eslint-disable-next-line no-new + new Vue({ + el, + apolloProvider, + provide: { + observerElSelector: '.user-counter.dropdown', + observerElToggledClass: 'show', + message: [ + __( + '%{strongStart}Need your attention%{strongEnd} are the merge requests that need your help to move forward, as an assignee or reviewer.', + ), + ], + featureName: 'attention_requests_top_nav', + popoverTarget: '#js-need-attention-nav', + }, + render(h) { + return h(NavigationPopover); + }, + }); +}; + +export const initSideNavPopover = () => { + const el = document.getElementById('js-need-attention-sidebar-onboarding'); + + if (!el) return; + + // eslint-disable-next-line no-new + new Vue({ + el, + apolloProvider, + provide: { + observerElSelector: '.js-right-sidebar', + observerElToggledClass: 'right-sidebar-expanded', + message: [ + __( + 'To ask someone to look at a merge request, select %{strongStart}Request attention%{strongEnd}. Select again to remove the request.', + ), + __( + 'Some actions remove attention requests, like a reviewer approving or anyone merging the merge request.', + ), + ], + featureName: 'attention_requests_side_nav', + popoverTarget: '.js-attention-request-toggle', + showAttentionIcon: true, + delay: 500, + popoverCssClass: 'attention-request-sidebar-popover', + }, + render(h) { + return h(NavigationPopover); + }, + }); +}; + +export default () => { + initTopNavPopover(); +}; diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index efc425a1972..b3cb93e74f2 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -161,6 +161,12 @@ function deferredInitialisation() { // Adding a helper class to activate animations only after all is rendered setTimeout(() => $body.addClass('page-initialised'), 1000); + + if (window.gon?.features?.mrAttentionRequests) { + import('~/attention_requests') + .then((module) => module.default()) + .catch(() => {}); + } } const $body = $('body'); diff --git a/app/assets/javascripts/sidebar/components/attention_requested_toggle.vue b/app/assets/javascripts/sidebar/components/attention_requested_toggle.vue index d11bed4f058..6ba88939373 100644 --- a/app/assets/javascripts/sidebar/components/attention_requested_toggle.vue +++ b/app/assets/javascripts/sidebar/components/attention_requested_toggle.vue @@ -70,7 +70,10 @@ export default { </script> <template> - <span v-gl-tooltip.left.viewport="tooltipTitle" class="gl-display-inline-block"> + <span + v-gl-tooltip.left.viewport="tooltipTitle" + class="gl-display-inline-block js-attention-request-toggle" + > <gl-button :loading="loading" :variant="user.attention_requested ? 'warning' : 'default'" diff --git a/app/assets/javascripts/sidebar/sidebar_bundle.js b/app/assets/javascripts/sidebar/sidebar_bundle.js index 1be670f7590..74ab65e4e04 100644 --- a/app/assets/javascripts/sidebar/sidebar_bundle.js +++ b/app/assets/javascripts/sidebar/sidebar_bundle.js @@ -3,7 +3,17 @@ import Mediator from './sidebar_mediator'; export default (store) => { const mediator = new Mediator(getSidebarOptions()); - mediator.fetch(); + mediator + .fetch() + .then(() => { + if (window.gon?.features?.mrAttentionRequests) { + return import('~/attention_requests'); + } + + return null; + }) + .then((module) => module?.initSideNavPopover()) + .catch(() => {}); mountSidebar(mediator, store); }; diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_related_links.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_related_links.vue index 730d11b1208..319df090ff1 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_related_links.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_related_links.vue @@ -1,5 +1,5 @@ <script> -import { GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui'; +import { GlSafeHtmlDirective as SafeHtml, GlLink } from '@gitlab/ui'; import { s__, n__ } from '~/locale'; import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; @@ -8,6 +8,9 @@ export default { directives: { SafeHtml, }, + components: { + GlLink, + }, mixins: [glFeatureFlagMixin()], props: { relatedLinks: { @@ -37,6 +40,17 @@ export default { return n__('mrWidget|Closes issue', 'mrWidget|Closes issues', this.relatedLinks.closingCount); }, + assignIssueText() { + if (this.relatedLinks.unassignedCount > 1) { + return s__('mrWidget|Assign yourself to these issues'); + } + return s__('mrWidget|Assign yourself to this issue'); + }, + shouldShowAssignToMeLink() { + return ( + this.relatedLinks.unassignedCount && this.relatedLinks.assignToMe && this.showAssignToMe + ); + }, }, }; </script> @@ -57,10 +71,14 @@ export default { <span v-safe-html="relatedLinks.mentioned"></span> </p> <p - v-if="relatedLinks.assignToMe && showAssignToMe" + v-if="shouldShowAssignToMeLink" :class="{ 'gl-display-line gl-m-0': glFeatures.restructuredMrWidget }" > - <span v-html="relatedLinks.assignToMe /* eslint-disable-line vue/no-v-html */"></span> + <span> + <gl-link rel="nofollow" data-method="post" :href="relatedLinks.assignToMe">{{ + assignIssueText + }}</gl-link> + </span> </p> </section> </template> diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js index 5378dabf638..994e0c23b44 100644 --- a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js +++ b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js @@ -82,14 +82,16 @@ export default class MergeRequestStore { const { closing } = links; const mentioned = links.mentioned_but_not_closing; const assignToMe = links.assign_to_closing; + const unassignedCount = links.assign_to_closing_count; - if (closing || mentioned || assignToMe) { + if (closing || mentioned || unassignedCount) { this.relatedLinks = { closing, mentioned, assignToMe, closingCount: links.closing_count, mentionedCount: links.mentioned_count, + unassignedCount: links.assign_to_closing_count, }; } } diff --git a/app/assets/stylesheets/page_bundles/merge_requests.scss b/app/assets/stylesheets/page_bundles/merge_requests.scss index d2649acd622..34a3d936a67 100644 --- a/app/assets/stylesheets/page_bundles/merge_requests.scss +++ b/app/assets/stylesheets/page_bundles/merge_requests.scss @@ -757,3 +757,7 @@ $tabs-holder-z-index: 250; background: linear-gradient(to bottom, rgba(#333, 0), rgba(#333, 1)); } } + +.attention-request-sidebar-popover { + z-index: 999; +} diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb index 898e7826e3a..f25cc1bbc32 100644 --- a/app/controllers/dashboard_controller.rb +++ b/app/controllers/dashboard_controller.rb @@ -20,10 +20,6 @@ class DashboardController < Dashboard::ApplicationController urgency :low, [:merge_requests] - before_action only: [:merge_requests] do - push_frontend_feature_flag(:mr_attention_requests, default_enabled: :yaml) - end - def activity respond_to do |format| format.html diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index a7d2fb4a127..5f39a8419c9 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -30,9 +30,6 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo before_action :set_issuables_index, only: [:index] before_action :authenticate_user!, only: [:assign_related_issues] before_action :check_user_can_push_to_source_branch!, only: [:rebase] - before_action only: [:index, :show] do - push_frontend_feature_flag(:mr_attention_requests, project, default_enabled: :yaml) - end before_action only: [:show] do push_frontend_feature_flag(:file_identifier_hash) diff --git a/app/models/users/callout.rb b/app/models/users/callout.rb index 800256a613b..0922323e12b 100644 --- a/app/models/users/callout.rb +++ b/app/models/users/callout.rb @@ -46,7 +46,9 @@ module Users storage_enforcement_banner_first_enforcement_threshold: 43, storage_enforcement_banner_second_enforcement_threshold: 44, storage_enforcement_banner_third_enforcement_threshold: 45, - storage_enforcement_banner_fourth_enforcement_threshold: 46 + storage_enforcement_banner_fourth_enforcement_threshold: 46, + attention_requests_top_nav: 47, + attention_requests_side_nav: 48 } validates :feature_name, diff --git a/app/presenters/merge_request_presenter.rb b/app/presenters/merge_request_presenter.rb index 2dc2ecad1b4..6dd3908b21d 100644 --- a/app/presenters/merge_request_presenter.rb +++ b/app/presenters/merge_request_presenter.rb @@ -149,7 +149,11 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated ) end - def assign_to_closing_issues_link + def assign_to_closing_issues_path + assign_related_issues_project_merge_request_path(project, merge_request) + end + + def assign_to_closing_issues_count # rubocop: disable CodeReuse/ServiceClass issues = MergeRequests::AssignIssuesService.new(project: project, current_user: current_user, @@ -157,14 +161,7 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated merge_request: merge_request, closes_issues: closing_issues }).assignable_issues - path = assign_related_issues_project_merge_request_path(project, merge_request) - if issues.present? - if issues.count > 1 - link_to _('Assign yourself to these issues'), path, method: :post - else - link_to _('Assign yourself to this issue'), path, method: :post - end - end + issues.count # rubocop: enable CodeReuse/ServiceClass end diff --git a/app/serializers/merge_request_widget_entity.rb b/app/serializers/merge_request_widget_entity.rb index b9c71e6d97b..21ab20747d0 100644 --- a/app/serializers/merge_request_widget_entity.rb +++ b/app/serializers/merge_request_widget_entity.rb @@ -104,7 +104,11 @@ class MergeRequestWidgetEntity < Grape::Entity # include them if they are explicitly requested on first load. expose :issues_links, if: -> (_, opts) { opts[:issues_links] } do expose :assign_to_closing do |merge_request| - presenter(merge_request).assign_to_closing_issues_link + presenter(merge_request).assign_to_closing_issues_path + end + + expose :assign_to_closing_count do |merge_request| + presenter(merge_request).assign_to_closing_issues_count end expose :closing do |merge_request| diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml index 7256b9ed467..512a4185bee 100644 --- a/app/views/layouts/header/_default.html.haml +++ b/app/views/layouts/header/_default.html.haml @@ -79,7 +79,8 @@ %li.dropdown-header = _('Merge requests') - if Feature.enabled?(:mr_attention_requests, default_enabled: :yaml) - %li + %li#js-need-attention-nav + #js-need-attention-nav-onboarding = link_to attention_requested_mrs_dashboard_path, class: 'gl-display-flex! gl-align-items-center js-prefetch-document' do = _('Need your attention') = gl_badge_tag user_merge_requests_counts[:attention_requested_count], { size: :sm, variant: user_merge_requests_counts[:attention_requested_count] == 0 ? :neutral : :warning }, { class: 'merge-request-badge gl-ml-auto js-attention-count' } diff --git a/app/views/projects/merge_requests/show.html.haml b/app/views/projects/merge_requests/show.html.haml index 43bcb75ebbf..008f2588dbd 100644 --- a/app/views/projects/merge_requests/show.html.haml +++ b/app/views/projects/merge_requests/show.html.haml @@ -92,5 +92,8 @@ #js-review-bar +- if Feature.enabled?(:mr_attention_requests, default_enabled: :yaml) + #js-need-attention-sidebar-onboarding + = render 'projects/invite_members_modal', project: @project = render 'shared/web_ide_path' |