diff --git a/app/assets/javascripts/pages/groups/new/components/app.vue b/app/assets/javascripts/pages/groups/new/components/app.vue index f01e5e595a30370db3e33a2e34292d9e1e4846d1..68ddcf3d8684244ffe7930df3f214d5f61d6b580 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 a555038ed5cbdd2222d66e27f698d7dc67ca2b5a..acaee097dc1e3ea99931fe0a3155a727cc58a296 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 3100029eb311d9564ec15fd70276f6224b87bac1..4f3924b04bfd3b93967c81855f34e98d1c4a479d 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 910244c657b17fc4cc18c436a63323360fb09254..0b190b6a88b8e1915439db6479c675197e8044e0 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/vue_shared/new_namespace/new_namespace_page.vue b/app/assets/javascripts/vue_shared/new_namespace/new_namespace_page.vue index 318adec23197d610dd03da24911ca8e62a35b96b..2533b3b5489789dd39b8974c20c4e6744fd944ec 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/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index 8f7a2c177b720c5be2589c897c68a334a9dcaaf8..70d482f26c17d0a76d7ebf795ef961a4d9899577 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -76,6 +76,7 @@ def index 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_controller.rb b/app/controllers/projects_controller.rb index 0338c912b533307bfd8c00738d5359bf67b40462..bb1f229e17f7907aa7907485213f3ae3df31cb8d 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -78,6 +78,8 @@ def new @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/groups_helper.rb b/app/helpers/groups_helper.rb index 129871ca3fd7c7e05b6490e08f3ae6092b8b76e2..ce64ac1f21f7567aca20f4172f08909a8196c99b 100644 --- a/app/helpers/groups_helper.rb +++ b/app/helpers/groups_helper.rb @@ -126,6 +126,7 @@ def project_list_sort_by 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/views/groups/_new_group_fields.html.haml b/app/views/groups/_new_group_fields.html.haml index 95990e8937cf32766cdb1e9c01a0bb0d81ff4ec5..ddf6e52796f86a8ec7ed198114178dff0ec31d7f 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/new.html.haml b/app/views/groups/new.html.haml index b75fda2f3441a85d866bd41078856c7a60598020..0878fbf9a359a64dd3aa996b62be941677aab447 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/layouts/dashboard.html.haml b/app/views/layouts/dashboard.html.haml index 89f238eb6b3ec89aef1202c300fee69054349901..1ac5f0a849722e1da7f96bef5429f40c1c23d8de 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/nav/sidebar/_group.html.haml b/app/views/layouts/nav/sidebar/_group.html.haml index c2b50bc0e52f9d9d4fbb9d0cfdc0fa8255c0d588..fd0e47b543f46388208eeed54e5998c4518fdc47 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/_new_project_fields.html.haml b/app/views/projects/_new_project_fields.html.haml index 53a1abdff3339967e8d0cf76c707e7bab85c09ec..27211ffb1e558ca0e0dd92385ac0f0d87608a7b5 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/new.html.haml b/app/views/projects/new.html.haml index 56581fe7b181b001b6e93886bca9f94c6349ab6e..a089dd60e7d5e2c66599885b3990433e82a1eb3d 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/ee/app/helpers/ee/groups_helper.rb b/ee/app/helpers/ee/groups_helper.rb index 6bd50941f776b76fed690d0bd60ba49970f49d2c..e6440db0407a382754779ca81edf61f0bbeb79a5 100644 --- a/ee/app/helpers/ee/groups_helper.rb +++ b/ee/app/helpers/ee/groups_helper.rb @@ -52,8 +52,8 @@ def deletion_adjourned_period def show_discover_group_security?(group) !!current_user && ::Gitlab.com? && - !@group.licensed_feature_available?(:security_dashboard) && - can?(current_user, :admin_group, @group) + !group.licensed_feature_available?(:security_dashboard) && + can?(current_user, :admin_group, group) end def show_group_activity_analytics? diff --git a/ee/spec/frontend/vue_shared/new_namespace/new_namespace_page_spec.js b/ee/spec/frontend/vue_shared/new_namespace/new_namespace_page_spec.js index e285c09d607f9aeacd2422edac477061b4172d6a..6c67a0a614e1c45aa086107f4ebf43d1b696ecf8 100644 --- a/ee/spec/frontend/vue_shared/new_namespace/new_namespace_page_spec.js +++ b/ee/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' }, diff --git a/lib/sidebars/groups/menus/scope_menu.rb b/lib/sidebars/groups/menus/scope_menu.rb index 6ce43491343b444042c04fdc1dfbb0fb7efc1449..f6e0906dac490bb8c63046bb80d66d19c87a7b77 100644 --- a/lib/sidebars/groups/menus/scope_menu.rb +++ b/lib/sidebars/groups/menus/scope_menu.rb @@ -16,7 +16,7 @@ def title 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 a9e933a0655a1c74d6b28f6dd958ca30108428b9..1ed8558324cedb1452f3753d6634f9869c090807 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -20381,6 +20381,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 "" @@ -34147,6 +34153,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/spec/features/groups/new_group_page_spec.rb b/spec/features/groups/new_group_page_spec.rb index a07c27331d99b74d8e9f45a45f09a9d09906cc35..4670df3fb5eef31ad767824d88ba59f16d3a21a3 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 @@ 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 @@ 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 c6a6ee6818540e71e7aa0e63502e3d92471a1303..3cbfa14208f9db84e6801022d64cdb344ca2eb4f 100644 --- a/spec/features/projects/new_project_spec.rb +++ b/spec/features/projects/new_project_spec.rb @@ -578,4 +578,25 @@ 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/pages/groups/new/components/app_spec.js b/spec/frontend/pages/groups/new/components/app_spec.js index ab4833160860ee0776f9f78580a7f3c099b5a3b2..a3aa87574b55c5187c404eb5b417ca314c1a6e86 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 6115dc6e61b47bc05d2d6be5e1e55a89df56cd31..40f75a0a8d686ee34077edb7c7ea4780e993d32d 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 8b4ac6a7cfd323b335c01bcfbdf08aa024a5cb4c..ce439e5bcddd8852be00091d4923ce97c6cb5478 100644 --- a/spec/helpers/groups_helper_spec.rb +++ b/spec/helpers/groups_helper_spec.rb @@ -436,7 +436,8 @@ 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 @@ 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