Commit 021ec839 authored by Simon Knox's avatar Simon Knox

Merge branch 'kp-add-issuable-sidebar' into 'master'

Update IssuableSidebar to be an independent component

See merge request gitlab-org/gitlab!44744
parents 7adb7125 d219fb2d
<script>
import Cookies from 'js-cookie';
import { GlIcon } from '@gitlab/ui';
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
import { parseBoolean } from '~/lib/utils/common_utils';
export default {
components: {
GlIcon,
},
data() {
const userExpanded = !parseBoolean(Cookies.get('collapsed_gutter'));
// We're deliberately keeping two different props for sidebar status;
// 1. userExpanded reflects value based on cookie `collapsed_gutter`.
// 2. isExpanded reflect actual sidebar state.
return {
userExpanded,
isExpanded: userExpanded ? bp.isDesktop() : userExpanded,
};
},
watch: {
isExpanded(expanded) {
this.$emit('sidebar-toggle', {
expanded,
});
},
},
mounted() {
window.addEventListener('resize', this.handleWindowResize);
},
beforeDestroy() {
window.removeEventListener('resize', this.handleWindowResize);
},
methods: {
updatePageContainerClass() {
const layoutPageEl = document.querySelector('.layout-page');
if (layoutPageEl) {
layoutPageEl.classList.toggle('right-sidebar-expanded', this.isExpanded);
layoutPageEl.classList.toggle('right-sidebar-collapsed', !this.isExpanded);
}
},
handleWindowResize() {
if (this.userExpanded) {
this.isExpanded = bp.isDesktop();
this.updatePageContainerClass();
}
},
handleToggleSidebarClick() {
this.isExpanded = !this.isExpanded;
this.userExpanded = this.isExpanded;
Cookies.set('collapsed_gutter', !this.userExpanded);
this.updatePageContainerClass();
},
},
};
</script>
<template>
<aside
:class="{ 'right-sidebar-expanded': isExpanded, 'right-sidebar-collapsed': !isExpanded }"
class="right-sidebar"
aria-live="polite"
>
<button
class="toggle-right-sidebar-button js-toggle-right-sidebar-button w-100 gl-text-decoration-none! gl-display-flex gl-outline-0!"
:title="__('Toggle sidebar')"
@click="handleToggleSidebarClick"
>
<span v-if="isExpanded" class="collapse-text gl-flex-grow-1 gl-text-left">{{
__('Collapse sidebar')
}}</span>
<gl-icon v-show="isExpanded" data-testid="icon-collapse" name="chevron-double-lg-right" />
<gl-icon
v-show="!isExpanded"
data-testid="icon-expand"
name="chevron-double-lg-left"
class="gl-ml-2"
/>
</button>
<div data-testid="sidebar-items" class="issuable-sidebar">
<slot name="right-sidebar-items" v-bind="{ sidebarExpanded: isExpanded }"></slot>
</div>
</aside>
</template>
<script>
export default {
props: {
signedIn: {
type: Boolean,
required: true,
},
sidebarStatusClass: {
type: String,
required: false,
default: '',
},
},
};
</script>
<template>
<aside
:class="sidebarStatusClass"
class="right-sidebar js-right-sidebar js-issuable-sidebar"
aria-live="polite"
></aside>
</template>
import Vue from 'vue';
import SidebarApp from './components/sidebar_app.vue';
export default () => {
const el = document.getElementById('js-vue-issuable-sidebar');
if (!el) {
return false;
}
const { sidebarStatusClass } = el.dataset;
// An empty string is present when user is signed in.
const signedIn = el.dataset.signedIn === '';
return new Vue({
el,
components: { SidebarApp },
render: createElement =>
createElement('sidebar-app', {
props: {
signedIn,
sidebarStatusClass,
},
}),
});
};
...@@ -10,7 +10,6 @@ import initIncidentApp from '~/issue_show/incident'; ...@@ -10,7 +10,6 @@ import initIncidentApp from '~/issue_show/incident';
import initIssuableHeaderWarning from '~/vue_shared/components/issuable/init_issuable_header_warning'; import initIssuableHeaderWarning from '~/vue_shared/components/issuable/init_issuable_header_warning';
import initSentryErrorStackTraceApp from '~/sentry_error_stack_trace'; import initSentryErrorStackTraceApp from '~/sentry_error_stack_trace';
import initRelatedMergeRequestsApp from '~/related_merge_requests'; import initRelatedMergeRequestsApp from '~/related_merge_requests';
import initVueIssuableSidebarApp from '~/issuable_sidebar/sidebar_bundle';
import { parseIssuableData } from '~/issue_show/utils/parse_data'; import { parseIssuableData } from '~/issue_show/utils/parse_data';
export default function() { export default function() {
...@@ -33,11 +32,7 @@ export default function() { ...@@ -33,11 +32,7 @@ export default function() {
new Issue(); // eslint-disable-line no-new new Issue(); // eslint-disable-line no-new
new ShortcutsIssuable(); // eslint-disable-line no-new new ShortcutsIssuable(); // eslint-disable-line no-new
new ZenMode(); // eslint-disable-line no-new new ZenMode(); // eslint-disable-line no-new
if (gon.features && gon.features.vueIssuableSidebar) { initIssuableSidebar();
initVueIssuableSidebarApp();
} else {
initIssuableSidebar();
}
loadAwardsHandler(); loadAwardsHandler();
} }
...@@ -4,17 +4,12 @@ import ShortcutsIssuable from '~/behaviors/shortcuts/shortcuts_issuable'; ...@@ -4,17 +4,12 @@ import ShortcutsIssuable from '~/behaviors/shortcuts/shortcuts_issuable';
import { handleLocationHash } from '~/lib/utils/common_utils'; import { handleLocationHash } from '~/lib/utils/common_utils';
import howToMerge from '~/how_to_merge'; import howToMerge from '~/how_to_merge';
import initPipelines from '~/commit/pipelines/pipelines_bundle'; import initPipelines from '~/commit/pipelines/pipelines_bundle';
import initVueIssuableSidebarApp from '~/issuable_sidebar/sidebar_bundle';
import initSourcegraph from '~/sourcegraph'; import initSourcegraph from '~/sourcegraph';
import loadAwardsHandler from '~/awards_handler'; import loadAwardsHandler from '~/awards_handler';
export default function() { export default function() {
new ZenMode(); // eslint-disable-line no-new new ZenMode(); // eslint-disable-line no-new
if (gon.features && gon.features.vueIssuableSidebar) { initIssuableSidebar();
initVueIssuableSidebarApp();
} else {
initIssuableSidebar();
}
initPipelines(); initPipelines();
new ShortcutsIssuable(true); // eslint-disable-line no-new new ShortcutsIssuable(true); // eslint-disable-line no-new
handleLocationHash(); handleLocationHash();
......
...@@ -363,20 +363,30 @@ ...@@ -363,20 +363,30 @@
// Collapsed nav // Collapsed nav
.toggle-sidebar-button, .toggle-sidebar-button,
.close-nav-button { .close-nav-button,
width: $contextual-sidebar-width - 1px; .toggle-right-sidebar-button {
transition: width $sidebar-transition-duration; transition: width $sidebar-transition-duration;
position: fixed;
height: $toggle-sidebar-height; height: $toggle-sidebar-height;
bottom: 0;
padding: 0 $gl-padding; padding: 0 $gl-padding;
background-color: $gray-light; background-color: $gray-light;
border: 0; border: 0;
border-top: 1px solid $border-color;
color: $gl-text-color-secondary; color: $gl-text-color-secondary;
display: flex; display: flex;
align-items: center; align-items: center;
&:hover {
background-color: $border-color;
color: $gl-text-color;
}
}
.toggle-sidebar-button,
.close-nav-button {
position: fixed;
bottom: 0;
width: $contextual-sidebar-width - 1px;
border-top: 1px solid $border-color;
svg { svg {
margin-right: 8px; margin-right: 8px;
} }
...@@ -384,11 +394,10 @@ ...@@ -384,11 +394,10 @@
.icon-chevron-double-lg-right { .icon-chevron-double-lg-right {
display: none; display: none;
} }
}
&:hover { .toggle-right-sidebar-button {
background-color: $border-color; border-bottom: 1px solid $border-color;
color: $gl-text-color;
}
} }
.collapse-text { .collapse-text {
......
...@@ -7,207 +7,203 @@ ...@@ -7,207 +7,203 @@
- add_page_startup_api_call "#{issuable_sidebar[:issuable_json_path]}?serializer=sidebar_extras" - add_page_startup_api_call "#{issuable_sidebar[:issuable_json_path]}?serializer=sidebar_extras"
- reviewers = local_assigns.fetch(:reviewers, nil) - reviewers = local_assigns.fetch(:reviewers, nil)
- if Feature.enabled?(:vue_issuable_sidebar, @project.group) %aside.right-sidebar.js-right-sidebar.js-issuable-sidebar{ data: { signed: { in: signed_in }, issuable_type: issuable_type }, class: sidebar_gutter_collapsed_class, 'aria-live' => 'polite' }
%aside#js-vue-issuable-sidebar{ data: { signed_in: signed_in, .issuable-sidebar
sidebar_status_class: sidebar_gutter_collapsed_class } } .block.issuable-sidebar-header
- else - if signed_in
%aside.right-sidebar.js-right-sidebar.js-issuable-sidebar{ data: { signed: { in: signed_in }, issuable_type: issuable_type }, class: sidebar_gutter_collapsed_class, 'aria-live' => 'polite' } %span.issuable-header-text.hide-collapsed.float-left
.issuable-sidebar = _('To Do')
.block.issuable-sidebar-header %a.gutter-toggle.float-right.js-sidebar-toggle.has-tooltip{ role: "button", href: "#", "aria-label" => _('Toggle sidebar'), title: sidebar_gutter_tooltip_text, data: { container: 'body', placement: 'left', boundary: 'viewport' } }
- if signed_in = sidebar_gutter_toggle_icon
%span.issuable-header-text.hide-collapsed.float-left - if signed_in
= _('To Do') = render "shared/issuable/sidebar_todo", issuable_sidebar: issuable_sidebar
%a.gutter-toggle.float-right.js-sidebar-toggle.has-tooltip{ role: "button", href: "#", "aria-label" => _('Toggle sidebar'), title: sidebar_gutter_tooltip_text, data: { container: 'body', placement: 'left', boundary: 'viewport' } }
= sidebar_gutter_toggle_icon = form_for issuable_type, url: issuable_sidebar[:issuable_json_path], remote: true, html: { class: 'issuable-context-form inline-update js-issuable-update' } do |f|
- if signed_in - if signed_in
= render "shared/issuable/sidebar_todo", issuable_sidebar: issuable_sidebar .block.todo.hide-expanded
= render "shared/issuable/sidebar_todo", issuable_sidebar: issuable_sidebar, is_collapsed: true
= form_for issuable_type, url: issuable_sidebar[:issuable_json_path], remote: true, html: { class: 'issuable-context-form inline-update js-issuable-update' } do |f| .block.assignee.qa-assignee-block
- if signed_in = render "shared/issuable/sidebar_assignees", issuable_sidebar: issuable_sidebar, assignees: assignees, signed_in: signed_in
.block.todo.hide-expanded
= render "shared/issuable/sidebar_todo", issuable_sidebar: issuable_sidebar, is_collapsed: true - if Feature.enabled?(:merge_request_reviewers, @project) && reviewers
.block.assignee.qa-assignee-block .block.reviewer.qa-reviewer-block
= render "shared/issuable/sidebar_assignees", issuable_sidebar: issuable_sidebar, assignees: assignees, signed_in: signed_in = render "shared/issuable/sidebar_reviewers", issuable_sidebar: issuable_sidebar, reviewers: reviewers, signed_in: signed_in
- if Feature.enabled?(:merge_request_reviewers, @project) && reviewers = render_if_exists 'shared/issuable/sidebar_item_epic', issuable_sidebar: issuable_sidebar
.block.reviewer.qa-reviewer-block
= render "shared/issuable/sidebar_reviewers", issuable_sidebar: issuable_sidebar, reviewers: reviewers, signed_in: signed_in - if issuable_sidebar[:supports_milestone]
- milestone = issuable_sidebar[:milestone] || {}
= render_if_exists 'shared/issuable/sidebar_item_epic', issuable_sidebar: issuable_sidebar .block.milestone{ data: { qa_selector: 'milestone_block' } }
.sidebar-collapsed-icon.has-tooltip{ title: sidebar_milestone_tooltip_label(milestone), data: { container: 'body', html: 'true', placement: 'left', boundary: 'viewport' } }
- if issuable_sidebar[:supports_milestone] = sprite_icon('clock')
- milestone = issuable_sidebar[:milestone] || {} %span.milestone-title.collapse-truncated-title
.block.milestone{ data: { qa_selector: 'milestone_block' } }
.sidebar-collapsed-icon.has-tooltip{ title: sidebar_milestone_tooltip_label(milestone), data: { container: 'body', html: 'true', placement: 'left', boundary: 'viewport' } }
= sprite_icon('clock')
%span.milestone-title.collapse-truncated-title
- if milestone.present?
= milestone[:title]
- else
= _('None')
.title.hide-collapsed
= _('Milestone')
= loading_icon(css_class: 'gl-vertical-align-text-bottom hidden block-loading')
- if can_edit_issuable
= link_to _('Edit'), '#', class: 'js-sidebar-dropdown-toggle edit-link float-right', data: { qa_selector: "edit_milestone_link", track_label: "right_sidebar", track_property: "milestone", track_event: "click_edit_button", track_value: "" }
.value.hide-collapsed
- if milestone.present? - if milestone.present?
- milestone_title = milestone[:expired] ? _("%{milestone_name} (Past due)").html_safe % { milestone_name: milestone[:title] } : milestone[:title] = milestone[:title]
= link_to milestone_title, milestone[:web_url], class: "bold has-tooltip", title: sidebar_milestone_remaining_days(milestone), data: { container: "body", html: 'true', boundary: 'viewport', qa_selector: 'milestone_link', qa_title: milestone[:title] }
- else - else
%span.no-value = _('None')
= _('None') .title.hide-collapsed
= _('Milestone')
.selectbox.hide-collapsed = loading_icon(css_class: 'gl-vertical-align-text-bottom hidden block-loading')
= f.hidden_field 'milestone_id', value: milestone[:id], id: nil
= dropdown_tag('Milestone', options: { title: _('Assign milestone'), toggle_class: 'js-milestone-select js-extra-options', filter: true, dropdown_class: 'dropdown-menu-selectable', placeholder: _('Search milestones'), data: { show_no: true, field_name: "#{issuable_type}[milestone_id]", project_id: issuable_sidebar[:project_id], issuable_id: issuable_sidebar[:id], ability_name: issuable_type, issue_update: issuable_sidebar[:issuable_json_path], use_id: true, default_no: true, selected: milestone[:title], null_default: true, display: 'static' }})
- if @project.group.present?
= render_if_exists 'shared/issuable/iteration_select', { can_edit: can_edit_issuable, group_path: @project.group.full_path, project_path: issuable_sidebar[:project_full_path], issue_iid: issuable_sidebar[:iid], issuable_type: issuable_type }
- if issuable_sidebar[:supports_time_tracking]
#issuable-time-tracker.block
// Fallback while content is loading
.title.hide-collapsed
= _('Time tracking')
= loading_icon
- if issuable_sidebar.has_key?(:due_date)
.block.due_date
.sidebar-collapsed-icon.has-tooltip{ data: { placement: 'left', container: 'body', html: 'true', boundary: 'viewport' }, title: sidebar_due_date_tooltip_label(issuable_sidebar[:due_date]) }
= sprite_icon('calendar')
%span.js-due-date-sidebar-value
= issuable_sidebar[:due_date].try(:to_s, :medium) || _('None')
.title.hide-collapsed
= _('Due date')
= loading_icon(css_class: 'gl-vertical-align-text-bottom hidden block-loading')
- if can_edit_issuable
= link_to _('Edit'), '#', class: 'js-sidebar-dropdown-toggle edit-link float-right', data: { track_label: "right_sidebar", track_property: "due_date", track_event: "click_edit_button", track_value: "" }
.value.hide-collapsed
%span.value-content
- if issuable_sidebar[:due_date]
%span.bold= issuable_sidebar[:due_date].to_s(:medium)
- else
%span.no-value
= _('None')
- if can_edit_issuable
%span.no-value.js-remove-due-date-holder{ class: ("hidden" if issuable_sidebar[:due_date].nil?) }
\-
%a.js-remove-due-date{ href: "#", role: "button" }
= _('remove due date')
- if can_edit_issuable - if can_edit_issuable
.selectbox.hide-collapsed = link_to _('Edit'), '#', class: 'js-sidebar-dropdown-toggle edit-link float-right', data: { qa_selector: "edit_milestone_link", track_label: "right_sidebar", track_property: "milestone", track_event: "click_edit_button", track_value: "" }
= f.hidden_field :due_date, value: issuable_sidebar[:due_date].try(:strftime, 'yy-mm-dd') .value.hide-collapsed
.dropdown - if milestone.present?
%button.dropdown-menu-toggle.js-due-date-select{ type: 'button', data: { toggle: 'dropdown', field_name: "#{issuable_type}[due_date]", ability_name: issuable_type, issue_update: issuable_sidebar[:issuable_json_path], display: 'static' } } - milestone_title = milestone[:expired] ? _("%{milestone_name} (Past due)").html_safe % { milestone_name: milestone[:title] } : milestone[:title]
%span.dropdown-toggle-text = link_to milestone_title, milestone[:web_url], class: "bold has-tooltip", title: sidebar_milestone_remaining_days(milestone), data: { container: "body", html: 'true', boundary: 'viewport', qa_selector: 'milestone_link', qa_title: milestone[:title] }
= _('Due date') - else
= icon('chevron-down', 'aria-hidden': 'true') %span.no-value
.dropdown-menu.dropdown-menu-due-date = _('None')
= dropdown_title(_('Due date'))
= dropdown_content do .selectbox.hide-collapsed
.js-due-date-calendar = f.hidden_field 'milestone_id', value: milestone[:id], id: nil
= dropdown_tag('Milestone', options: { title: _('Assign milestone'), toggle_class: 'js-milestone-select js-extra-options', filter: true, dropdown_class: 'dropdown-menu-selectable', placeholder: _('Search milestones'), data: { show_no: true, field_name: "#{issuable_type}[milestone_id]", project_id: issuable_sidebar[:project_id], issuable_id: issuable_sidebar[:id], ability_name: issuable_type, issue_update: issuable_sidebar[:issuable_json_path], use_id: true, default_no: true, selected: milestone[:title], null_default: true, display: 'static' }})
- if @project.group.present?
- if Feature.enabled?(:vue_sidebar_labels, @project) = render_if_exists 'shared/issuable/iteration_select', { can_edit: can_edit_issuable, group_path: @project.group.full_path, project_path: issuable_sidebar[:project_full_path], issue_iid: issuable_sidebar[:iid], issuable_type: issuable_type }
.js-sidebar-labels{ data: { allow_label_create: issuable_sidebar.dig(:current_user, :can_admin_label).to_s,
allow_scoped_labels: issuable_sidebar[:scoped_labels_available].to_s, - if issuable_sidebar[:supports_time_tracking]
can_edit: can_edit_issuable.to_s, #issuable-time-tracker.block
iid: issuable_sidebar[:iid], // Fallback while content is loading
issuable_type: issuable_type, .title.hide-collapsed
labels_fetch_path: issuable_sidebar[:project_labels_path], = _('Time tracking')
labels_manage_path: project_labels_path(@project), = loading_icon
labels_update_path: issuable_sidebar[:issuable_json_path], - if issuable_sidebar.has_key?(:due_date)
project_issues_path: issuable_sidebar[:project_issuables_path], .block.due_date
project_path: @project.full_path, .sidebar-collapsed-icon.has-tooltip{ data: { placement: 'left', container: 'body', html: 'true', boundary: 'viewport' }, title: sidebar_due_date_tooltip_label(issuable_sidebar[:due_date]) }
selected_labels: issuable_sidebar[:labels].to_json } } = sprite_icon('calendar')
- else %span.js-due-date-sidebar-value
- selected_labels = issuable_sidebar[:labels] = issuable_sidebar[:due_date].try(:to_s, :medium) || _('None')
.block.labels{ data: { qa_selector: 'labels_block' } } .title.hide-collapsed
.sidebar-collapsed-icon.js-sidebar-labels-tooltip{ title: issuable_labels_tooltip(selected_labels), data: { placement: "left", container: "body", boundary: 'viewport' } } = _('Due date')
= sprite_icon('labels') = loading_icon(css_class: 'gl-vertical-align-text-bottom hidden block-loading')
%span - if can_edit_issuable
= selected_labels.size = link_to _('Edit'), '#', class: 'js-sidebar-dropdown-toggle edit-link float-right', data: { track_label: "right_sidebar", track_property: "due_date", track_event: "click_edit_button", track_value: "" }
.title.hide-collapsed .value.hide-collapsed
= _('Labels') %span.value-content
= loading_icon(css_class: 'gl-vertical-align-text-bottom hidden block-loading') - if issuable_sidebar[:due_date]
- if can_edit_issuable %span.bold= issuable_sidebar[:due_date].to_s(:medium)
= link_to _('Edit'), '#', class: 'js-sidebar-dropdown-toggle edit-link float-right', data: { qa_selector: "labels_edit_button", track_label: "right_sidebar", track_property: "labels", track_event: "click_edit_button", track_value: "" }
.value.issuable-show-labels.dont-hide.hide-collapsed{ class: ("has-labels" if selected_labels.any?) }
- if selected_labels.any?
- selected_labels.each do |label_hash|
= render_label(label_from_hash(label_hash).present(issuable_subject: nil), link: sidebar_label_filter_path(issuable_sidebar[:project_issuables_path], label_hash[:title]), dataset: { qa_selector: 'selected_label_content', qa_label_name: label_hash[:title] })
- else - else
%span.no-value %span.no-value
= _('None') = _('None')
- if can_edit_issuable
%span.no-value.js-remove-due-date-holder{ class: ("hidden" if issuable_sidebar[:due_date].nil?) }
\-
%a.js-remove-due-date{ href: "#", role: "button" }
= _('remove due date')
- if can_edit_issuable
.selectbox.hide-collapsed .selectbox.hide-collapsed
- selected_labels.each do |label| = f.hidden_field :due_date, value: issuable_sidebar[:due_date].try(:strftime, 'yy-mm-dd')
= hidden_field_tag "#{issuable_type}[label_names][]", label[:id], id: nil
.dropdown .dropdown
%button.dropdown-menu-toggle.js-label-select.js-multiselect.js-label-sidebar-dropdown{ type: "button", data: sidebar_label_dropdown_data(issuable_type, issuable_sidebar) } %button.dropdown-menu-toggle.js-due-date-select{ type: 'button', data: { toggle: 'dropdown', field_name: "#{issuable_type}[due_date]", ability_name: issuable_type, issue_update: issuable_sidebar[:issuable_json_path], display: 'static' } }
%span.dropdown-toggle-text{ class: ("is-default" if selected_labels.empty?) } %span.dropdown-toggle-text
= multi_label_name(selected_labels, "Labels") = _('Due date')
= icon('chevron-down', 'aria-hidden': 'true') = icon('chevron-down', 'aria-hidden': 'true')
.dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable.dropdown-extended-height{ data: { qa_selector: "labels_dropdown_content"} } .dropdown-menu.dropdown-menu-due-date
= render partial: "shared/issuable/label_page_default" = dropdown_title(_('Due date'))
- if issuable_sidebar.dig(:current_user, :can_admin_label) = dropdown_content do
= render partial: "shared/issuable/label_page_create" .js-due-date-calendar
= render_if_exists 'shared/issuable/sidebar_weight', issuable_sidebar: issuable_sidebar
- if Feature.enabled?(:vue_sidebar_labels, @project)
- if issuable_sidebar[:supports_severity] .js-sidebar-labels{ data: { allow_label_create: issuable_sidebar.dig(:current_user, :can_admin_label).to_s,
#js-severity allow_scoped_labels: issuable_sidebar[:scoped_labels_available].to_s,
can_edit: can_edit_issuable.to_s,
- if issuable_sidebar.dig(:features_available, :health_status) iid: issuable_sidebar[:iid],
.js-sidebar-status-entry-point issuable_type: issuable_type,
labels_fetch_path: issuable_sidebar[:project_labels_path],
- if issuable_sidebar.has_key?(:confidential) labels_manage_path: project_labels_path(@project),
%script#js-confidential-issue-data{ type: "application/json" }= { is_confidential: issuable_sidebar[:confidential], is_editable: can_edit_issuable }.to_json.html_safe labels_update_path: issuable_sidebar[:issuable_json_path],
#js-confidential-entry-point project_issues_path: issuable_sidebar[:project_issuables_path],
project_path: @project.full_path,
%script#js-lock-issue-data{ type: "application/json" }= { is_locked: !!issuable_sidebar[:discussion_locked], is_editable: can_edit_issuable }.to_json.html_safe selected_labels: issuable_sidebar[:labels].to_json } }
#js-lock-entry-point - else
- selected_labels = issuable_sidebar[:labels]
.js-sidebar-participants-entry-point .block.labels{ data: { qa_selector: 'labels_block' } }
.sidebar-collapsed-icon.js-sidebar-labels-tooltip{ title: issuable_labels_tooltip(selected_labels), data: { placement: "left", container: "body", boundary: 'viewport' } }
- if signed_in = sprite_icon('labels')
.js-sidebar-subscriptions-entry-point %span
= selected_labels.size
- project_ref = issuable_sidebar[:reference] .title.hide-collapsed
.block.with-sub-blocks = _('Labels')
.project-reference.sub-block = loading_icon(css_class: 'gl-vertical-align-text-bottom hidden block-loading')
- if can_edit_issuable
= link_to _('Edit'), '#', class: 'js-sidebar-dropdown-toggle edit-link float-right', data: { qa_selector: "labels_edit_button", track_label: "right_sidebar", track_property: "labels", track_event: "click_edit_button", track_value: "" }
.value.issuable-show-labels.dont-hide.hide-collapsed{ class: ("has-labels" if selected_labels.any?) }
- if selected_labels.any?
- selected_labels.each do |label_hash|
= render_label(label_from_hash(label_hash).present(issuable_subject: nil), link: sidebar_label_filter_path(issuable_sidebar[:project_issuables_path], label_hash[:title]), dataset: { qa_selector: 'selected_label_content', qa_label_name: label_hash[:title] })
- else
%span.no-value
= _('None')
.selectbox.hide-collapsed
- selected_labels.each do |label|
= hidden_field_tag "#{issuable_type}[label_names][]", label[:id], id: nil
.dropdown
%button.dropdown-menu-toggle.js-label-select.js-multiselect.js-label-sidebar-dropdown{ type: "button", data: sidebar_label_dropdown_data(issuable_type, issuable_sidebar) }
%span.dropdown-toggle-text{ class: ("is-default" if selected_labels.empty?) }
= multi_label_name(selected_labels, "Labels")
= icon('chevron-down', 'aria-hidden': 'true')
.dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable.dropdown-extended-height{ data: { qa_selector: "labels_dropdown_content"} }
= render partial: "shared/issuable/label_page_default"
- if issuable_sidebar.dig(:current_user, :can_admin_label)
= render partial: "shared/issuable/label_page_create"
= render_if_exists 'shared/issuable/sidebar_weight', issuable_sidebar: issuable_sidebar
- if issuable_sidebar[:supports_severity]
#js-severity
- if issuable_sidebar.dig(:features_available, :health_status)
.js-sidebar-status-entry-point
- if issuable_sidebar.has_key?(:confidential)
%script#js-confidential-issue-data{ type: "application/json" }= { is_confidential: issuable_sidebar[:confidential], is_editable: can_edit_issuable }.to_json.html_safe
#js-confidential-entry-point
%script#js-lock-issue-data{ type: "application/json" }= { is_locked: !!issuable_sidebar[:discussion_locked], is_editable: can_edit_issuable }.to_json.html_safe
#js-lock-entry-point
.js-sidebar-participants-entry-point
- if signed_in
.js-sidebar-subscriptions-entry-point
- project_ref = issuable_sidebar[:reference]
.block.with-sub-blocks
.project-reference.sub-block
.sidebar-collapsed-icon.dont-change-state
= clipboard_button(text: project_ref, title: _('Copy reference'), placement: "left", boundary: 'viewport')
.cross-project-reference.hide-collapsed
%span
= _('Reference:')
%cite{ title: project_ref }
= project_ref
= clipboard_button(text: project_ref, title: _('Copy reference'), placement: "left", boundary: 'viewport')
- if issuable_type == 'merge_request'
.sidebar-source-branch.sub-block
.sidebar-collapsed-icon.dont-change-state .sidebar-collapsed-icon.dont-change-state
= clipboard_button(text: project_ref, title: _('Copy reference'), placement: "left", boundary: 'viewport') = clipboard_button(text: source_branch, title: _('Copy branch name'), placement: "left", boundary: 'viewport')
.cross-project-reference.hide-collapsed .sidebar-mr-source-branch.hide-collapsed
%span %span
= _('Reference:') = _('Source branch: %{source_branch_open}%{source_branch}%{source_branch_close}').html_safe % { source_branch_open: "<cite class='ref-name' title='#{source_branch}'>".html_safe, source_branch_close: "</cite>".html_safe, source_branch: source_branch }
%cite{ title: project_ref } = clipboard_button(text: source_branch, title: _('Copy branch name'), placement: "left", boundary: 'viewport')
= project_ref
= clipboard_button(text: project_ref, title: _('Copy reference'), placement: "left", boundary: 'viewport') - if issuable_sidebar.dig(:current_user, :can_move)
- if issuable_type == 'merge_request' .block.js-sidebar-move-issue-block
.sidebar-source-branch.sub-block .sidebar-collapsed-icon{ data: { toggle: 'tooltip', placement: 'left', container: 'body', boundary: 'viewport' }, title: _('Move issue') }
.sidebar-collapsed-icon.dont-change-state = custom_icon('icon_arrow_right')
= clipboard_button(text: source_branch, title: _('Copy branch name'), placement: "left", boundary: 'viewport') .dropdown.sidebar-move-issue-dropdown.hide-collapsed
.sidebar-mr-source-branch.hide-collapsed %button.btn.btn-default.btn-block.js-sidebar-dropdown-toggle.js-move-issue{ type: 'button',
%span data: { toggle: 'dropdown', display: 'static', track_label: "right_sidebar", track_property: "move_issue", track_event: "click_button", track_value: "" } }
= _('Source branch: %{source_branch_open}%{source_branch}%{source_branch_close}').html_safe % { source_branch_open: "<cite class='ref-name' title='#{source_branch}'>".html_safe, source_branch_close: "</cite>".html_safe, source_branch: source_branch } = _('Move issue')
= clipboard_button(text: source_branch, title: _('Copy branch name'), placement: "left", boundary: 'viewport') .dropdown-menu.dropdown-menu-selectable.dropdown-extended-height
= dropdown_title(_('Move issue'))
- if issuable_sidebar.dig(:current_user, :can_move) = dropdown_filter(_('Search project'), search_id: 'sidebar-move-issue-dropdown-search')
.block.js-sidebar-move-issue-block = dropdown_content
.sidebar-collapsed-icon{ data: { toggle: 'tooltip', placement: 'left', container: 'body', boundary: 'viewport' }, title: _('Move issue') } = dropdown_loading
= custom_icon('icon_arrow_right') = dropdown_footer add_content_class: true do
.dropdown.sidebar-move-issue-dropdown.hide-collapsed %button.btn.btn-success.sidebar-move-issue-confirmation-button.js-move-issue-confirmation-button{ type: 'button', disabled: true }
%button.btn.btn-default.btn-block.js-sidebar-dropdown-toggle.js-move-issue{ type: 'button', = _('Move')
data: { toggle: 'dropdown', display: 'static', track_label: "right_sidebar", track_property: "move_issue", track_event: "click_button", track_value: "" } } = loading_icon(css_class: 'gl-vertical-align-text-bottom sidebar-move-issue-confirmation-loading-icon')
= _('Move issue')
.dropdown-menu.dropdown-menu-selectable.dropdown-extended-height -# haml-lint:disable InlineJavaScript
= dropdown_title(_('Move issue')) %script.js-sidebar-options{ type: "application/json" }= issuable_sidebar_options(issuable_sidebar).to_json.html_safe
= dropdown_filter(_('Search project'), search_id: 'sidebar-move-issue-dropdown-search')
= dropdown_content
= dropdown_loading
= dropdown_footer add_content_class: true do
%button.btn.btn-success.sidebar-move-issue-confirmation-button.js-move-issue-confirmation-button{ type: 'button', disabled: true }
= _('Move')
= loading_icon(css_class: 'gl-vertical-align-text-bottom sidebar-move-issue-confirmation-loading-icon')
-# haml-lint:disable InlineJavaScript
%script.js-sidebar-options{ type: "application/json" }= issuable_sidebar_options(issuable_sidebar).to_json.html_safe
...@@ -4,7 +4,6 @@ import { mapState, mapGetters } from 'vuex'; ...@@ -4,7 +4,6 @@ import { mapState, mapGetters } from 'vuex';
import { PathIdSeparator } from '~/related_issues/constants'; import { PathIdSeparator } from '~/related_issues/constants';
import IssuableBody from '~/issue_show/components/app.vue'; import IssuableBody from '~/issue_show/components/app.vue';
import IssuableSidebar from '~/issuable_sidebar/components/sidebar_app.vue';
import EpicSidebar from './epic_sidebar.vue'; import EpicSidebar from './epic_sidebar.vue';
...@@ -12,7 +11,6 @@ export default { ...@@ -12,7 +11,6 @@ export default {
PathIdSeparator, PathIdSeparator,
components: { components: {
IssuableBody, IssuableBody,
IssuableSidebar,
EpicSidebar, EpicSidebar,
}, },
computed: { computed: {
...@@ -33,9 +31,6 @@ export default { ...@@ -33,9 +31,6 @@ export default {
'sidebarCollapsed', 'sidebarCollapsed',
]), ]),
...mapGetters(['isUserSignedIn']), ...mapGetters(['isUserSignedIn']),
isVueIssuableEpicSidebarEnabled() {
return gon.features && gon.features.vueIssuableEpicSidebar;
},
sidebarStatusClass() { sidebarStatusClass() {
return this.sidebarCollapsed ? 'right-sidebar-collapsed' : 'right-sidebar-expanded'; return this.sidebarCollapsed ? 'right-sidebar-collapsed' : 'right-sidebar-expanded';
}, },
...@@ -67,11 +62,6 @@ export default { ...@@ -67,11 +62,6 @@ export default {
issuable-type="epic" issuable-type="epic"
/> />
</div> </div>
<issuable-sidebar <epic-sidebar />
v-if="isVueIssuableEpicSidebarEnabled"
:signed-in="isUserSignedIn"
:sidebar-status-class="sidebarStatusClass"
/>
<epic-sidebar v-else />
</div> </div>
</template> </template>
import { shallowMount } from '@vue/test-utils';
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
import Cookies from 'js-cookie';
import IssuableSidebarRoot from '~/issuable_sidebar/components/issuable_sidebar_root.vue';
const createComponent = (expanded = true) =>
shallowMount(IssuableSidebarRoot, {
propsData: {
expanded,
},
slots: {
'right-sidebar-items': `
<button class="js-todo">Todo</button>
`,
},
});
describe('IssuableSidebarRoot', () => {
let wrapper;
beforeEach(() => {
wrapper = createComponent();
});
afterEach(() => {
wrapper.destroy();
});
describe('watch', () => {
describe('isExpanded', () => {
it('emits `sidebar-toggle` event on component', async () => {
wrapper.setData({
isExpanded: false,
});
await wrapper.vm.$nextTick();
expect(wrapper.emitted('sidebar-toggle')).toBeTruthy();
expect(wrapper.emitted('sidebar-toggle')[0]).toEqual([
{
expanded: false,
},
]);
});
});
});
describe('methods', () => {
describe('updatePageContainerClass', () => {
beforeEach(() => {
setFixtures('<div class="layout-page"></div>');
});
it.each`
isExpanded | layoutPageClass
${true} | ${'right-sidebar-expanded'}
${false} | ${'right-sidebar-collapsed'}
`(
'set class $layoutPageClass to container element when `isExpanded` prop is $isExpanded',
async ({ isExpanded, layoutPageClass }) => {
wrapper.setData({
isExpanded,
});
await wrapper.vm.$nextTick();
wrapper.vm.updatePageContainerClass();
expect(document.querySelector('.layout-page').classList.contains(layoutPageClass)).toBe(
true,
);
},
);
});
describe('handleWindowResize', () => {
beforeEach(async () => {
wrapper.setData({
userExpanded: true,
});
await wrapper.vm.$nextTick();
});
it.each`
breakpoint | isExpandedValue
${'xs'} | ${false}
${'sm'} | ${false}
${'md'} | ${false}
${'lg'} | ${true}
${'xl'} | ${true}
`(
'sets `isExpanded` prop to $isExpandedValue only when current screen size is `lg` or `xl`',
async ({ breakpoint, isExpandedValue }) => {
jest.spyOn(bp, 'isDesktop').mockReturnValue(breakpoint === 'lg' || breakpoint === 'xl');
wrapper.vm.handleWindowResize();
expect(wrapper.vm.isExpanded).toBe(isExpandedValue);
},
);
it('calls `updatePageContainerClass` method', () => {
jest.spyOn(wrapper.vm, 'updatePageContainerClass');
wrapper.vm.handleWindowResize();
expect(wrapper.vm.updatePageContainerClass).toHaveBeenCalled();
});
});
describe('handleToggleSidebarClick', () => {
beforeEach(async () => {
jest.spyOn(Cookies, 'set').mockImplementation(jest.fn());
wrapper.setData({
isExpanded: true,
});
await wrapper.vm.$nextTick();
});
it('flips value of `isExpanded`', () => {
wrapper.vm.handleToggleSidebarClick();
expect(wrapper.vm.isExpanded).toBe(false);
expect(wrapper.vm.userExpanded).toBe(false);
});
it('updates "collapsed_gutter" cookie value', () => {
wrapper.vm.handleToggleSidebarClick();
expect(Cookies.set).toHaveBeenCalledWith('collapsed_gutter', true);
});
it('calls `updatePageContainerClass` method', () => {
jest.spyOn(wrapper.vm, 'updatePageContainerClass');
wrapper.vm.handleWindowResize();
expect(wrapper.vm.updatePageContainerClass).toHaveBeenCalled();
});
});
});
describe('template', () => {
describe('sidebar expanded', () => {
beforeEach(async () => {
wrapper.setData({
isExpanded: true,
});
await wrapper.vm.$nextTick();
});
it('renders component container element with class `right-sidebar-expanded` when `isExpanded` prop is true', () => {
expect(wrapper.classes()).toContain('right-sidebar-expanded');
});
it('renders sidebar toggle button with text and icon', () => {
const buttonEl = wrapper.find('button');
expect(buttonEl.exists()).toBe(true);
expect(buttonEl.attributes('title')).toBe('Toggle sidebar');
expect(buttonEl.find('span').text()).toBe('Collapse sidebar');
expect(buttonEl.find('[data-testid="icon-collapse"]').isVisible()).toBe(true);
});
});
describe('sidebar collapsed', () => {
beforeEach(async () => {
wrapper.setData({
isExpanded: false,
});
await wrapper.vm.$nextTick();
});
it('renders component container element with class `right-sidebar-collapsed` when `isExpanded` prop is false', () => {
expect(wrapper.classes()).toContain('right-sidebar-collapsed');
});
it('renders sidebar toggle button with text and icon', () => {
const buttonEl = wrapper.find('button');
expect(buttonEl.exists()).toBe(true);
expect(buttonEl.attributes('title')).toBe('Toggle sidebar');
expect(buttonEl.find('[data-testid="icon-expand"]').isVisible()).toBe(true);
});
});
it('renders sidebar items', () => {
const sidebarItemsEl = wrapper.find('[data-testid="sidebar-items"]');
expect(sidebarItemsEl.exists()).toBe(true);
expect(sidebarItemsEl.find('button.js-todo').exists()).toBe(true);
});
});
});
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment