Skip to content
Snippets Groups Projects
Verified Commit bb5d7e32 authored by Thomas Hutterer's avatar Thomas Hutterer :palm_tree: Committed by GitLab
Browse files

Detect usage of pinned nav item and handle section expanded state

If a pinned nav item was used, the Pinned section will show as
expanded and the item's original section shows as collapsed.

If an item was clicked in its original section, this section shows as
expanded. As before, the pinned section still can show as expanded
because we remember its state seperately in a cookie.

Changelog: changed
parent 629a1255
No related branches found
No related tags found
2 merge requests!144312Change service start (cut-off) date for code suggestions to March 15th,!142430Detect usage of pinned nav item and handle section expanded state
Showing
with 158 additions and 26 deletions
......@@ -141,6 +141,7 @@ export default {
:is-flyout="true"
@pin-add="(itemId, itemTitle) => $emit('pin-add', itemId, itemTitle)"
@pin-remove="(itemId, itemTitle) => $emit('pin-remove', itemId, itemTitle)"
@nav-link-click="$emit('nav-link-click')"
/>
</ul>
<svg
......
......@@ -150,6 +150,7 @@ export default {
@mouseleave="isMouseOverFlyout = false"
@pin-add="(itemId, itemTitle) => $emit('pin-add', itemId, itemTitle)"
@pin-remove="(itemId, itemTitle) => $emit('pin-remove', itemId, itemTitle)"
@nav-link-click="$emit('nav-link-click')"
/>
<gl-collapse
......
......@@ -181,7 +181,11 @@ export default {
},
mounted() {
if (this.item.is_active) {
this.$el.scrollIntoView(false);
this.$el.scrollIntoView({
behavior: 'instant',
block: 'center',
inline: 'nearest',
});
}
eventHub.$on('updatePillValue', this.updatePillValue);
......@@ -226,6 +230,7 @@ export default {
class="super-sidebar-nav-item gl-relative gl-display-flex gl-align-items-center gl-min-h-7 gl-gap-3 gl-mb-1 gl-py-2 gl-text-black-normal! gl-text-decoration-none! gl-focus--focus show-on-focus-or-hover--control hide-on-focus-or-hover--control"
:class="computedLinkClasses"
data-testid="nav-item-link"
@nav-link-click="$emit('nav-link-click')"
>
<div
:class="[isActive ? 'gl-opacity-10' : 'gl-opacity-0']"
......
......@@ -29,7 +29,7 @@ export default {
</script>
<template>
<a v-bind="linkProps" :class="computedLinkClasses">
<a v-bind="linkProps" :class="computedLinkClasses" @click="$emit('nav-link-click')">
<slot :is-active="isActive"></slot>
</a>
</template>
......@@ -2,7 +2,11 @@
import Draggable from 'vuedraggable';
import { s__ } from '~/locale';
import { setCookie, getCookie } from '~/lib/utils/common_utils';
import { SIDEBAR_PINS_EXPANDED_COOKIE, SIDEBAR_COOKIE_EXPIRATION } from '../constants';
import {
PINNED_NAV_STORAGE_KEY,
SIDEBAR_PINS_EXPANDED_COOKIE,
SIDEBAR_COOKIE_EXPIRATION,
} from '../constants';
import MenuSection from './menu_section.vue';
import NavItem from './nav_item.vue';
......@@ -35,22 +39,24 @@ export default {
required: false,
default: false,
},
wasPinnedNav: {
type: Boolean,
required: false,
default: false,
},
},
data() {
return {
expanded: getCookie(SIDEBAR_PINS_EXPANDED_COOKIE) !== 'false',
expanded: getCookie(SIDEBAR_PINS_EXPANDED_COOKIE) !== 'false' || this.wasPinnedNav,
draggableItems: this.renameSettings(this.items),
};
},
computed: {
isActive() {
return this.items.some((item) => item.is_active);
},
sectionItem() {
return {
title: this.$options.i18n.pinned,
icon: 'thumbtack',
is_active: this.isActive,
is_active: this.wasPinnedNav,
items: this.draggableItems,
};
},
......@@ -87,6 +93,9 @@ export default {
onPinRemove(itemId, itemTitle) {
this.$emit('pin-remove', itemId, itemTitle);
},
writePinnedClick() {
sessionStorage.setItem(PINNED_NAV_STORAGE_KEY, true);
},
},
};
</script>
......@@ -98,6 +107,7 @@ export default {
:has-flyout="hasFlyout"
@collapse-toggle="expanded = !expanded"
@pin-remove="onPinRemove"
@nav-link-click="writePinnedClick"
>
<draggable
v-if="items.length > 0"
......@@ -114,6 +124,7 @@ export default {
:item="item"
is-in-pinned-section
@pin-remove="onPinRemove(item.id, item.title)"
@nav-link-click="writePinnedClick"
/>
</draggable>
<li
......
......@@ -4,7 +4,7 @@ import { s__, sprintf } from '~/locale';
import * as Sentry from '~/sentry/sentry_browser_wrapper';
import axios from '~/lib/utils/axios_utils';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { PANELS_WITH_PINS } from '../constants';
import { PANELS_WITH_PINS, PINNED_NAV_STORAGE_KEY } from '../constants';
import NavItem from './nav_item.vue';
import PinnedSection from './pinned_section.vue';
import MenuSection from './menu_section.vue';
......@@ -58,6 +58,10 @@ export default {
return {
showFlyoutMenus: false,
// This is used to detect if user came to this page by clicking a
// nav item in the pinned section.
wasPinnedNav: this.readAndResetPinnedNav(),
// This is used as a provide and injected into the nav items.
// Note: It has to be an object to be reactive.
changedPinnedItemIds: { ids: this.pinnedItemIds },
......@@ -73,17 +77,15 @@ export default {
},
// Returns only the items that aren't static at the top and makes sure no
// section shows as active (and expanded) when one of its items is pinned.
// section shows as active (and expanded) when a pinned nav item was used.
nonStaticItems() {
if (!this.supportsPins) return this.items;
return this.items
.filter((item) => item.items && item.items.length > 0)
.map((item) => {
const hasActivePinnedChild = item.items.some((childItem) => {
return childItem.is_active && this.changedPinnedItemIds.ids.includes(childItem.id);
});
const showAsActive = item.is_active && !hasActivePinnedChild;
const showAsActive = item.is_active && !this.wasPinnedNav;
return { ...item, is_active: showAsActive };
});
......@@ -166,6 +168,11 @@ export default {
decideFlyoutState() {
this.showFlyoutMenus = GlBreakpointInstance.windowWidth() >= breakpoints.md;
},
readAndResetPinnedNav() {
const wasPinnedNav = sessionStorage.getItem(PINNED_NAV_STORAGE_KEY);
sessionStorage.removeItem(PINNED_NAV_STORAGE_KEY);
return wasPinnedNav === 'true';
},
},
};
</script>
......@@ -183,6 +190,7 @@ export default {
v-if="supportsPins"
:items="pinnedItems"
:has-flyout="showFlyoutMenus"
:was-pinned-nav="wasPinnedNav"
@pin-remove="destroyPin"
@pin-reorder="movePin"
/>
......
......@@ -52,6 +52,8 @@ export const HELP_MENU_TRACKING_DEFAULTS = {
export const SIDEBAR_PINS_EXPANDED_COOKIE = 'sidebar_pinned_section_expanded';
export const SIDEBAR_COOKIE_EXPIRATION = 365 * 10;
export const PINNED_NAV_STORAGE_KEY = 'super-sidebar-pinned-nav-item-clicked';
export const DROPDOWN_Y_OFFSET = 4;
export const NAV_ITEM_LINK_ACTIVE_CLASS = 'super-sidebar-nav-item-current';
......
......@@ -15,7 +15,6 @@
it 'has a `Issue boards` item' do
within_testid 'super-sidebar' do
click_button 'Plan'
expect(page).to have_link 'Issue boards'
end
end
......
......@@ -28,6 +28,7 @@
it 'ignores archived merge request count badges in navbar', :js do
within_testid('super-sidebar') do
click_on 'Pinned' # to close the Pinned section to only have one match
expect(find_link(text: 'Merge requests').find('.badge').text).to eq("1")
end
end
......
......@@ -173,7 +173,9 @@
before do
# we can't go directly to the commits page since it doesn't load discussions
visit project_merge_request_path(project, merge_request)
click_link 'Commits'
within '.merge-request-tabs' do
click_link 'Commits'
end
end
it_behaves_like 'a page with no code discussions'
......
......@@ -4,6 +4,7 @@
RSpec.describe 'Navigation menu item pinning', :js, feature_category: :navigation do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project) }
before do
sign_in(user)
......@@ -28,8 +29,6 @@
end
describe 'pinnable navigation menu' do
let_it_be(:project) { create(:project) }
before do
project.add_member(user, :owner)
visit project_path(project)
......@@ -166,6 +165,56 @@
end
end
describe 'section collapse states after using a pinned item to navigate' do
before do
project.add_member(user, :owner)
visit project_path(project)
end
context 'when a pinned item is clicked in the Pinned section' do
before do
within '[data-testid="pinned-nav-items"]' do
click_on 'Issues'
end
end
it 'shows the Pinned section as expanded' do
within '[data-testid="pinned-nav-items"]' do
expect(page).to have_link 'Issues'
end
end
it 'shows the original section as collapsed' do
within '#menu-section-button-plan' do
expect(page).not_to have_link 'Issues'
end
end
end
context 'when a pinned item is clicked in its original section' do
before do
within '#super-sidebar' do
click_on 'Plan'
end
within '#super-sidebar #plan' do
click_on 'Issues'
end
end
it 'shows the Pinned section as collapsed' do
within '#menu-section-button-plan' do
expect(page).not_to have_link 'Issues'
end
end
it 'shows the original section as expanded' do
within '#super-sidebar #plan' do
expect(page).to have_link 'Issues'
end
end
end
end
private
def add_pin(nav_item_title)
......
......@@ -71,13 +71,10 @@ def click_tab(title)
visit project_issues_path(project)
end
it_behaves_like 'page has active tab', 'Pinned'
it_behaves_like 'page has active tab', 'Plan'
context "on project Code/Milestones" do
context 'on project Plan/Milestones' do
before do
within_testid('super-sidebar') do
click_button("Plan")
end
click_tab('Milestones')
end
......
......@@ -290,7 +290,11 @@ describe('NavItem component', () => {
createWrapper({
item: { is_active: true },
});
expect(wrapper.element.scrollIntoView).toHaveBeenNthCalledWith(1, false);
expect(wrapper.element.scrollIntoView).toHaveBeenNthCalledWith(1, {
behavior: 'instant',
block: 'center',
inline: 'nearest',
});
});
});
......
......@@ -4,7 +4,12 @@ import { mountExtended } from 'helpers/vue_test_utils_helper';
import PinnedSection from '~/super_sidebar/components/pinned_section.vue';
import MenuSection from '~/super_sidebar/components/menu_section.vue';
import NavItem from '~/super_sidebar/components/nav_item.vue';
import { SIDEBAR_PINS_EXPANDED_COOKIE, SIDEBAR_COOKIE_EXPIRATION } from '~/super_sidebar/constants';
import NavItemLink from '~/super_sidebar/components/nav_item_link.vue';
import {
PINNED_NAV_STORAGE_KEY,
SIDEBAR_PINS_EXPANDED_COOKIE,
SIDEBAR_COOKIE_EXPIRATION,
} from '~/super_sidebar/constants';
import { setCookie } from '~/lib/utils/common_utils';
jest.mock('@floating-ui/dom');
......@@ -74,6 +79,17 @@ describe('PinnedSection component', () => {
});
});
});
describe('when a pinned nav item was used before', () => {
beforeEach(() => {
Cookies.set(SIDEBAR_PINS_EXPANDED_COOKIE, 'false');
createWrapper({ wasPinnedNav: true });
});
it('is expanded', () => {
expect(wrapper.findComponent(NavItem).isVisible()).toBe(true);
});
});
});
describe('hasFlyout prop', () => {
......@@ -116,4 +132,16 @@ describe('PinnedSection component', () => {
]);
});
});
describe('click on a pinned nav item', () => {
beforeEach(() => {
createWrapper();
});
it('stores pinned nav usage in sessionStorage', () => {
expect(window.sessionStorage.getItem(PINNED_NAV_STORAGE_KEY)).toBe(null);
wrapper.findComponent(NavItemLink).vm.$emit('nav-link-click');
expect(window.sessionStorage.getItem(PINNED_NAV_STORAGE_KEY)).toBe('true');
});
});
});
......@@ -4,7 +4,7 @@ import SidebarMenu from '~/super_sidebar/components/sidebar_menu.vue';
import PinnedSection from '~/super_sidebar/components/pinned_section.vue';
import NavItem from '~/super_sidebar/components/nav_item.vue';
import MenuSection from '~/super_sidebar/components/menu_section.vue';
import { PANELS_WITH_PINS } from '~/super_sidebar/constants';
import { PANELS_WITH_PINS, PINNED_NAV_STORAGE_KEY } from '~/super_sidebar/constants';
import { sidebarData } from '../mock_data';
const menuItems = [
......@@ -175,4 +175,28 @@ describe('Sidebar Menu', () => {
expect(findMainMenuSeparator().exists()).toBe(false);
});
});
describe('Detect if pinned nav item was used', () => {
describe('when sessionStorage is "true"', () => {
beforeEach(() => {
window.sessionStorage.setItem(PINNED_NAV_STORAGE_KEY, 'true');
createWrapper({ panelType: 'project' });
});
it('sets prop for pinned section to true', () => {
expect(findPinnedSection().props('wasPinnedNav')).toBe(true);
});
});
describe('when sessionStorage is null', () => {
beforeEach(() => {
window.sessionStorage.setItem(PINNED_NAV_STORAGE_KEY, null);
createWrapper({ panelType: 'project' });
});
it('sets prop for pinned section to false', () => {
expect(findPinnedSection().props('wasPinnedNav')).toBe(false);
});
});
});
});
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment