summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/nav
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-05-19 15:44:42 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2021-05-19 15:44:42 +0000
commit4555e1b21c365ed8303ffb7a3325d773c9b8bf31 (patch)
tree5423a1c7516cffe36384133ade12572cf709398d /app/assets/javascripts/nav
parente570267f2f6b326480d284e0164a6464ba4081bc (diff)
downloadgitlab-ce-4555e1b21c365ed8303ffb7a3325d773c9b8bf31.tar.gz
Add latest changes from gitlab-org/gitlab@13-12-stable-eev13.12.0-rc42
Diffstat (limited to 'app/assets/javascripts/nav')
-rw-r--r--app/assets/javascripts/nav/components/top_nav_app.vue59
-rw-r--r--app/assets/javascripts/nav/components/top_nav_container_view.vue74
-rw-r--r--app/assets/javascripts/nav/components/top_nav_dropdown_menu.vue144
-rw-r--r--app/assets/javascripts/nav/components/top_nav_menu_item.vue31
-rw-r--r--app/assets/javascripts/nav/index.js12
-rw-r--r--app/assets/javascripts/nav/mount.js23
-rw-r--r--app/assets/javascripts/nav/stores/index.js4
7 files changed, 347 insertions, 0 deletions
diff --git a/app/assets/javascripts/nav/components/top_nav_app.vue b/app/assets/javascripts/nav/components/top_nav_app.vue
new file mode 100644
index 00000000000..f8f3ba26536
--- /dev/null
+++ b/app/assets/javascripts/nav/components/top_nav_app.vue
@@ -0,0 +1,59 @@
+<script>
+import { GlNav, GlNavItemDropdown, GlDropdownForm, GlTooltip } from '@gitlab/ui';
+import { s__ } from '~/locale';
+import TopNavDropdownMenu from './top_nav_dropdown_menu.vue';
+
+const TOOLTIP = s__('TopNav|Switch to...');
+
+export default {
+ components: {
+ GlNav,
+ GlNavItemDropdown,
+ GlDropdownForm,
+ GlTooltip,
+ TopNavDropdownMenu,
+ },
+ props: {
+ navData: {
+ type: Object,
+ required: true,
+ },
+ },
+ methods: {
+ findTooltipTarget() {
+ // ### Why use a target function instead of `v-gl-tooltip`?
+ // To get the tooltip to align correctly, we need it to target the actual
+ // toggle button which we don't directly render.
+ return this.$el.querySelector('.js-top-nav-dropdown-toggle');
+ },
+ },
+ TOOLTIP,
+};
+</script>
+
+<template>
+ <gl-nav class="navbar-sub-nav">
+ <gl-nav-item-dropdown
+ :text="navData.activeTitle"
+ icon="dot-grid"
+ menu-class="gl-mt-3! gl-max-w-none! gl-max-h-none! gl-sm-w-auto!"
+ toggle-class="top-nav-toggle js-top-nav-dropdown-toggle gl-px-3!"
+ no-flip
+ >
+ <gl-dropdown-form>
+ <top-nav-dropdown-menu
+ :primary="navData.primary"
+ :secondary="navData.secondary"
+ :views="navData.views"
+ />
+ </gl-dropdown-form>
+ </gl-nav-item-dropdown>
+ <gl-tooltip
+ boundary="window"
+ :boundary-padding="0"
+ :target="findTooltipTarget"
+ placement="right"
+ :title="$options.TOOLTIP"
+ />
+ </gl-nav>
+</template>
diff --git a/app/assets/javascripts/nav/components/top_nav_container_view.vue b/app/assets/javascripts/nav/components/top_nav_container_view.vue
new file mode 100644
index 00000000000..21ff3ebcd7d
--- /dev/null
+++ b/app/assets/javascripts/nav/components/top_nav_container_view.vue
@@ -0,0 +1,74 @@
+<script>
+import FrequentItemsApp from '~/frequent_items/components/app.vue';
+import eventHub from '~/frequent_items/event_hub';
+import VuexModuleProvider from '~/vue_shared/components/vuex_module_provider.vue';
+import TopNavMenuItem from './top_nav_menu_item.vue';
+
+export default {
+ components: {
+ FrequentItemsApp,
+ TopNavMenuItem,
+ VuexModuleProvider,
+ },
+ props: {
+ frequentItemsVuexModule: {
+ type: String,
+ required: true,
+ },
+ frequentItemsDropdownType: {
+ type: String,
+ required: true,
+ },
+ linksPrimary: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ linksSecondary: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ },
+ computed: {
+ linkGroups() {
+ return [
+ { key: 'primary', links: this.linksPrimary },
+ { key: 'secondary', links: this.linksSecondary },
+ ].filter((x) => x.links?.length);
+ },
+ },
+ mounted() {
+ // For historic reasons, the frequent-items-app component requires this too start up.
+ this.$nextTick(() => {
+ eventHub.$emit(`${this.frequentItemsDropdownType}-dropdownOpen`);
+ });
+ },
+};
+</script>
+
+<template>
+ <div class="top-nav-container-view gl-display-flex gl-flex-direction-column">
+ <div class="frequent-items-dropdown-container gl-w-auto">
+ <div class="frequent-items-dropdown-content gl-w-full! gl-pt-0!">
+ <vuex-module-provider :vuex-module="frequentItemsVuexModule">
+ <frequent-items-app v-bind="$attrs" />
+ </vuex-module-provider>
+ </div>
+ </div>
+ <div
+ v-for="({ key, links }, groupIndex) in linkGroups"
+ :key="key"
+ :class="{ 'gl-mt-3': groupIndex !== 0 }"
+ class="gl-mt-auto gl-pt-3 gl-border-1 gl-border-t-solid gl-border-gray-100"
+ data-testid="menu-item-group"
+ >
+ <top-nav-menu-item
+ v-for="(link, linkIndex) in links"
+ :key="link.title"
+ :menu-item="link"
+ :class="{ 'gl-mt-1': linkIndex !== 0 }"
+ />
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/nav/components/top_nav_dropdown_menu.vue b/app/assets/javascripts/nav/components/top_nav_dropdown_menu.vue
new file mode 100644
index 00000000000..1cbd64b501d
--- /dev/null
+++ b/app/assets/javascripts/nav/components/top_nav_dropdown_menu.vue
@@ -0,0 +1,144 @@
+<script>
+import { FREQUENT_ITEMS_PROJECTS, FREQUENT_ITEMS_GROUPS } from '~/frequent_items/constants';
+import KeepAliveSlots from '~/vue_shared/components/keep_alive_slots.vue';
+import TopNavContainerView from './top_nav_container_view.vue';
+import TopNavMenuItem from './top_nav_menu_item.vue';
+
+const ACTIVE_CLASS = 'gl-shadow-none! gl-font-weight-bold! active';
+const SECONDARY_GROUP_CLASS = 'gl-pt-3 gl-mt-3 gl-border-1 gl-border-t-solid gl-border-gray-100';
+
+export default {
+ components: {
+ KeepAliveSlots,
+ TopNavContainerView,
+ TopNavMenuItem,
+ },
+ props: {
+ primary: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ secondary: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ views: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
+ },
+ data() {
+ return {
+ activeId: '',
+ };
+ },
+ computed: {
+ menuItemGroups() {
+ return [
+ { key: 'primary', items: this.primary, classes: '' },
+ {
+ key: 'secondary',
+ items: this.secondary,
+ classes: SECONDARY_GROUP_CLASS,
+ },
+ ].filter((x) => x.items?.length);
+ },
+ allMenuItems() {
+ return this.menuItemGroups.flatMap((x) => x.items);
+ },
+ activeMenuItem() {
+ return this.allMenuItems.find((x) => x.id === this.activeId);
+ },
+ activeView() {
+ return this.activeMenuItem?.view;
+ },
+ menuClass() {
+ if (!this.activeView) {
+ return 'gl-w-full';
+ }
+
+ return '';
+ },
+ },
+ created() {
+ // Initialize activeId based on initialization prop
+ this.activeId = this.allMenuItems.find((x) => x.active)?.id;
+ },
+ methods: {
+ onClick({ id, href }) {
+ // If we're a link, let's just do the default behavior so the view won't change
+ if (href) {
+ return;
+ }
+
+ this.activeId = id;
+ },
+ menuItemClasses(menuItem) {
+ if (menuItem.id === this.activeId) {
+ return ACTIVE_CLASS;
+ }
+
+ return '';
+ },
+ },
+ FREQUENT_ITEMS_PROJECTS,
+ FREQUENT_ITEMS_GROUPS,
+ // expose for unit tests
+ ACTIVE_CLASS,
+ SECONDARY_GROUP_CLASS,
+};
+</script>
+
+<template>
+ <div class="gl-display-flex gl-align-items-stretch">
+ <div
+ class="gl-w-grid-size-30 gl-flex-shrink-0 gl-bg-gray-10"
+ :class="menuClass"
+ data-testid="menu-sidebar"
+ >
+ <div
+ class="gl-py-3 gl-px-5 gl-h-full gl-display-flex gl-align-items-stretch gl-flex-direction-column"
+ >
+ <div
+ v-for="group in menuItemGroups"
+ :key="group.key"
+ :class="group.classes"
+ data-testid="menu-item-group"
+ >
+ <top-nav-menu-item
+ v-for="(menu, index) in group.items"
+ :key="menu.id"
+ data-testid="menu-item"
+ :class="[{ 'gl-mt-1': index !== 0 }, menuItemClasses(menu)]"
+ :menu-item="menu"
+ @click="onClick(menu)"
+ />
+ </div>
+ </div>
+ </div>
+ <keep-alive-slots
+ v-show="activeView"
+ :slot-key="activeView"
+ class="gl-w-grid-size-40 gl-overflow-hidden gl-py-3 gl-px-5"
+ data-testid="menu-subview"
+ >
+ <template #projects>
+ <top-nav-container-view
+ :frequent-items-dropdown-type="$options.FREQUENT_ITEMS_PROJECTS.namespace"
+ :frequent-items-vuex-module="$options.FREQUENT_ITEMS_PROJECTS.vuexModule"
+ v-bind="views.projects"
+ />
+ </template>
+ <template #groups>
+ <top-nav-container-view
+ :frequent-items-dropdown-type="$options.FREQUENT_ITEMS_GROUPS.namespace"
+ :frequent-items-vuex-module="$options.FREQUENT_ITEMS_GROUPS.vuexModule"
+ v-bind="views.groups"
+ />
+ </template>
+ </keep-alive-slots>
+ </div>
+</template>
diff --git a/app/assets/javascripts/nav/components/top_nav_menu_item.vue b/app/assets/javascripts/nav/components/top_nav_menu_item.vue
new file mode 100644
index 00000000000..a0d92811a6f
--- /dev/null
+++ b/app/assets/javascripts/nav/components/top_nav_menu_item.vue
@@ -0,0 +1,31 @@
+<script>
+import { GlButton, GlIcon } from '@gitlab/ui';
+
+export default {
+ components: {
+ GlButton,
+ GlIcon,
+ },
+ props: {
+ menuItem: {
+ type: Object,
+ required: true,
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-button
+ category="tertiary"
+ :href="menuItem.href"
+ class="top-nav-menu-item gl-display-block"
+ v-on="$listeners"
+ >
+ <span class="gl-display-flex">
+ <gl-icon v-if="menuItem.icon" :name="menuItem.icon" class="gl-mr-2!" />
+ {{ menuItem.title }}
+ <gl-icon v-if="menuItem.view" name="chevron-right" class="gl-ml-auto" />
+ </span>
+ </gl-button>
+</template>
diff --git a/app/assets/javascripts/nav/index.js b/app/assets/javascripts/nav/index.js
new file mode 100644
index 00000000000..646ce3f0ecf
--- /dev/null
+++ b/app/assets/javascripts/nav/index.js
@@ -0,0 +1,12 @@
+export const initTopNav = async () => {
+ const el = document.getElementById('js-top-nav');
+
+ if (!el) {
+ return;
+ }
+
+ // With combined_menu feature flag, there's a benefit to splitting up the import
+ const { mountTopNav } = await import(/* webpackChunkName: 'top_nav' */ './mount');
+
+ mountTopNav(el);
+};
diff --git a/app/assets/javascripts/nav/mount.js b/app/assets/javascripts/nav/mount.js
new file mode 100644
index 00000000000..0d46ff56249
--- /dev/null
+++ b/app/assets/javascripts/nav/mount.js
@@ -0,0 +1,23 @@
+import Vue from 'vue';
+import Vuex from 'vuex';
+import App from './components/top_nav_app.vue';
+import { createStore } from './stores';
+
+Vue.use(Vuex);
+
+export const mountTopNav = (el) => {
+ const viewModel = JSON.parse(el.dataset.viewModel);
+ const store = createStore();
+
+ return new Vue({
+ el,
+ store,
+ render(h) {
+ return h(App, {
+ props: {
+ navData: viewModel,
+ },
+ });
+ },
+ });
+};
diff --git a/app/assets/javascripts/nav/stores/index.js b/app/assets/javascripts/nav/stores/index.js
new file mode 100644
index 00000000000..527bbdd5c3f
--- /dev/null
+++ b/app/assets/javascripts/nav/stores/index.js
@@ -0,0 +1,4 @@
+import Vuex from 'vuex';
+import { createStoreOptions } from '~/frequent_items/store';
+
+export const createStore = () => new Vuex.Store(createStoreOptions());