diff options
78 files changed, 697 insertions, 221 deletions
diff --git a/.rubocop_todo/layout/empty_line_after_magic_comment.yml b/.rubocop_todo/layout/empty_line_after_magic_comment.yml index 9c7b9f2c1a8..07353ea456c 100644 --- a/.rubocop_todo/layout/empty_line_after_magic_comment.yml +++ b/.rubocop_todo/layout/empty_line_after_magic_comment.yml @@ -555,7 +555,6 @@ Layout/EmptyLineAfterMagicComment: - 'spec/components/previews/pajamas/card_component_preview.rb' - 'spec/components/previews/pajamas/spinner_component_preview.rb' - 'spec/controllers/application_controller_spec.rb' - - 'spec/controllers/concerns/analytics/cycle_analytics/value_stream_actions_spec.rb' - 'spec/controllers/projects/jobs_controller_spec.rb' - 'spec/controllers/projects/merge_requests/drafts_controller_spec.rb' - 'spec/factories/airflow/dags.rb' diff --git a/app/assets/javascripts/batch_comments/components/preview_dropdown.vue b/app/assets/javascripts/batch_comments/components/preview_dropdown.vue index 4ac0c8c4894..ca9cb03ca37 100644 --- a/app/assets/javascripts/batch_comments/components/preview_dropdown.vue +++ b/app/assets/javascripts/batch_comments/components/preview_dropdown.vue @@ -55,21 +55,25 @@ export default { <template> <gl-disclosure-dropdown :items="listItems" dropup data-qa-selector="review_preview_dropdown"> <template #toggle> - <gl-button - >{{ __('Pending comments') }} <drafts-count variant="neutral" /><gl-icon - class="dropdown-chevron" - name="chevron-up" - /></gl-button> + <gl-button> + {{ __('Pending comments') }} + <drafts-count variant="neutral" /> + <gl-icon class="dropdown-chevron" name="chevron-up" /> + </gl-button> </template> <template #header> - <p class="gl-dropdown-header-top"> - {{ n__('%d pending comment', '%d pending comments', draftsCount) }} - </p> + <div + class="gl-display-flex gl-align-items-center gl-p-4! gl-min-h-8 gl-border-b-1 gl-border-b-solid gl-border-b-gray-200" + > + <span class="gl-flex-grow-1 gl-font-weight-bold gl-font-sm gl-pr-2"> + {{ n__('%d pending comment', '%d pending comments', draftsCount) }} + </span> + </div> </template> <template #list-item="{ item }"> - <preview-item :draft="item" :is-last="item.last" @click="onClickDraft(item)" /> + <preview-item :draft="item" :is-last="item.last" /> </template> </gl-disclosure-dropdown> </template> diff --git a/app/assets/javascripts/layout_nav.js b/app/assets/javascripts/layout_nav.js index b8138f34d45..f5078962b8f 100644 --- a/app/assets/javascripts/layout_nav.js +++ b/app/assets/javascripts/layout_nav.js @@ -70,10 +70,11 @@ function initDeferred() { } export default function initLayoutNav() { - const contextualSidebar = new ContextualSidebar(); - contextualSidebar.bindEvents(); - - initFlyOutNav(); + if (!gon.use_new_navigation) { + const contextualSidebar = new ContextualSidebar(); + contextualSidebar.bindEvents(); + initFlyOutNav(); + } requestIdleCallback(initDeferred); } diff --git a/app/assets/javascripts/pages/groups/new/components/app.vue b/app/assets/javascripts/pages/groups/new/components/app.vue index f01e5e595a3..68ddcf3d868 100644 --- a/app/assets/javascripts/pages/groups/new/components/app.vue +++ b/app/assets/javascripts/pages/groups/new/components/app.vue @@ -2,7 +2,7 @@ import importGroupIllustration from '@gitlab/svgs/dist/illustrations/group-import.svg'; import newGroupIllustration from '@gitlab/svgs/dist/illustrations/group-new.svg'; -import { __, s__ } from '~/locale'; +import { s__ } from '~/locale'; import NewNamespacePage from '~/vue_shared/new_namespace/new_namespace_page.vue'; import createGroupDescriptionDetails from './create_group_description_details.vue'; @@ -11,6 +11,11 @@ export default { NewNamespacePage, }, props: { + parentGroupUrl: { + type: String, + required: false, + default: null, + }, parentGroupName: { type: String, required: false, @@ -28,8 +33,13 @@ export default { }, }, computed: { - initialBreadcrumb() { - return this.parentGroupName || __('New group'); + initialBreadcrumbs() { + return this.parentGroupUrl + ? [ + { text: this.parentGroupName, href: this.parentGroupUrl }, + { text: s__('GroupsNew|New subgroup'), href: '#' }, + ] + : [{ text: s__('GroupsNew|New group'), href: '#' }]; }, panels() { return [ @@ -68,7 +78,7 @@ export default { <template> <new-namespace-page :jump-to-last-persisted-panel="hasErrors" - :initial-breadcrumb="initialBreadcrumb" + :initial-breadcrumbs="initialBreadcrumbs" :panels="panels" :title="s__('GroupsNew|Create new group')" persistence-key="new_group_last_active_tab" diff --git a/app/assets/javascripts/pages/groups/new/index.js b/app/assets/javascripts/pages/groups/new/index.js index a555038ed5c..acaee097dc1 100644 --- a/app/assets/javascripts/pages/groups/new/index.js +++ b/app/assets/javascripts/pages/groups/new/index.js @@ -22,6 +22,7 @@ initFilePickers(); function initNewGroupCreation(el) { const { hasErrors, + parentGroupUrl, parentGroupName, importExistingGroupPath, verificationRequired, @@ -30,6 +31,7 @@ function initNewGroupCreation(el) { } = el.dataset; const props = { + parentGroupUrl, parentGroupName, importExistingGroupPath, hasErrors: parseBoolean(hasErrors), diff --git a/app/assets/javascripts/projects/new/components/app.vue b/app/assets/javascripts/projects/new/components/app.vue index 3100029eb31..4f3924b04bf 100644 --- a/app/assets/javascripts/projects/new/components/app.vue +++ b/app/assets/javascripts/projects/new/components/app.vue @@ -59,6 +59,16 @@ export default { SafeHtml, }, props: { + parentGroupUrl: { + type: String, + required: false, + default: null, + }, + parentGroupName: { + type: String, + required: false, + default: '', + }, hasErrors: { type: Boolean, required: false, @@ -77,6 +87,12 @@ export default { }, computed: { + initialBreadcrumbs() { + return [ + this.parentGroupUrl && { text: this.parentGroupName, href: this.parentGroupUrl }, + { text: s__('ProjectsNew|New project'), href: '#' }, + ].filter(Boolean); + }, availablePanels() { return this.isCiCdAvailable ? PANELS : PANELS.filter((p) => p.name !== CI_CD_PANEL); }, @@ -95,7 +111,7 @@ export default { <template> <new-namespace-page - :initial-breadcrumb="__('New project')" + :initial-breadcrumbs="initialBreadcrumbs" :panels="availablePanels" :jump-to-last-persisted-panel="hasErrors" :title="s__('ProjectsNew|Create new project')" diff --git a/app/assets/javascripts/projects/new/index.js b/app/assets/javascripts/projects/new/index.js index 910244c657b..0b190b6a88b 100644 --- a/app/assets/javascripts/projects/new/index.js +++ b/app/assets/javascripts/projects/new/index.js @@ -15,12 +15,16 @@ export function initNewProjectCreation() { newProjectGuidelines, hasErrors, isCiCdAvailable, + parentGroupUrl, + parentGroupName, } = el.dataset; const props = { hasErrors: parseBoolean(hasErrors), isCiCdAvailable: parseBoolean(isCiCdAvailable), newProjectGuidelines, + parentGroupUrl, + parentGroupName, }; const provide = { diff --git a/app/assets/javascripts/super_sidebar/components/help_center.vue b/app/assets/javascripts/super_sidebar/components/help_center.vue index 8e7c7efa631..ee4c50e7705 100644 --- a/app/assets/javascripts/super_sidebar/components/help_center.vue +++ b/app/assets/javascripts/super_sidebar/components/help_center.vue @@ -96,12 +96,6 @@ export default { return true; }, - handleAction({ action }) { - if (action) { - action(); - } - }, - showKeyboardShortcuts() { this.$refs.dropdown.close(); window?.toggleShortcutsHelp(); @@ -140,11 +134,7 @@ export default { :group="itemGroups.versionCheck" > <template #list-item="{ item }"> - <a - :href="item.href" - tabindex="-1" - class="gl-display-flex gl-flex-direction-column gl-line-height-24 gl-text-gray-900 gl-hover-text-gray-900 gl-hover-text-decoration-none" - > + <span class="gl-display-flex gl-flex-direction-column gl-line-height-24"> <span class="gl-font-sm gl-font-weight-bold"> {{ item.text }} <gl-emoji data-name="rocket" /> @@ -153,7 +143,7 @@ export default { <span class="gl-mr-2">{{ item.version }}</span> <gitlab-version-check-badge v-if="updateSeverity" :status="updateSeverity" size="sm" /> </span> - </a> + </span> </template> </gl-disclosure-dropdown-group> @@ -162,16 +152,15 @@ export default { :bordered="sidebarData.show_version_check" /> - <gl-disclosure-dropdown-group :group="itemGroups.helpActions" bordered @action="handleAction"> + <gl-disclosure-dropdown-group :group="itemGroups.helpActions" bordered> <template #list-item="{ item }"> - <button - tabindex="-1" - class="gl-bg-transparent gl-w-full gl-border-none gl-display-flex gl-justify-content-space-between gl-p-0 gl-text-gray-900" + <span + class="gl-display-flex gl-justify-content-space-between gl-align-items-center gl-my-n1" > {{ item.text }} <gl-badge v-if="item.count" pill size="sm" variant="info">{{ item.count }}</gl-badge> <kbd v-else-if="item.shortcut" class="flat">?</kbd> - </button> + </span> </template> </gl-disclosure-dropdown-group> </gl-disclosure-dropdown> diff --git a/app/assets/javascripts/super_sidebar/components/merge_request_menu.vue b/app/assets/javascripts/super_sidebar/components/merge_request_menu.vue index edc13e305cf..94fc6aedcc0 100644 --- a/app/assets/javascripts/super_sidebar/components/merge_request_menu.vue +++ b/app/assets/javascripts/super_sidebar/components/merge_request_menu.vue @@ -12,29 +12,19 @@ export default { required: true, }, }, - methods: { - navigate() { - this.$refs.link.click(); - }, - }, }; </script> <template> - <gl-disclosure-dropdown :items="items" placement="center" @action="navigate"> + <gl-disclosure-dropdown :items="items" placement="center"> <template #toggle> <slot></slot> </template> <template #list-item="{ item }"> - <a - ref="link" - class="gl-display-flex gl-align-items-center gl-justify-content-space-between gl-hover-text-gray-900 gl-hover-text-decoration-none gl-text-gray-900" - :href="item.href" - tabindex="-1" - > + <span class="gl-display-flex gl-align-items-center gl-justify-content-space-between"> {{ item.text }} <gl-badge pill size="sm" variant="neutral">{{ item.count || 0 }}</gl-badge> - </a> + </span> </template> </gl-disclosure-dropdown> </template> diff --git a/app/assets/javascripts/super_sidebar/components/super_sidebar.vue b/app/assets/javascripts/super_sidebar/components/super_sidebar.vue index cff68258a35..309299855d7 100644 --- a/app/assets/javascripts/super_sidebar/components/super_sidebar.vue +++ b/app/assets/javascripts/super_sidebar/components/super_sidebar.vue @@ -37,7 +37,7 @@ export default { <template> <aside id="super-sidebar" - class="super-sidebar gl-fixed gl-bottom-0 gl-left-0 gl-display-flex gl-flex-direction-column gl-bg-gray-10 gl-border-r gl-border-gray-a-08" + class="super-sidebar gl-display-flex gl-flex-direction-column" data-testid="super-sidebar" > <user-bar :sidebar-data="sidebarData" /> diff --git a/app/assets/javascripts/super_sidebar/components/user_bar.vue b/app/assets/javascripts/super_sidebar/components/user_bar.vue index ee72e8eafb4..4a7ea5bac80 100644 --- a/app/assets/javascripts/super_sidebar/components/user_bar.vue +++ b/app/assets/javascripts/super_sidebar/components/user_bar.vue @@ -1,9 +1,10 @@ <script> -import { GlAvatar, GlDropdown, GlIcon, GlTooltipDirective } from '@gitlab/ui'; +import { GlAvatar, GlButton, GlDropdown, GlIcon, GlTooltipDirective } from '@gitlab/ui'; import { __ } from '~/locale'; import SafeHtml from '~/vue_shared/directives/safe_html'; import NewNavToggle from '~/nav/components/new_nav_toggle.vue'; import logo from '../../../../views/shared/_logo.svg'; +import { toggleSuperSidebarCollapsed } from '../super_sidebar_collapsed_state_manager'; import CreateMenu from './create_menu.vue'; import Counter from './counter.vue'; import MergeRequestMenu from './merge_request_menu.vue'; @@ -12,6 +13,7 @@ export default { logo, components: { GlAvatar, + GlButton, GlDropdown, GlIcon, CreateMenu, @@ -20,6 +22,7 @@ export default { MergeRequestMenu, }, i18n: { + collapseSidebar: __('Collapse sidebar'), createNew: __('Create new...'), issues: __('Issues'), mergeRequests: __('Merge requests'), @@ -36,15 +39,27 @@ export default { required: true, }, }, + methods: { + collapseSidebar() { + toggleSuperSidebarCollapsed(true, true); + }, + }, }; </script> <template> <div class="user-bar"> - <div class="gl-display-flex gl-align-items-center gl-px-3 gl-py-2 gl-gap-3"> + <div class="gl-display-flex gl-align-items-center gl-px-3 gl-py-2 gl-gap-2"> <div class="gl-flex-grow-1"> <a v-safe-html="$options.logo" :href="rootPath"></a> </div> + <gl-button + v-gl-tooltip:super-sidebar.hover.bottom="$options.i18n.collapseSidebar" + :aria-label="$options.i18n.collapseSidebar" + icon="sidebar" + category="tertiary" + @click="collapseSidebar" + /> <create-menu :groups="sidebarData.create_new_menu_groups" /> <button class="gl-border-none"> <gl-icon name="search" class="gl-vertical-align-middle" /> diff --git a/app/assets/javascripts/super_sidebar/super_sidebar_bundle.js b/app/assets/javascripts/super_sidebar/super_sidebar_bundle.js index b9c7073df8c..307c920e638 100644 --- a/app/assets/javascripts/super_sidebar/super_sidebar_bundle.js +++ b/app/assets/javascripts/super_sidebar/super_sidebar_bundle.js @@ -1,9 +1,16 @@ import Vue from 'vue'; +import { + bindSuperSidebarCollapsedEvents, + initSuperSidebarCollapsedState, +} from './super_sidebar_collapsed_state_manager'; import SuperSidebar from './components/super_sidebar.vue'; export const initSuperSidebar = () => { const el = document.querySelector('.js-super-sidebar'); + bindSuperSidebarCollapsedEvents(); + initSuperSidebarCollapsedState(); + if (!el) return false; const { rootPath, sidebar, toggleNewNavEndpoint } = el.dataset; diff --git a/app/assets/javascripts/super_sidebar/super_sidebar_collapsed_state_manager.js b/app/assets/javascripts/super_sidebar/super_sidebar_collapsed_state_manager.js new file mode 100644 index 00000000000..1dfd132d522 --- /dev/null +++ b/app/assets/javascripts/super_sidebar/super_sidebar_collapsed_state_manager.js @@ -0,0 +1,45 @@ +import { GlBreakpointInstance as bp, breakpoints } from '@gitlab/ui/dist/utils'; +import { debounce } from 'lodash'; +import { setCookie, getCookie } from '~/lib/utils/common_utils'; + +export const SIDEBAR_COLLAPSED_CLASS = 'page-with-super-sidebar-collapsed'; +export const SIDEBAR_COLLAPSED_COOKIE = 'super_sidebar_collapsed'; +export const SIDEBAR_COLLAPSED_COOKIE_EXPIRATION = 365 * 10; + +export const findPage = () => document.querySelector('.page-with-super-sidebar'); +export const findToggle = () => document.querySelector('.js-super-sidebar-toggle'); + +const isCollapsed = () => findPage().classList.contains(SIDEBAR_COLLAPSED_CLASS); + +// See documentation: https://design.gitlab.com/patterns/navigation#left-sidebar +// NOTE: at 1200px nav sidebar should not overlap the content +// https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/24555#note_134136110 +export const isDesktopBreakpoint = () => bp.windowWidth() >= breakpoints.xl; + +export const getCollapsedCookie = () => getCookie(SIDEBAR_COLLAPSED_COOKIE) === 'true'; + +export const toggleSuperSidebarCollapsed = (collapsed, saveCookie) => { + findPage().classList.toggle(SIDEBAR_COLLAPSED_CLASS, collapsed); + + if (saveCookie && isDesktopBreakpoint()) { + setCookie(SIDEBAR_COLLAPSED_COOKIE, collapsed, { + expires: SIDEBAR_COLLAPSED_COOKIE_EXPIRATION, + }); + } +}; + +export const initSuperSidebarCollapsedState = () => { + const collapsed = isDesktopBreakpoint() ? getCollapsedCookie() : true; + toggleSuperSidebarCollapsed(collapsed, false); +}; + +export const bindSuperSidebarCollapsedEvents = () => { + if (findToggle()) { + findToggle().addEventListener('click', () => { + const value = !isCollapsed(); + toggleSuperSidebarCollapsed(value, true); + }); + } + + window.addEventListener('resize', debounce(initSuperSidebarCollapsedState, 100)); +}; diff --git a/app/assets/javascripts/vue_shared/new_namespace/new_namespace_page.vue b/app/assets/javascripts/vue_shared/new_namespace/new_namespace_page.vue index 318adec2319..2533b3b5489 100644 --- a/app/assets/javascripts/vue_shared/new_namespace/new_namespace_page.vue +++ b/app/assets/javascripts/vue_shared/new_namespace/new_namespace_page.vue @@ -29,8 +29,8 @@ export default { type: String, required: true, }, - initialBreadcrumb: { - type: String, + initialBreadcrumbs: { + type: Array, required: true, }, panels: { @@ -60,6 +60,10 @@ export default { return this.panels.find((p) => p.name === this.activePanelName); }, + detailProps() { + return this.activePanel.detailProps || {}; + }, + details() { return this.activePanel.details || this.activePanel.description; }, @@ -69,14 +73,15 @@ export default { }, breadcrumbs() { - if (!this.activePanel) { - return null; - } - - return [ - { text: this.initialBreadcrumb, href: '#' }, - { text: this.activePanel.title, href: `#${this.activePanel.name}` }, - ]; + return this.activePanel + ? [ + ...this.initialBreadcrumbs, + { + text: this.activePanel.title, + href: `#${this.activePanel.name}`, + }, + ] + : this.initialBreadcrumbs; }, shouldVerify() { @@ -125,24 +130,29 @@ export default { <template> <credit-card-verification v-if="shouldVerify" @verified="onVerified" /> - <welcome-page v-else-if="!activePanelName" :panels="panels" :title="title"> - <template #footer> - <slot name="welcome-footer"> </slot> - </template> - </welcome-page> - <div v-else class="row"> - <div class="col-lg-3"> - <div v-safe-html="activePanel.illustration" class="gl-text-white"></div> - <h4>{{ activePanel.title }}</h4> - - <p v-if="hasTextDetails">{{ details }}</p> - <component :is="details" v-else v-bind="activePanel.detailProps || {}" /> + <div v-else-if="!activePanelName"> + <gl-breadcrumb :items="breadcrumbs" /> + <welcome-page :panels="panels" :title="title"> + <template #footer> + <slot name="welcome-footer"> </slot> + </template> + </welcome-page> + </div> + <div v-else> + <gl-breadcrumb :items="breadcrumbs" /> + <div class="gl-display-flex gl-py-5 gl-align-items-center"> + <div v-safe-html="activePanel.illustration" class="gl-text-white col-auto"></div> + <div class="col"> + <h4>{{ activePanel.title }}</h4> + + <p v-if="hasTextDetails">{{ details }}</p> + <component :is="details" v-else v-bind="detailProps" /> + </div> <slot name="extra-description"></slot> </div> - <div class="col-lg-9"> + <div> <new-top-level-group-alert v-if="showNewTopLevelGroupAlert" /> - <gl-breadcrumb v-if="breadcrumbs" :items="breadcrumbs" /> <legacy-container :key="activePanel.name" :selector="activePanel.selector" /> </div> </div> diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index 7baf84198e4..d56337fcd60 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -321,7 +321,6 @@ $search-input-field-x-min-width: 200px; .breadcrumbs-container { display: flex; width: 100%; - position: relative; padding-top: $gl-padding / 2; padding-bottom: $gl-padding / 2; align-items: center; @@ -558,25 +557,16 @@ $search-input-field-x-min-width: 200px; } .toggle-mobile-nav { - display: none; - background-color: transparent; - border: 0; - padding: 6px 16px; - margin: 0 0 0 -15px; - height: 46px; - color: $gl-text-color; + @include gl-display-none; @include media-breakpoint-down(sm) { - display: flex; - align-items: center; - - i { - font-size: 18px; - } + @include gl-display-block; + .breadcrumbs-links { - padding-left: $gl-padding; - border-left: 1px solid $gl-text-color-quaternary; + @include gl-pl-4; + @include gl-border-l-1; + @include gl-border-l-solid; + @include gl-border-gray-100; } } } diff --git a/app/assets/stylesheets/framework/super_sidebar.scss b/app/assets/stylesheets/framework/super_sidebar.scss index 4b55b39d6f3..6758c8b4014 100644 --- a/app/assets/stylesheets/framework/super_sidebar.scss +++ b/app/assets/stylesheets/framework/super_sidebar.scss @@ -1,8 +1,27 @@ .super-sidebar { - top: 0; - width: $contextual-sidebar-width; + @include gl-fixed; + @include gl-top-0; + @include gl-bottom-0; + @include gl-left-0; + @include gl-bg-gray-10; + @include gl-border-r; + @include gl-border-gray-a-08; + transform: translate3d(0, 0, 0); + width: $super-sidebar-width; z-index: 600; + &.super-sidebar-loading { + transform: translate3d(-100%, 0, 0); + + @include media-breakpoint-up(xl) { + transform: translate3d(0, 0, 0); + } + } + + &:not(.super-sidebar-loading) { + transition: transform $gl-transition-duration-medium; + } + .user-bar { background-color: $t-gray-a-04; @@ -60,6 +79,41 @@ } } +.page-with-super-sidebar { + padding-left: 0; + + @include media-breakpoint-up(xl) { + padding-left: $contextual-sidebar-width; + + .super-sidebar-toggle { + display: none; + } + } +} + +.page-with-super-sidebar-collapsed { + .super-sidebar { + transform: translate3d(-100%, 0, 0); + } + + @include media-breakpoint-up(xl) { + padding-left: 0; + + .super-sidebar-toggle { + display: block; + } + } +} + +.container-limited .super-sidebar-toggle { + @media (min-width: $super-sidebar-toggle-position-breakpoint) { + position: absolute; + left: $gl-spacing-scale-3; + top: $gl-spacing-scale-3; + margin: 0; + } +} + .with-performance-bar .super-sidebar { top: $performance-bar-height; } diff --git a/app/assets/stylesheets/framework/system_messages.scss b/app/assets/stylesheets/framework/system_messages.scss index 89585fd96ae..590a66ff28e 100644 --- a/app/assets/stylesheets/framework/system_messages.scss +++ b/app/assets/stylesheets/framework/system_messages.scss @@ -48,6 +48,7 @@ // left sidebar eg: project page // right sidebar eg: MR page .nav-sidebar, + .super-sidebar, .right-sidebar { top: calc(#{$system-header-height} + #{$header-height}); } @@ -72,6 +73,7 @@ // left sidebar eg: project page // right sidebar eg: MR page .nav-sidebar, + .super-sidebar, .right-sidebar { top: calc(#{$header-height} + #{$performance-bar-height} + #{$system-header-height}); } @@ -83,6 +85,7 @@ // left sidebar eg: project page // right sidebar eg: mr page .nav-sidebar, + .super-sidebar, .right-sidebar, // navless pages' footer eg: login page // navless pages' footer border eg: login page diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index c616915073e..ac27f27ae2a 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -10,6 +10,8 @@ $default-transition-duration: 0.15s; $contextual-sidebar-width: 256px; $contextual-sidebar-collapsed-width: 56px; $toggle-sidebar-height: 48px; +$super-sidebar-width: 256px; +$super-sidebar-toggle-position-breakpoint: 1360px; /** 🚨 Do not use this spacing scale — it is deprecated and being removed. 🚨 diff --git a/app/assets/stylesheets/print.scss b/app/assets/stylesheets/print.scss index ab86a2f69dd..265f27f21fa 100644 --- a/app/assets/stylesheets/print.scss +++ b/app/assets/stylesheets/print.scss @@ -25,6 +25,7 @@ nav, nav.navbar-collapse, nav.navbar-collapse.collapse, .nav-sidebar, +.super-sidebar, .profiler-results, .tree-ref-holder, .tree-holder .breadcrumb, diff --git a/app/assets/stylesheets/startup/startup-dark.scss b/app/assets/stylesheets/startup/startup-dark.scss index 3b28025053b..accc50697c5 100644 --- a/app/assets/stylesheets/startup/startup-dark.scss +++ b/app/assets/stylesheets/startup/startup-dark.scss @@ -1493,6 +1493,42 @@ kbd { display: none; } } +.super-sidebar { + position: fixed; + top: 0; + bottom: 0; + left: 0; + background-color: #1f1e24; + border-right: solid 1px #434248; + border-color: rgba(251, 250, 253, 0.08); + transform: translate3d(0, 0, 0); + width: 256px; + z-index: 600; +} +.super-sidebar.super-sidebar-loading { + transform: translate3d(-100%, 0, 0); +} +@media (min-width: 1200px) { + .super-sidebar.super-sidebar-loading { + transform: translate3d(0, 0, 0); + } +} +.page-with-super-sidebar { + padding-left: 0; +} +@media (min-width: 1200px) { + .page-with-super-sidebar { + padding-left: 256px; + } +} +.page-with-super-sidebar-collapsed .super-sidebar { + transform: translate3d(-100%, 0, 0); +} +@media (min-width: 1200px) { + .page-with-super-sidebar-collapsed { + padding-left: 0; + } +} input::-moz-placeholder { color: #737278; opacity: 1; diff --git a/app/assets/stylesheets/startup/startup-general.scss b/app/assets/stylesheets/startup/startup-general.scss index adafe719892..511c270d556 100644 --- a/app/assets/stylesheets/startup/startup-general.scss +++ b/app/assets/stylesheets/startup/startup-general.scss @@ -1493,6 +1493,42 @@ kbd { display: none; } } +.super-sidebar { + position: fixed; + top: 0; + bottom: 0; + left: 0; + background-color: #fbfafd; + border-right: solid 1px #dcdcde; + border-color: rgba(31, 30, 36, 0.08); + transform: translate3d(0, 0, 0); + width: 256px; + z-index: 600; +} +.super-sidebar.super-sidebar-loading { + transform: translate3d(-100%, 0, 0); +} +@media (min-width: 1200px) { + .super-sidebar.super-sidebar-loading { + transform: translate3d(0, 0, 0); + } +} +.page-with-super-sidebar { + padding-left: 0; +} +@media (min-width: 1200px) { + .page-with-super-sidebar { + padding-left: 256px; + } +} +.page-with-super-sidebar-collapsed .super-sidebar { + transform: translate3d(-100%, 0, 0); +} +@media (min-width: 1200px) { + .page-with-super-sidebar-collapsed { + padding-left: 0; + } +} input::-moz-placeholder { color: #89888d; opacity: 1; diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index 76065bc3ab8..a0c82998108 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -75,6 +75,7 @@ class GroupsController < Groups::ApplicationController end def new + @parent_group = Group.find_by_id(params[:parent_id]) @group = Group.new(params.permit(:parent_id)) @group.build_namespace_settings end diff --git a/app/controllers/projects/google_cloud/base_controller.rb b/app/controllers/projects/google_cloud/base_controller.rb index dfb73821b0f..7eccc0c1c77 100644 --- a/app/controllers/projects/google_cloud/base_controller.rb +++ b/app/controllers/projects/google_cloud/base_controller.rb @@ -45,8 +45,8 @@ class Projects::GoogleCloud::BaseController < Projects::ApplicationController return_url = project_google_cloud_configuration_path(project) state = generate_session_key_redirect(request.url, return_url) @authorize_url = GoogleApi::CloudPlatform::Client.new(nil, - callback_google_api_auth_url, - state: state).authorize_url + callback_google_api_auth_url, + state: state).authorize_url redirect_to @authorize_url end diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 71ad747b6b1..dec170cf582 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -78,6 +78,8 @@ class ProjectsController < Projects::ApplicationController @namespace = Namespace.find_by(id: params[:namespace_id]) if params[:namespace_id] return access_denied! if @namespace && !can?(current_user, :create_projects, @namespace) + @parent_group = Group.find_by(id: params[:namespace_id]) + @current_user_group = if current_user.manageable_groups(include_groups_with_developer_maintainer_access: true).count == 1 current_user.manageable_groups(include_groups_with_developer_maintainer_access: true).first diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 93b7c8c0b94..23a21bce456 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -374,6 +374,10 @@ module ApplicationHelper cookies["sidebar_collapsed"] == "true" end + def collapsed_super_sidebar? + cookies["super_sidebar_collapsed"] == "true" + end + def locale_path asset_path("locale/#{Gitlab::I18n.locale}/app.js") end diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb index 129871ca3fd..ce64ac1f21f 100644 --- a/app/helpers/groups_helper.rb +++ b/app/helpers/groups_helper.rb @@ -126,6 +126,7 @@ module GroupsHelper def subgroup_creation_data(group) { + parent_group_url: group.parent && group_url(group.parent), parent_group_name: group.parent&.name, import_existing_group_path: new_group_path(parent_id: group.parent_id, anchor: 'import-group-pane') } diff --git a/app/helpers/nav/new_dropdown_helper.rb b/app/helpers/nav/new_dropdown_helper.rb index ddd6469a9e4..98ff48bf19b 100644 --- a/app/helpers/nav/new_dropdown_helper.rb +++ b/app/helpers/nav/new_dropdown_helper.rb @@ -2,23 +2,25 @@ module Nav module NewDropdownHelper - def new_dropdown_view_model(group:, project:) + def new_dropdown_view_model(group:, project:, with_context: false) return unless current_user menu_sections = [] + data = { title: _('Create new...') } - if group&.persisted? - menu_sections.push(group_menu_section(group)) - elsif project&.persisted? + if project&.persisted? menu_sections.push(project_menu_section(project)) + data[:context] = project if with_context + elsif group&.persisted? + menu_sections.push(group_menu_section(group)) + data[:context] = group if with_context end menu_sections.push(general_menu_section) - { - title: _("Create new..."), - menu_sections: menu_sections.select { |x| x.fetch(:menu_items).any? } - } + data[:menu_sections] = menu_sections.select { |x| x.fetch(:menu_items).any? } + + data end private @@ -53,7 +55,8 @@ module Nav if can?(current_user, :admin_group_member, group) menu_items.push( invite_members_menu_item( - href: group_group_members_path(group) + href: group_group_members_path(group), + partial: 'groups/invite_members_top_nav_link' ) ) end @@ -104,7 +107,8 @@ module Nav if can_admin_project_member?(project) menu_items.push( invite_members_menu_item( - href: project_project_members_path(project) + href: project_project_members_path(project), + partial: 'projects/invite_members_top_nav_link' ) ) end @@ -157,11 +161,12 @@ module Nav } end - def invite_members_menu_item(href:) + def invite_members_menu_item(href:, partial:) ::Gitlab::Nav::TopNavMenuItem.build( id: 'invite', title: s_('InviteMember|Invite members'), - emoji: 'shaking_hands', + icon: 'shaking_hands', + partial: partial, href: href, data: { track_action: 'click_link_invite_members', diff --git a/app/helpers/nav_helper.rb b/app/helpers/nav_helper.rb index 0b0921cea8b..0f328410abc 100644 --- a/app/helpers/nav_helper.rb +++ b/app/helpers/nav_helper.rb @@ -12,7 +12,9 @@ module NavHelper def page_with_sidebar_class class_name = page_gutter_class class_name << 'page-with-contextual-sidebar' if defined?(@left_sidebar) && @left_sidebar - class_name << 'page-with-icon-sidebar' if collapsed_sidebar? && @left_sidebar + class_name << 'page-with-super-sidebar' if show_super_sidebar? && @left_sidebar + class_name << 'page-with-super-sidebar-collapsed' if show_super_sidebar? && collapsed_super_sidebar? && @left_sidebar + class_name << 'page-with-icon-sidebar' if collapsed_sidebar? && @left_sidebar && !show_super_sidebar? class_name -= ['right-sidebar-expanded'] if defined?(@right_sidebar) && !@right_sidebar class_name diff --git a/app/views/groups/_invite_members_modal.html.haml b/app/views/groups/_invite_members_modal.html.haml index f0fd9026b30..cd3327ba9ec 100644 --- a/app/views/groups/_invite_members_modal.html.haml +++ b/app/views/groups/_invite_members_modal.html.haml @@ -2,5 +2,5 @@ .js-invite-members-modal{ data: { is_project: 'false', access_levels: group.access_level_roles.to_json, - reload_page_on_submit: local_assigns.fetch(:reload_page_on_submit, false).to_s, + reload_page_on_submit: current_path?('group_members#index').to_s, help_link: help_page_url('user/permissions') }.merge(common_invite_modal_dataset(group)).merge(users_filter_data(group)) } diff --git a/app/views/groups/_invite_members_side_nav_link.html.haml b/app/views/groups/_invite_members_side_nav_link.html.haml index 978ef01984c..ab7d4fc5041 100644 --- a/app/views/groups/_invite_members_side_nav_link.html.haml +++ b/app/views/groups/_invite_members_side_nav_link.html.haml @@ -5,4 +5,3 @@ qa_selector: 'invite_members_sidebar_button' } } = render partial: 'shared/nav/sidebar_submenu', locals: { sidebar_menu: sidebar_menu } -= render 'groups/invite_members_modal', group: group diff --git a/app/views/groups/_invite_members_top_nav_link.html.haml b/app/views/groups/_invite_members_top_nav_link.html.haml new file mode 100644 index 00000000000..a3a28ffd6e0 --- /dev/null +++ b/app/views/groups/_invite_members_top_nav_link.html.haml @@ -0,0 +1,6 @@ += link_to local_assigns.fetch(:href), class: local_assigns.fetch(:css_class), data: local_assigns.fetch(:data) do + = local_assigns.fetch(:display_text) + - if local_assigns.fetch(:icon) + = " #{emoji_icon(local_assigns.fetch(:icon), 'aria-hidden': true, class: 'gl-font-base gl-vertical-align-baseline')}".html_safe + += render 'groups/invite_members_modal', group: local_assigns.fetch(:context) diff --git a/app/views/groups/_new_group_fields.html.haml b/app/views/groups/_new_group_fields.html.haml index 95990e8937c..ddf6e52796f 100644 --- a/app/views/groups/_new_group_fields.html.haml +++ b/app/views/groups/_new_group_fields.html.haml @@ -31,6 +31,5 @@ .row .col-sm-12 = f.submit submit_label, pajamas_button: true, data: { qa_selector: 'create_group_button' } - = render Pajamas::ButtonComponent.new(href: dashboard_groups_path) do + = render Pajamas::ButtonComponent.new(href: @parent_group || dashboard_groups_path) do = _('Cancel') - diff --git a/app/views/groups/group_members/index.html.haml b/app/views/groups/group_members/index.html.haml index 298ed2c0806..04bf3f98a1e 100644 --- a/app/views/groups/group_members/index.html.haml +++ b/app/views/groups/group_members/index.html.haml @@ -16,7 +16,6 @@ trigger_source: 'group-members-page', display_text: _('Invite members') } } = render 'groups/invite_groups_modal', group: @group, reload_page_on_submit: true - = render 'groups/invite_members_modal', group: @group, reload_page_on_submit: true = render_if_exists 'groups/group_members/ldap_sync' diff --git a/app/views/groups/new.html.haml b/app/views/groups/new.html.haml index b75fda2f344..0878fbf9a35 100644 --- a/app/views/groups/new.html.haml +++ b/app/views/groups/new.html.haml @@ -4,7 +4,7 @@ - header_title _("Groups"), dashboard_groups_path - add_page_specific_style 'page_bundles/new_namespace' -.group-edit-container.gl-mt-5 +.group-edit-container .js-new-group-creation{ data: { has_errors: @group.errors.any?.to_s }.merge(subgroup_creation_data(@group), verification_for_group_creation_data) } diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml index 72b7bec1b92..7983274f319 100644 --- a/app/views/groups/show.html.haml +++ b/app/views/groups/show.html.haml @@ -14,7 +14,6 @@ callouts_path: group_callouts_path, callouts_feature_id: Users::GroupCalloutsHelper::INVITE_MEMBERS_BANNER, group_id: @group.id } } - = render 'groups/invite_members_modal', group: @group = content_for :meta_tags do = auto_discovery_link_tag(:atom, group_url(@group, rss_url_options), title: "#{@group.name} activity") diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml index 77f0c3c636f..0fd0a97a658 100644 --- a/app/views/layouts/_page.html.haml +++ b/app/views/layouts/_page.html.haml @@ -4,7 +4,7 @@ - if show_super_sidebar? - sidebar_panel = super_sidebar_nav_panel(nav: nav, user: current_user, group: @group, project: @project, current_ref: current_ref, ref_type: @ref_type) - sidebar_data = super_sidebar_context(current_user, group: @group, project: @project, panel: sidebar_panel).to_json - %aside.js-super-sidebar.nav-sidebar{ data: { root_path: root_path, sidebar: sidebar_data, toggle_new_nav_endpoint: profile_preferences_url } } + %aside.js-super-sidebar.super-sidebar.super-sidebar-loading{ data: { root_path: root_path, sidebar: sidebar_data, toggle_new_nav_endpoint: profile_preferences_url } } - if display_whats_new? #whats-new-app{ data: { version_digest: whats_new_version_digest } } diff --git a/app/views/layouts/dashboard.html.haml b/app/views/layouts/dashboard.html.haml index 89f238eb6b3..1ac5f0a8497 100644 --- a/app/views/layouts/dashboard.html.haml +++ b/app/views/layouts/dashboard.html.haml @@ -2,6 +2,6 @@ - header_title _("Dashboard"), root_path unless header_title - @left_sidebar = true -- nav "your_work" +- nav (@parent_group ? "group" : "your_work") = render template: "layouts/application" diff --git a/app/views/layouts/header/_new_dropdown.html.haml b/app/views/layouts/header/_new_dropdown.html.haml index 372babea18e..c133ce21c6c 100644 --- a/app/views/layouts/header/_new_dropdown.html.haml +++ b/app/views/layouts/header/_new_dropdown.html.haml @@ -1,4 +1,4 @@ -- view_model = new_dropdown_view_model(project: @project, group: @group) +- view_model = new_dropdown_view_model(project: @project, group: @group, with_context: true) - menu_sections = view_model.fetch(:menu_sections) - title = view_model.fetch(:title) - show_headers = menu_sections.length > 1 @@ -26,8 +26,16 @@ = section.fetch(:title) - section.fetch(:menu_items).each do |menu_item| %li< - = link_to menu_item.fetch(:href), class: menu_item.fetch(:css_class), data: menu_item.fetch(:data) do - = menu_item.fetch(:title) - - if menu_item.fetch(:emoji) - -# We need to insert a space between the title and emoji - = " #{emoji_icon(menu_item.fetch(:emoji), 'aria-hidden': true, class: 'gl-font-base gl-vertical-align-baseline')}".html_safe + - if menu_item.fetch(:partial).present? + = render partial: menu_item.fetch(:partial), + locals: { context: view_model[:context], + display_text: menu_item.fetch(:title), + icon: menu_item.fetch(:icon), + css_class: menu_item.fetch(:css_class), + href: menu_item.fetch(:href), + data: menu_item.fetch(:data) } + - else + = link_to menu_item.fetch(:title), + menu_item.fetch(:href), + class: menu_item.fetch(:css_class), + data: menu_item.fetch(:data) diff --git a/app/views/layouts/nav/_breadcrumbs.html.haml b/app/views/layouts/nav/_breadcrumbs.html.haml index 98d6af28cf5..06dff99718c 100644 --- a/app/views/layouts/nav/_breadcrumbs.html.haml +++ b/app/views/layouts/nav/_breadcrumbs.html.haml @@ -3,25 +3,26 @@ - unless @skip_current_level_breadcrumb - push_to_schema_breadcrumb(@breadcrumb_title, breadcrumb_title_link) -%nav.breadcrumbs{ class: [container, @content_class], 'aria-label': _('Breadcrumbs') } - .breadcrumbs-container{ class: ("border-bottom-0" if @no_breadcrumb_border) } - - if defined?(@left_sidebar) - = button_tag class: 'toggle-mobile-nav', data: { qa_selector: 'toggle_mobile_nav_button' }, type: 'button' do - %span.sr-only= _("Open sidebar") - = sprite_icon('sidebar', size: 18) - .breadcrumbs-links{ data: { testid: 'breadcrumb-links', qa_selector: 'breadcrumb_links_content' } } - %ul.list-unstyled.breadcrumbs-list.js-breadcrumbs-list - - unless hide_top_links - = header_title - - if @breadcrumbs_extra_links - - @breadcrumbs_extra_links.each do |extra| - = breadcrumb_list_item link_to(extra[:text], extra[:link]) - = render "layouts/nav/breadcrumbs/collapsed_inline_list", location: :after - - unless @skip_current_level_breadcrumb - %li{ data: { testid: 'breadcrumb-current-link', qa_selector: 'breadcrumb_current_link' } } - = link_to @breadcrumb_title, breadcrumb_title_link - -# haml-lint:disable InlineJavaScript - %script{ type: 'application/ld+json' } - :plain - #{schema_breadcrumb_json} - = yield :header_content +.gl-relative + .breadcrumbs{ class: [container, @content_class] } + .breadcrumbs-container{ class: ("border-bottom-0" if @no_breadcrumb_border) } + - if show_super_sidebar? + = render Pajamas::ButtonComponent.new(icon: 'sidebar', category: :tertiary, button_options: { class: 'js-super-sidebar-toggle super-sidebar-toggle gl-ml-n3 gl-mr-2', title: _('Expand sidebar'), aria: { label: _('Expand sidebar') }, data: {toggle: 'tooltip', placement: 'right' } }) + - elsif defined?(@left_sidebar) + = render Pajamas::ButtonComponent.new(icon: 'sidebar', category: :tertiary, button_options: { class: 'toggle-mobile-nav gl-ml-n3 gl-mr-2', data: { qa_selector: 'toggle_mobile_nav_button' }, aria: { label: _('Open sidebar') } }) + %nav.breadcrumbs-links{ 'aria-label': _('Breadcrumbs'), data: { testid: 'breadcrumb-links', qa_selector: 'breadcrumb_links_content' } } + %ul.list-unstyled.breadcrumbs-list.js-breadcrumbs-list + - unless hide_top_links + = header_title + - if @breadcrumbs_extra_links + - @breadcrumbs_extra_links.each do |extra| + = breadcrumb_list_item link_to(extra[:text], extra[:link]) + = render "layouts/nav/breadcrumbs/collapsed_inline_list", location: :after + - unless @skip_current_level_breadcrumb + %li{ data: { testid: 'breadcrumb-current-link', qa_selector: 'breadcrumb_current_link' } } + = link_to @breadcrumb_title, breadcrumb_title_link + -# haml-lint:disable InlineJavaScript + %script{ type: 'application/ld+json' } + :plain + #{schema_breadcrumb_json} + = yield :header_content diff --git a/app/views/layouts/nav/sidebar/_group.html.haml b/app/views/layouts/nav/sidebar/_group.html.haml index c2b50bc0e52..fd0e47b543f 100644 --- a/app/views/layouts/nav/sidebar/_group.html.haml +++ b/app/views/layouts/nav/sidebar/_group.html.haml @@ -1 +1,2 @@ -= render partial: 'shared/nav/sidebar', object: Sidebars::Groups::Panel.new(group_sidebar_context(@group, current_user)) +- group = @parent_group || @group += render partial: 'shared/nav/sidebar', object: Sidebars::Groups::Panel.new(group_sidebar_context(group, current_user)) diff --git a/app/views/projects/_invite_members_empty_project.html.haml b/app/views/projects/_invite_members_empty_project.html.haml index 5bc53339bf0..1ab92d56d25 100644 --- a/app/views/projects/_invite_members_empty_project.html.haml +++ b/app/views/projects/_invite_members_empty_project.html.haml @@ -9,5 +9,3 @@ trigger_source: 'project-empty-page', event: 'click_button', label: 'invite_members_empty_project' } } - -= render 'projects/invite_members_modal', project: @project diff --git a/app/views/projects/_invite_members_modal.html.haml b/app/views/projects/_invite_members_modal.html.haml index 53f74a0f270..a1b0bdd6c56 100644 --- a/app/views/projects/_invite_members_modal.html.haml +++ b/app/views/projects/_invite_members_modal.html.haml @@ -2,5 +2,5 @@ .js-invite-members-modal{ data: { is_project: 'true', access_levels: ProjectMember.permissible_access_level_roles(current_user, project).to_json, - reload_page_on_submit: local_assigns.fetch(:reload_page_on_submit, false).to_s, + reload_page_on_submit: current_path?('project_members#index').to_s, help_link: help_page_url('user/permissions') }.merge(common_invite_modal_dataset(project)).merge(users_filter_data(project.group)) } diff --git a/app/views/projects/_invite_members_side_nav_link.html.haml b/app/views/projects/_invite_members_side_nav_link.html.haml index b96a7608ce2..771c7c650c2 100644 --- a/app/views/projects/_invite_members_side_nav_link.html.haml +++ b/app/views/projects/_invite_members_side_nav_link.html.haml @@ -5,4 +5,3 @@ qa_selector: 'invite_members_sidebar_button' } } = render partial: 'shared/nav/sidebar_submenu', locals: { sidebar_menu: sidebar_menu } -= render 'projects/invite_members_modal', project: project diff --git a/app/views/projects/_invite_members_top_nav_link.html.haml b/app/views/projects/_invite_members_top_nav_link.html.haml new file mode 100644 index 00000000000..e3a5c40d93c --- /dev/null +++ b/app/views/projects/_invite_members_top_nav_link.html.haml @@ -0,0 +1,6 @@ += link_to local_assigns.fetch(:href), class: local_assigns.fetch(:css_class), data: local_assigns.fetch(:data) do + = local_assigns.fetch(:display_text) + - if local_assigns.fetch(:icon) + = " #{emoji_icon(local_assigns.fetch(:icon), 'aria-hidden': true, class: 'gl-font-base gl-vertical-align-baseline')}".html_safe + += render 'projects/invite_members_modal', project: local_assigns.fetch(:context) diff --git a/app/views/projects/_new_project_fields.html.haml b/app/views/projects/_new_project_fields.html.haml index 53a1abdff33..27211ffb1e5 100644 --- a/app/views/projects/_new_project_fields.html.haml +++ b/app/views/projects/_new_project_fields.html.haml @@ -98,4 +98,4 @@ -# this partial is from JiHu, see details in https://jihulab.com/gitlab-cn/gitlab/-/merge_requests/675 = render_if_exists 'shared/other_project_options', f: f, visibility_level: visibility_level, track_label: track_label = f.submit _('Create project'), class: "js-create-project-button", data: { qa_selector: 'project_create_button', track_label: "#{track_label}", track_action: "click_button", track_property: "create_project", track_value: "" }, pajamas_button: true -= link_to _('Cancel'), dashboard_projects_path, class: 'btn gl-button btn-default btn-cancel', data: { track_label: "#{track_label}", track_action: "click_button", track_property: "cancel", track_value: "" } += link_to _('Cancel'), @parent_group || dashboard_groups_path, class: 'btn gl-button btn-default btn-cancel', data: { track_label: "#{track_label}", track_action: "click_button", track_property: "cancel", track_value: "" } diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml index a8edf87b696..7e8bf4ae57f 100644 --- a/app/views/projects/issues/show.html.haml +++ b/app/views/projects/issues/show.html.haml @@ -8,4 +8,3 @@ - add_page_specific_style 'page_bundles/work_items' = render 'projects/issuable/show', issuable: @issue, api_awards_path: award_emoji_issue_api_path(@issue) -= render 'projects/invite_members_modal', project: @project diff --git a/app/views/projects/merge_requests/_page.html.haml b/app/views/projects/merge_requests/_page.html.haml index 880bffc43ab..47760211bd5 100644 --- a/app/views/projects/merge_requests/_page.html.haml +++ b/app/views/projects/merge_requests/_page.html.haml @@ -110,5 +110,4 @@ - if current_user && Feature.enabled?(:mr_experience_survey, current_user) #js-mr-experience-survey{ data: { account_age: current_user.account_age_in_days } } -= render 'projects/invite_members_modal', project: @project = render 'shared/web_ide_path' diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml index 56581fe7b18..a089dd60e7d 100644 --- a/app/views/projects/new.html.haml +++ b/app/views/projects/new.html.haml @@ -4,11 +4,11 @@ - header_title _("Projects"), dashboard_projects_path - add_page_specific_style 'page_bundles/new_namespace' -.project-edit-container.gl-mt-5 +.project-edit-container .project-edit-errors = render 'projects/errors' - .js-new-project-creation{ data: { is_ci_cd_available: (ci_cd_projects_available? if Gitlab.ee?).to_s, has_errors: @project.errors.any?.to_s, new_project_guidelines: brand_new_project_guidelines, push_to_create_project_command: push_to_create_project_command, working_with_projects_help_path: help_page_path("user/project/working_with_projects") } } + .js-new-project-creation{ data: { is_ci_cd_available: (ci_cd_projects_available? if Gitlab.ee?).to_s, has_errors: @project.errors.any?.to_s, new_project_guidelines: brand_new_project_guidelines, push_to_create_project_command: push_to_create_project_command, working_with_projects_help_path: help_page_path("user/project/working_with_projects"), parent_group_url: @project.parent && group_url(@project.parent), parent_group_name: @project.parent&.name } } .row{ 'v-cloak': true } #blank-project-pane.tab-pane.active diff --git a/app/views/projects/project_members/index.html.haml b/app/views/projects/project_members/index.html.haml index 4ac0e28d386..a0a90fbe204 100644 --- a/app/views/projects/project_members/index.html.haml +++ b/app/views/projects/project_members/index.html.haml @@ -25,7 +25,6 @@ classes: 'gl-md-w-auto gl-w-full gl-md-ml-3 gl-md-mt-0 gl-mt-3', trigger_source: 'project-members-page', display_text: _('Invite members') } } - = render 'projects/invite_members_modal', project: @project, reload_page_on_submit: true - else - if project_can_be_shared? %h4 diff --git a/app/views/projects/work_items/index.html.haml b/app/views/projects/work_items/index.html.haml index 69597aab7ef..dad8bb09ff6 100644 --- a/app/views/projects/work_items/index.html.haml +++ b/app/views/projects/work_items/index.html.haml @@ -4,4 +4,3 @@ - @noteable_type = 'WorkItem' #js-work-items{ data: work_items_index_data(@project) } -= render 'projects/invite_members_modal', project: @project diff --git a/db/migrate/20230215180605_index_sbom_occurrences_on_project_id_and_id.rb b/db/migrate/20230215180605_index_sbom_occurrences_on_project_id_and_id.rb new file mode 100644 index 00000000000..34538a1a7ff --- /dev/null +++ b/db/migrate/20230215180605_index_sbom_occurrences_on_project_id_and_id.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class IndexSbomOccurrencesOnProjectIdAndId < Gitlab::Database::Migration[2.1] + disable_ddl_transaction! + + INDEX_NAME = 'index_sbom_occurrences_on_project_id_and_id' + + def up + add_concurrent_index :sbom_occurrences, [:project_id, :id], name: INDEX_NAME + end + + def down + remove_concurrent_index_by_name :sbom_occurrences, INDEX_NAME + end +end diff --git a/db/schema_migrations/20230215180605 b/db/schema_migrations/20230215180605 new file mode 100644 index 00000000000..a3b222de10f --- /dev/null +++ b/db/schema_migrations/20230215180605 @@ -0,0 +1 @@ +879bee488f4089527de02ebfd6c9d6f6de7ab24d87361e29f998d86b62ca7461
\ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index 61b0e28c8ad..83e66e11098 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -31635,6 +31635,8 @@ CREATE INDEX index_sbom_occurrences_on_pipeline_id ON sbom_occurrences USING btr CREATE INDEX index_sbom_occurrences_on_project_id ON sbom_occurrences USING btree (project_id); +CREATE INDEX index_sbom_occurrences_on_project_id_and_id ON sbom_occurrences USING btree (project_id, id); + CREATE INDEX index_sbom_occurrences_on_source_id ON sbom_occurrences USING btree (source_id); CREATE UNIQUE INDEX index_sbom_occurrences_on_uuid ON sbom_occurrences USING btree (uuid); diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 5e0ef4f9c5d..088fa9a4cc6 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -7630,6 +7630,29 @@ The edge type for [`DastSiteValidation`](#dastsitevalidation). | <a id="dastsitevalidationedgecursor"></a>`cursor` | [`String!`](#string) | A cursor for use in pagination. | | <a id="dastsitevalidationedgenode"></a>`node` | [`DastSiteValidation`](#dastsitevalidation) | The item at the end of the edge. | +#### `DependencyConnection` + +The connection type for [`Dependency`](#dependency). + +##### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| <a id="dependencyconnectionedges"></a>`edges` | [`[DependencyEdge]`](#dependencyedge) | A list of edges. | +| <a id="dependencyconnectionnodes"></a>`nodes` | [`[Dependency]`](#dependency) | A list of nodes. | +| <a id="dependencyconnectionpageinfo"></a>`pageInfo` | [`PageInfo!`](#pageinfo) | Information to aid in pagination. | + +#### `DependencyEdge` + +The edge type for [`Dependency`](#dependency). + +##### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| <a id="dependencyedgecursor"></a>`cursor` | [`String!`](#string) | A cursor for use in pagination. | +| <a id="dependencyedgenode"></a>`node` | [`Dependency`](#dependency) | The item at the end of the edge. | + #### `DependencyProxyBlobConnection` The connection type for [`DependencyProxyBlob`](#dependencyproxyblob). @@ -12335,6 +12358,20 @@ The response from the AdminSidekiqQueuesDeleteJobs mutation. | <a id="deletednoteid"></a>`id` | [`NoteID!`](#noteid) | ID of the deleted note. | | <a id="deletednotelastdiscussionnote"></a>`lastDiscussionNote` | [`Boolean`](#boolean) | Whether deleted note is the last note in the discussion. | +### `Dependency` + +A software dependency used by a project. + +#### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| <a id="dependencyid"></a>`id` | [`GlobalID!`](#globalid) | ID of the dependency. | +| <a id="dependencylocation"></a>`location` | [`Location`](#location) | Information about where the dependency is located. | +| <a id="dependencyname"></a>`name` | [`String!`](#string) | Name of the dependency. | +| <a id="dependencypackager"></a>`packager` | [`String`](#string) | Description of the tool used to manage the dependency. | +| <a id="dependencyversion"></a>`version` | [`String`](#string) | Version of the dependency. | + ### `DependencyProxyBlob` Dependency proxy blob. @@ -15457,6 +15494,15 @@ Represents an entry from the Cloud License history. | <a id="licensehistoryentrytype"></a>`type` | [`String!`](#string) | Type of the license. | | <a id="licensehistoryentryusersinlicensecount"></a>`usersInLicenseCount` | [`Int`](#int) | Number of paid users in the license. | +### `Location` + +#### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| <a id="locationblobpath"></a>`blobPath` | [`String`](#string) | HTTP URI path to view the input file in GitLab. | +| <a id="locationpath"></a>`path` | [`String`](#string) | Path, relative to the root of the repository, of the filewhich was analyzed to detect the dependency. | + ### `MavenMetadata` Maven metadata. @@ -17740,6 +17786,7 @@ Represents a product analytics dashboard visualization. | <a id="projectcreatedat"></a>`createdAt` | [`Time`](#time) | Timestamp of the project creation. | | <a id="projectdastscannerprofiles"></a>`dastScannerProfiles` | [`DastScannerProfileConnection`](#dastscannerprofileconnection) | DAST scanner profiles associated with the project. (see [Connections](#connections)) | | <a id="projectdastsiteprofiles"></a>`dastSiteProfiles` | [`DastSiteProfileConnection`](#dastsiteprofileconnection) | DAST Site Profiles associated with the project. (see [Connections](#connections)) | +| <a id="projectdependencies"></a>`dependencies` **{warning-solid}** | [`DependencyConnection`](#dependencyconnection) | **Introduced** in 15.9. This feature is in Alpha. It can be changed or removed at any time. Software dependencies used by the project. | | <a id="projectdescription"></a>`description` | [`String`](#string) | Short description of the project. | | <a id="projectdescriptionhtml"></a>`descriptionHtml` | [`String`](#string) | GitLab Flavored Markdown rendering of `description`. | | <a id="projectdora"></a>`dora` | [`Dora`](#dora) | Project's DORA metrics. | diff --git a/doc/api/group_badges.md b/doc/api/group_badges.md index ede7591c6d1..0d5faae7c14 100644 --- a/doc/api/group_badges.md +++ b/doc/api/group_badges.md @@ -10,7 +10,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w ## Placeholder tokens -Badges support placeholders that are replaced in real time in both the link and image URL. The allowed placeholders are: +[Badges](../user/project/badges.md) support placeholders that are replaced in real time in both the link and image URL. The allowed placeholders are: <!-- vale gitlab.Spelling = NO --> diff --git a/doc/api/project_badges.md b/doc/api/project_badges.md index 3dad40a3f96..a8940a7875c 100644 --- a/doc/api/project_badges.md +++ b/doc/api/project_badges.md @@ -8,7 +8,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w ## Placeholder tokens -Badges support placeholders that are replaced in real-time in both the link and image URL. The allowed placeholders are: +[Badges](../user/project/badges.md) support placeholders that are replaced in real-time in both the link and image URL. The allowed placeholders are: <!-- vale gitlab.Spelling = NO --> diff --git a/doc/api/projects.md b/doc/api/projects.md index b502d547ddc..c8755e020a7 100644 --- a/doc/api/projects.md +++ b/doc/api/projects.md @@ -1433,6 +1433,7 @@ Supported attributes: |-------------------------------------------------------------|----------------|------------------------|-------------| | `id` | integer or string | **{check-circle}** Yes | The ID or [URL-encoded path of the project](rest/index.md#namespaced-path-encoding). | | `allow_merge_on_skipped_pipeline` | boolean | **{dotted-circle}** No | Set whether or not merge requests can be merged with skipped jobs. | +| `allow_pipeline_trigger_approve_deployment` **(PREMIUM)** | boolean | **{dotted-circle}** No | Set whether or not a pipeline triggerer is allowed to approve deployments. | | `only_allow_merge_if_all_status_checks_passed` **(ULTIMATE)** | boolean | **{dotted-circle}** No | Indicates that merges of merge requests should be blocked unless all status checks have passed. Defaults to false.<br/><br/>[Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/369859) in GitLab 15.5 with feature flag `only_allow_merge_if_all_status_checks_passed` disabled by default. The feature flag was enabled by default in GitLab 15.9. | | `analytics_access_level` | string | **{dotted-circle}** No | One of `disabled`, `private` or `enabled` | | `approvals_before_merge` **(PREMIUM)** | integer | **{dotted-circle}** No | How many approvers should approve merge request by default. To configure approval rules, see [Merge request approvals API](merge_request_approvals.md). | diff --git a/doc/ci/yaml/index.md b/doc/ci/yaml/index.md index d5c0fe1d41d..5d5a1f9e8fe 100644 --- a/doc/ci/yaml/index.md +++ b/doc/ci/yaml/index.md @@ -422,23 +422,30 @@ A configuration with different pipeline names depending on the pipeline conditio ```yaml variables: - PIPELINE_NAME: 'Default pipeline name' # A default is not required. + PROJECT1_PIPELINE_NAME: 'Default pipeline name' # A default is not required. workflow: - name: '$PIPELINE_NAME' + name: '$PROJECT1_PIPELINE_NAME' rules: - if: '$CI_PIPELINE_SOURCE == "merge_request_event"' variables: - PIPELINE_NAME: 'MR pipeline: $CI_COMMIT_BRANCH' + PROJECT1_PIPELINE_NAME: 'MR pipeline: $CI_MERGE_REQUEST_SOURCE_BRANCH_NAME' - if: '$CI_MERGE_REQUEST_LABELS =~ /pipeline:run-in-ruby3/' variables: - PIPELINE_NAME: 'Ruby 3 pipeline' + PROJECT1_PIPELINE_NAME: 'Ruby 3 pipeline' ``` **Additional details**: - If the name is an empty string, the pipeline is not assigned a name. A name consisting of only CI/CD variables could evaluate to an empty string if all the variables are also empty. +- `workflow:rules:variables` become [global variables](#variables) available in all jobs, + including [`trigger`](#trigger) jobs which forward variables to downstream pipelines by default. + If the downstream pipeline uses the same variable, the [variable is overwritten](../variables/index.md#cicd-variable-precedence) + by the upstream variable value. Be sure to either: + - Use a unique variable name in every project's pipeline configuration, like `PROJECT1_PIPELINE_NAME`. + - Use [`inherit:variables`](#inheritvariables) in the trigger job to select the + exact variables to forward to the downstream pipeline. #### `workflow:rules` @@ -552,6 +559,16 @@ When the branch is something else: - job1's `DEPLOY_VARIABLE` is `job1-default-deploy`. - job2's `DEPLOY_VARIABLE` is `default-deploy`. +**Additional details**: + +- `workflow:rules:variables` become [global variables](#variables) available in all jobs, + including [`trigger`](#trigger) jobs which forward variables to downstream pipelines by default. + If the downstream pipeline uses the same variable, the [variable is overwritten](../variables/index.md#cicd-variable-precedence) + by the upstream variable value. Be sure to either: + - Use unique variable names in every project's pipeline configuration, like `PROJECT1_VARIABLE_NAME`. + - Use [`inherit:variables`](#inheritvariables) in the trigger job to select the + exact variables to forward to the downstream pipeline. + ## Job keywords The following topics explain how to use keywords to configure CI/CD pipelines. diff --git a/lib/gitlab/nav/top_nav_menu_item.rb b/lib/gitlab/nav/top_nav_menu_item.rb index 75eb0e8a264..af88f55014c 100644 --- a/lib/gitlab/nav/top_nav_menu_item.rb +++ b/lib/gitlab/nav/top_nav_menu_item.rb @@ -8,7 +8,10 @@ module Gitlab # this is already :/. We could also take a hash and manually check every # entry, but it's much more maintainable to do rely on native Ruby. # rubocop: disable Metrics/ParameterLists - def self.build(id:, title:, active: false, icon: '', href: '', view: '', css_class: nil, data: nil, emoji: nil) + def self.build( + id:, title:, active: false, icon: '', href: '', view: '', + css_class: nil, data: nil, partial: nil + ) { id: id, type: :item, @@ -19,7 +22,7 @@ module Gitlab view: view.to_s, css_class: css_class, data: data || { qa_selector: 'menu_item_link', qa_title: title }, - emoji: emoji + partial: partial } end # rubocop: enable Metrics/ParameterLists diff --git a/lib/sidebars/groups/menus/scope_menu.rb b/lib/sidebars/groups/menus/scope_menu.rb index 6ce43491343..f6e0906dac4 100644 --- a/lib/sidebars/groups/menus/scope_menu.rb +++ b/lib/sidebars/groups/menus/scope_menu.rb @@ -16,7 +16,7 @@ module Sidebars override :active_routes def active_routes - { path: %w[groups#show groups#details] } + { path: %w[groups#show groups#details groups#new projects#new] } end override :extra_nav_link_html_options diff --git a/locale/gitlab.pot b/locale/gitlab.pot index da09ebd8400..ea74c2390ac 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -20441,6 +20441,12 @@ msgstr "" msgid "GroupsNew|Importing groups by direct transfer is currently disabled." msgstr "" +msgid "GroupsNew|New group" +msgstr "" + +msgid "GroupsNew|New subgroup" +msgstr "" + msgid "GroupsNew|No import options available" msgstr "" @@ -34249,6 +34255,9 @@ msgstr "" msgid "ProjectsNew|Must start with a lowercase or uppercase letter, digit, emoji, or underscore. Can also contain dots, pluses, dashes, or spaces." msgstr "" +msgid "ProjectsNew|New project" +msgstr "" + msgid "ProjectsNew|No import options available" msgstr "" diff --git a/package.json b/package.json index c9339d3c1bb..3a502402912 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,7 @@ "@gitlab/favicon-overlay": "2.0.0", "@gitlab/fonts": "^1.2.0", "@gitlab/svgs": "3.20.0", - "@gitlab/ui": "55.2.1", + "@gitlab/ui": "56.0.0", "@gitlab/visual-review-tools": "1.7.3", "@gitlab/web-ide": "0.0.1-dev-20230216131813", "@rails/actioncable": "6.1.4-7", diff --git a/scripts/frontend/startup_css/constants.js b/scripts/frontend/startup_css/constants.js index 5143c04dc37..e6ca4472fe3 100644 --- a/scripts/frontend/startup_css/constants.js +++ b/scripts/frontend/startup_css/constants.js @@ -52,11 +52,14 @@ const createMainOutput = ({ outFile, cssKeys, type }) => ({ path.join(FIXTURES_ROOT, `startup_css/project-${type}.html`), path.join(FIXTURES_ROOT, `startup_css/project-${type}-signed-out.html`), path.join(FIXTURES_ROOT, `startup_css/project-${type}-search-ff-off.html`), + path.join(FIXTURES_ROOT, `startup_css/project-${type}-super-sidebar.html`), ], cssKeys, purgeOptions: { safelist: { standard: [ + 'page-with-super-sidebar', + 'page-with-super-sidebar-collapsed', 'page-with-icon-sidebar', 'sidebar-collapsed-desktop', // We want to include the root dropdown-menu style since it should be hidden by default diff --git a/spec/controllers/concerns/analytics/cycle_analytics/value_stream_actions_spec.rb b/spec/controllers/concerns/analytics/cycle_analytics/value_stream_actions_spec.rb index 246119a8118..a6a0f2c11b4 100644 --- a/spec/controllers/concerns/analytics/cycle_analytics/value_stream_actions_spec.rb +++ b/spec/controllers/concerns/analytics/cycle_analytics/value_stream_actions_spec.rb @@ -1,8 +1,9 @@ # frozen_string_literal: true + require 'spec_helper' RSpec.describe Analytics::CycleAnalytics::ValueStreamActions, type: :controller, -feature_category: :planning_analytics do + feature_category: :planning_analytics do subject(:controller) do Class.new(ApplicationController) do include Analytics::CycleAnalytics::ValueStreamActions diff --git a/spec/features/groups/new_group_page_spec.rb b/spec/features/groups/new_group_page_spec.rb index a07c27331d9..4670df3fb5e 100644 --- a/spec/features/groups/new_group_page_spec.rb +++ b/spec/features/groups/new_group_page_spec.rb @@ -3,15 +3,14 @@ require 'spec_helper' RSpec.describe 'New group page', :js, feature_category: :subgroups do - let(:user) { create(:user) } - let(:group) { create(:group) } + let_it_be(:user) { create(:user) } + let_it_be(:parent_group) { create(:group) } before do + parent_group.add_owner(user) sign_in(user) end - it_behaves_like 'a dashboard page with sidebar', :new_group_path, :groups - describe 'new top level group alert' do context 'when a user visits the new group page' do it 'shows the new top level group alert' do @@ -22,8 +21,6 @@ RSpec.describe 'New group page', :js, feature_category: :subgroups do end context 'when a user visits the new sub group page' do - let(:parent_group) { create(:group) } - it 'does not show the new top level group alert' do visit new_group_path(parent_id: parent_group.id, anchor: 'create-group-pane') @@ -31,4 +28,19 @@ RSpec.describe 'New group page', :js, feature_category: :subgroups do end end end + + describe 'sidebar' do + context 'for a new top-level group' do + it_behaves_like 'a dashboard page with sidebar', :new_group_path, :groups + end + + context 'for a new subgroup' do + it 'shows the group sidebar of the parent group' do + visit new_group_path(parent_id: parent_group.id, anchor: 'create-group-pane') + expect(page).to have_selector( + ".nav-sidebar[aria-label=\"Group navigation\"] .context-header[title=\"#{parent_group.name}\"]" + ) + end + end + end end diff --git a/spec/features/projects/new_project_spec.rb b/spec/features/projects/new_project_spec.rb index c6a6ee68185..3cbfa14208f 100644 --- a/spec/features/projects/new_project_spec.rb +++ b/spec/features/projects/new_project_spec.rb @@ -578,4 +578,25 @@ RSpec.describe 'New project', :js, feature_category: :projects do it_behaves_like 'has instructions to enable OAuth' end end + + describe 'sidebar' do + let_it_be(:user) { create(:user) } + let_it_be(:parent_group) { create(:group) } + + before do + parent_group.add_owner(user) + sign_in(user) + end + + context 'for a new top-level project' do + it_behaves_like 'a dashboard page with sidebar', :new_project_path, :projects + end + + context 'for a new group project' do + it 'shows the group sidebar of the parent group' do + visit new_project_path(namespace_id: parent_group.id) + expect(page).to have_selector(".nav-sidebar[aria-label=\"Group navigation\"] .context-header[title=\"#{parent_group.name}\"]") + end + end + end end diff --git a/spec/frontend/batch_comments/components/preview_dropdown_spec.js b/spec/frontend/batch_comments/components/preview_dropdown_spec.js index f86e003ab5f..3a28bf4ade8 100644 --- a/spec/frontend/batch_comments/components/preview_dropdown_spec.js +++ b/spec/frontend/batch_comments/components/preview_dropdown_spec.js @@ -1,7 +1,6 @@ import Vue, { nextTick } from 'vue'; import Vuex from 'vuex'; -import { GlDisclosureDropdown } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; +import { mount } from '@vue/test-utils'; import { TEST_HOST } from 'helpers/test_constants'; import { visitUrl } from '~/lib/utils/url_utility'; import PreviewDropdown from '~/batch_comments/components/preview_dropdown.vue'; @@ -46,9 +45,11 @@ function factory({ viewDiffsFileByFile = false, draftsCount = 1, sortedDrafts = }, }); - wrapper = shallowMount(PreviewDropdown, { + wrapper = mount(PreviewDropdown, { store, - stubs: { GlDisclosureDropdown }, + stubs: { + PreviewItem: true, + }, }); } @@ -59,12 +60,12 @@ describe('Batch comments preview dropdown', () => { viewDiffsFileByFile: true, sortedDrafts: [{ id: 1, file_hash: 'hash' }], }); - - findPreviewItem().vm.$emit('click'); - + findPreviewItem().trigger('click'); await nextTick(); expect(setCurrentFileHash).toHaveBeenCalledWith(expect.anything(), 'hash'); + + await nextTick(); expect(scrollToDraft).toHaveBeenCalledWith( expect.anything(), expect.objectContaining({ id: 1, file_hash: 'hash' }), @@ -77,7 +78,7 @@ describe('Batch comments preview dropdown', () => { sortedDrafts: [{ id: 1 }], }); - findPreviewItem().vm.$emit('click'); + findPreviewItem().trigger('click'); await nextTick(); @@ -93,7 +94,7 @@ describe('Batch comments preview dropdown', () => { sortedDrafts: [{ id: 1, position: { head_sha: '1234' } }], }); - findPreviewItem().vm.$emit('click'); + findPreviewItem().trigger('click'); await nextTick(); diff --git a/spec/frontend/fixtures/startup_css.rb b/spec/frontend/fixtures/startup_css.rb index bd2d63a1827..2d766086cb7 100644 --- a/spec/frontend/fixtures/startup_css.rb +++ b/spec/frontend/fixtures/startup_css.rb @@ -55,6 +55,21 @@ RSpec.describe 'Startup CSS fixtures', type: :controller do expect(response).to be_successful end + + # This Feature Flag is off by default + # This ensures that the correct css is generated for super sidebar + # When the feature flag is off, the general startup will capture it + it "startup_css/project-#{type}-super-sidebar.html" do + stub_feature_flags(super_sidebar_nav: true) + user.update!(use_new_navigation: true) + + get :show, params: { + namespace_id: project.namespace.to_param, + id: project + } + + expect(response).to be_successful + end end describe ProjectsController, '(Startup CSS fixtures)', type: :controller do diff --git a/spec/frontend/pages/groups/new/components/app_spec.js b/spec/frontend/pages/groups/new/components/app_spec.js index ab483316086..a3aa87574b5 100644 --- a/spec/frontend/pages/groups/new/components/app_spec.js +++ b/spec/frontend/pages/groups/new/components/app_spec.js @@ -23,7 +23,9 @@ describe('App component', () => { it('creates correct component for group creation', () => { createComponent(); - expect(findNewNamespacePage().props('initialBreadcrumb')).toBe('New group'); + expect(findNewNamespacePage().props('initialBreadcrumbs')).toEqual([ + { href: '#', text: 'New group' }, + ]); expect(findCreateGroupPanel().title).toBe('Create group'); }); @@ -32,7 +34,9 @@ describe('App component', () => { createComponent(props); - expect(findNewNamespacePage().props('initialBreadcrumb')).toBe('parent'); + expect(findNewNamespacePage().props('initialBreadcrumbs')).toEqual([ + { href: '#', text: 'New group' }, + ]); expect(findCreateGroupPanel().title).toBe('Create subgroup'); expect(findCreateGroupPanel().detailProps).toEqual(props); }); diff --git a/spec/frontend/vue_shared/new_namespace/new_namespace_page_spec.js b/spec/frontend/vue_shared/new_namespace/new_namespace_page_spec.js index 6115dc6e61b..40f75a0a8d6 100644 --- a/spec/frontend/vue_shared/new_namespace/new_namespace_page_spec.js +++ b/spec/frontend/vue_shared/new_namespace/new_namespace_page_spec.js @@ -14,7 +14,7 @@ describe('Experimental new project creation app', () => { const DEFAULT_PROPS = { title: 'Create something', - initialBreadcrumb: 'Something', + initialBreadcrumbs: [{ text: 'Something', href: '#' }], panels: [ { name: 'panel1', selector: '#some-selector1' }, { name: 'panel2', selector: '#some-selector2' }, @@ -46,8 +46,8 @@ describe('Experimental new project creation app', () => { expect(findWelcomePage().exists()).toBe(true); }); - it('does not render breadcrumbs', () => { - expect(findBreadcrumb().exists()).toBe(false); + it('renders breadcrumbs', () => { + expect(findBreadcrumb().exists()).toBe(true); }); }); @@ -75,7 +75,7 @@ describe('Experimental new project creation app', () => { it('renders breadcrumbs', () => { const breadcrumb = findBreadcrumb(); expect(breadcrumb.exists()).toBe(true); - expect(breadcrumb.props().items[0].text).toBe(DEFAULT_PROPS.initialBreadcrumb); + expect(breadcrumb.props().items[0].text).toBe(DEFAULT_PROPS.initialBreadcrumbs[0].text); }); }); diff --git a/spec/helpers/groups_helper_spec.rb b/spec/helpers/groups_helper_spec.rb index 8b4ac6a7cfd..ce439e5bcdd 100644 --- a/spec/helpers/groups_helper_spec.rb +++ b/spec/helpers/groups_helper_spec.rb @@ -436,7 +436,8 @@ RSpec.describe GroupsHelper do it 'returns expected hash' do expect(subgroup_creation_data(subgroup)).to eq({ import_existing_group_path: '/groups/new#import-group-pane', - parent_group_name: name + parent_group_name: name, + parent_group_url: group_url(group) }) end end @@ -445,7 +446,8 @@ RSpec.describe GroupsHelper do it 'returns expected hash' do expect(subgroup_creation_data(group)).to eq({ import_existing_group_path: '/groups/new#import-group-pane', - parent_group_name: nil + parent_group_name: nil, + parent_group_url: nil }) end end diff --git a/spec/helpers/nav/new_dropdown_helper_spec.rb b/spec/helpers/nav/new_dropdown_helper_spec.rb index 3a66fe474ab..1df0ee8c18a 100644 --- a/spec/helpers/nav/new_dropdown_helper_spec.rb +++ b/spec/helpers/nav/new_dropdown_helper_spec.rb @@ -11,8 +11,12 @@ RSpec.describe Nav::NewDropdownHelper, feature_category: :navigation do let(:with_can_create_project) { false } let(:with_can_create_group) { false } let(:with_can_create_snippet) { false } + let(:with_context) { true } + let(:title) { 'Create new...' } - subject(:view_model) { helper.new_dropdown_view_model(project: current_project, group: current_group) } + subject(:view_model) do + helper.new_dropdown_view_model(project: current_project, group: current_group, with_context: with_context) + end before do allow(helper).to receive(:current_user) { current_user } @@ -22,7 +26,7 @@ RSpec.describe Nav::NewDropdownHelper, feature_category: :navigation do allow(user).to receive(:can?).with(:create_snippet) { with_can_create_snippet } end - shared_examples 'invite member item' do + shared_examples 'invite member item' do |partial| it 'shows invite member link with emoji' do expect(view_model[:menu_sections]).to eq( expected_menu_section( @@ -30,8 +34,9 @@ RSpec.describe Nav::NewDropdownHelper, feature_category: :navigation do menu_item: ::Gitlab::Nav::TopNavMenuItem.build( id: 'invite', title: 'Invite members', - emoji: 'shaking_hands', + icon: 'shaking_hands', href: expected_href, + partial: partial, data: { track_action: 'click_link_invite_members', track_label: 'plus_menu_dropdown', @@ -54,8 +59,13 @@ RSpec.describe Nav::NewDropdownHelper, feature_category: :navigation do end context 'when group and project are nil' do - it 'has no menu sections' do - expect(view_model[:menu_sections]).to eq([]) + it 'has base results' do + results = { + title: title, + menu_sections: [] + } + + expect(view_model).to eq(results) end context 'when can create project' do @@ -145,8 +155,27 @@ RSpec.describe Nav::NewDropdownHelper, feature_category: :navigation do .to receive(:can?).with(current_user, :admin_group_member, group) { with_can_admin_in_group } end - it 'has no menu sections' do - expect(view_model[:menu_sections]).to eq([]) + it 'has base results' do + results = { + title: title, + menu_sections: [], + context: group + } + + expect(view_model).to eq(results) + end + + context 'without context' do + let(:with_context) { false } + + it 'has base results' do + results = { + title: title, + menu_sections: [] + } + + expect(view_model).to eq(results) + end end context 'when can create projects in group' do @@ -199,7 +228,7 @@ RSpec.describe Nav::NewDropdownHelper, feature_category: :navigation do let(:expected_title) { 'In this group' } let(:expected_href) { "/groups/#{group.full_path}/-/group_members" } - it_behaves_like 'invite member item' + it_behaves_like 'invite member item', 'groups/invite_members_top_nav_link' end end @@ -219,8 +248,27 @@ RSpec.describe Nav::NewDropdownHelper, feature_category: :navigation do allow(helper).to receive(:can_admin_project_member?) { with_can_admin_project_member } end - it 'has no menu sections' do - expect(view_model[:menu_sections]).to eq([]) + it 'has base results with context' do + results = { + title: title, + menu_sections: [], + context: project + } + + expect(view_model).to eq(results) + end + + context 'without context' do + let(:with_context) { false } + + it 'has base results without context' do + results = { + title: title, + menu_sections: [] + } + + expect(view_model).to eq(results) + end end context 'with show_new_issue_link?' do @@ -296,7 +344,7 @@ RSpec.describe Nav::NewDropdownHelper, feature_category: :navigation do let(:expected_title) { 'In this project' } let(:expected_href) { "/#{project.path_with_namespace}/-/project_members" } - it_behaves_like 'invite member item' + it_behaves_like 'invite member item', 'projects/invite_members_top_nav_link' end end @@ -311,22 +359,36 @@ RSpec.describe Nav::NewDropdownHelper, feature_category: :navigation do allow(helper).to receive(:can?).with(current_user, :create_projects, group).and_return(true) end - it 'gives precedence to group over project' do - group_section = expected_menu_section( - title: 'In this group', + it 'gives precedence to project over group' do + project_section = expected_menu_section( + title: 'In this project', menu_item: ::Gitlab::Nav::TopNavMenuItem.build( - id: 'new_project', - title: 'New project/repository', - href: "/projects/new?namespace_id=#{group.id}", + id: 'new_issue', + title: 'New issue', + href: "/#{project.path_with_namespace}/-/issues/new", data: { - track_action: 'click_link_new_project_group', + track_action: 'click_link_new_issue', track_label: 'plus_menu_dropdown', - track_property: 'navigation_top' + track_property: 'navigation_top', + qa_selector: 'new_issue_link' } ) ) + results = { + title: title, + menu_sections: project_section, + context: project + } + + expect(view_model).to eq(results) + end + + context 'without context' do + let(:with_context) { false } - expect(view_model[:menu_sections]).to eq(group_section) + it 'does not include context' do + expect(view_model.keys).to match_array([:title, :menu_sections]) + end end end diff --git a/spec/lib/gitlab/nav/top_nav_menu_item_spec.rb b/spec/lib/gitlab/nav/top_nav_menu_item_spec.rb index 6632a8106ca..3377da5ca3a 100644 --- a/spec/lib/gitlab/nav/top_nav_menu_item_spec.rb +++ b/spec/lib/gitlab/nav/top_nav_menu_item_spec.rb @@ -14,7 +14,7 @@ RSpec.describe ::Gitlab::Nav::TopNavMenuItem, feature_category: :navigation do view: 'view', css_class: 'css_class', data: {}, - emoji: 'smile' + partial: 'groups/some_view_partial_file' } expect(described_class.build(**item)).to eq(item.merge(type: :item)) diff --git a/spec/views/groups/group_members/index.html.haml_spec.rb b/spec/views/groups/group_members/index.html.haml_spec.rb index 0b3b149238f..fdc6b09d32a 100644 --- a/spec/views/groups/group_members/index.html.haml_spec.rb +++ b/spec/views/groups/group_members/index.html.haml_spec.rb @@ -25,7 +25,6 @@ RSpec.describe 'groups/group_members/index', :aggregate_failures, feature_catego expect(rendered).to have_selector('.js-invite-group-trigger') expect(rendered).to have_selector('.js-invite-members-trigger') - expect(response).to render_template(partial: 'groups/_invite_members_modal') end end diff --git a/spec/views/layouts/header/_new_dropdown.haml_spec.rb b/spec/views/layouts/header/_new_dropdown.haml_spec.rb index 178448022d1..ef8b859c9d9 100644 --- a/spec/views/layouts/header/_new_dropdown.haml_spec.rb +++ b/spec/views/layouts/header/_new_dropdown.haml_spec.rb @@ -8,6 +8,7 @@ RSpec.describe 'layouts/header/_new_dropdown', feature_category: :navigation do shared_examples_for 'invite member selector' do context 'with ability to invite members' do it { is_expected.to have_link('Invite members', href: href) } + it { is_expected.to have_selector('.js-invite-members-modal') } end context 'without ability to invite members' do @@ -159,6 +160,29 @@ RSpec.describe 'layouts/header/_new_dropdown', feature_category: :navigation do expect(rendered).to have_link('New snippet', href: new_snippet_path) end + context 'when partial exists in a menu item' do + it 'renders the menu item partial without rendering invite modal partial' do + view_model = { + title: '_title_', + menu_sections: [ + { + title: '_section_title_', + menu_items: [ + ::Gitlab::Nav::TopNavMenuItem + .build(id: '_id_', title: '_title_', partial: 'groups/invite_members_top_nav_link') + ] + } + ] + } + + allow(view).to receive(:new_dropdown_view_model).and_return(view_model) + + render + + expect(response).to render_template(partial: 'groups/_invite_members_top_nav_link') + end + end + context 'when the user is not allowed to do anything' do let(:user) { create(:user, :external) } # rubocop:disable RSpec/FactoryBot/AvoidCreate diff --git a/spec/views/projects/empty.html.haml_spec.rb b/spec/views/projects/empty.html.haml_spec.rb index 6077dda3c98..a782bab161f 100644 --- a/spec/views/projects/empty.html.haml_spec.rb +++ b/spec/views/projects/empty.html.haml_spec.rb @@ -73,7 +73,6 @@ RSpec.describe 'projects/empty' do expect(rendered).to have_content('Invite your team') expect(rendered).to have_content('Add members to this project and start collaborating with your team.') expect(rendered).to have_selector('.js-invite-members-trigger') - expect(rendered).to have_selector('.js-invite-members-modal') expect(rendered).to have_selector('[data-label=invite_members_empty_project]') expect(rendered).to have_selector('[data-event=click_button]') expect(rendered).to have_selector('[data-trigger-source=project-empty-page]') @@ -87,7 +86,6 @@ RSpec.describe 'projects/empty' do expect(rendered).not_to have_content('Invite your team') expect(rendered).not_to have_selector('.js-invite-members-trigger') - expect(rendered).not_to have_selector('.js-invite-members-modal') end end end diff --git a/spec/views/projects/project_members/index.html.haml_spec.rb b/spec/views/projects/project_members/index.html.haml_spec.rb index 4c4cde01cca..2fcc5c6935b 100644 --- a/spec/views/projects/project_members/index.html.haml_spec.rb +++ b/spec/views/projects/project_members/index.html.haml_spec.rb @@ -28,7 +28,6 @@ RSpec.describe 'projects/project_members/index', :aggregate_failures, feature_ca expect(rendered).to have_selector('.js-invite-group-trigger') expect(rendered).to have_selector('.js-invite-members-trigger') expect(rendered).not_to have_content('Members can be added by project') - expect(response).to render_template(partial: 'projects/_invite_members_modal') end context 'when project is not allowed to share with group' do diff --git a/yarn.lock b/yarn.lock index 79f1fba387d..4da96720728 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1347,10 +1347,10 @@ resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-3.20.0.tgz#4ee4f2f24304d13ccce58f82c2ecd87e556f35b4" integrity sha512-nYTF4j5kon4XbBr/sAzuubgxjIne9+RTZLmSrSaL9FL4eyuv9aa7YMCcOrlIbYX5jlSYlcD+ck2F2M1sqXXOBA== -"@gitlab/ui@55.2.1": - version "55.2.1" - resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-55.2.1.tgz#140d8e2c7428777a745fb1cfb33c29c3f1133113" - integrity sha512-fIvGzM4nZtHWaUWfaM9PSPJJJFcnvCLCrdAsobiiMlxFFCEwDNm99oL+uzQgbatQRjjIdHqyFKFNMo3rg6U+AQ== +"@gitlab/ui@56.0.0": + version "56.0.0" + resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-56.0.0.tgz#fd3b144893b01a8eae408ce8ca4477240aeb493e" + integrity sha512-kuoI+q5zYmmK9stpb1YJKvJNTD+NXiG6EOCw5+UQDpo5PgeDzrQ4vm7JuocYU9dNZlHMR2vVPMxi3uG0+y5JkA== dependencies: "@popperjs/core" "^2.11.2" bootstrap-vue "2.20.1" |