Commit db58b29a authored by Illya Klymov's avatar Illya Klymov

Update group creation UI

- introduce card-based UI

Changelog: changed
parent af493f8b
<script>
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 NewNamespacePage from '~/vue_shared/new_namespace/new_namespace_page.vue';
import createGroupDescriptionDetails from './create_group_description_details.vue';
const PANELS = [
{
name: 'create-group-pane',
selector: '#create-group-pane',
title: s__('GroupsNew|Create group'),
description: s__(
'GroupsNew|Assemble related projects together and grant members access to several projects at once.',
),
illustration: newGroupIllustration,
details: createGroupDescriptionDetails,
},
{
name: 'import-group-pane',
selector: '#import-group-pane',
title: s__('GroupsNew|Import group'),
description: s__(
'GroupsNew|Export groups with all their related data and move to a new GitLab instance.',
),
illustration: importGroupIllustration,
details: 'Migrate your existing groups from another instance of GitLab.',
},
];
export default {
components: {
NewNamespacePage,
},
props: {
hasErrors: {
type: Boolean,
required: false,
default: false,
},
},
PANELS,
};
</script>
<template>
<new-namespace-page
:jump-to-last-persisted-panel="hasErrors"
:initial-breadcrumb="s__('New group')"
:panels="$options.PANELS"
:title="s__('GroupsNew|Create new group')"
persistence-key="new_group_last_active_tab"
/>
</template>
<script>
import { GlSprintf, GlLink } from '@gitlab/ui';
import { helpPagePath } from '~/helpers/help_page_helper';
export default {
components: {
GlLink,
GlSprintf,
},
computed: {
groupsHelpPath() {
return helpPagePath('user/group/index');
},
subgroupsHelpPath() {
return helpPagePath('user/group/subgroups/index');
},
},
};
</script>
<template>
<div>
<p>
<gl-sprintf
:message="
s__(
'GroupsNew|%{linkStart}Groups%{linkEnd} allow you to manage and collaborate across multiple projects. Members of a group have access to all of its projects. Groups can also be nested by creating subgroups.',
)
"
>
<template #link="{ content }">
<gl-link :href="groupsHelpPath" target="_blank">{{ content }}</gl-link>
</template>
</gl-sprintf>
</p>
<p>
<gl-sprintf
:message="
s__('GroupsNew|Groups can also be nested by creating %{linkStart}subgroups%{linkEnd}.')
"
>
<template #link="{ content }">
<gl-link :href="subgroupsHelpPath" target="_blank">{{ content }}</gl-link>
</template>
</gl-sprintf>
</p>
</div>
</template>
import $ from 'jquery'; import Vue from 'vue';
import BindInOut from '~/behaviors/bind_in_out'; import BindInOut from '~/behaviors/bind_in_out';
import initFilePickers from '~/file_pickers'; import initFilePickers from '~/file_pickers';
import Group from '~/group'; import Group from '~/group';
import LinkedTabs from '~/lib/utils/bootstrap_linked_tabs'; import { parseBoolean } from '~/lib/utils/common_utils';
import NewGroupCreationApp from './components/app.vue';
import GroupPathValidator from './group_path_validator'; import GroupPathValidator from './group_path_validator';
new GroupPathValidator(); // eslint-disable-line no-new new GroupPathValidator(); // eslint-disable-line no-new
...@@ -12,15 +13,21 @@ initFilePickers(); ...@@ -12,15 +13,21 @@ initFilePickers();
new Group(); // eslint-disable-line no-new new Group(); // eslint-disable-line no-new
const CONTAINER_SELECTOR = '.group-edit-container .nav-tabs'; function initNewGroupCreation(el) {
const DEFAULT_ACTION = '#create-group-pane'; const { hasErrors } = el.dataset;
// eslint-disable-next-line no-new
new LinkedTabs({ const props = {
defaultAction: DEFAULT_ACTION, hasErrors: parseBoolean(hasErrors),
parentEl: CONTAINER_SELECTOR, };
hashedTabs: true,
}); return new Vue({
el,
if (window.location.hash) { render(h) {
$(CONTAINER_SELECTOR).find(`a[href="${window.location.hash}"]`).tab('show'); return h(NewGroupCreationApp, { props });
},
});
} }
const el = document.querySelector('.js-new-group-creation');
initNewGroupCreation(el);
...@@ -2,47 +2,24 @@ ...@@ -2,47 +2,24 @@
- @hide_top_links = true - @hide_top_links = true
- page_title _('New Group') - page_title _('New Group')
- header_title _("Groups"), dashboard_groups_path - header_title _("Groups"), dashboard_groups_path
- active_tab = local_assigns.fetch(:active_tab, 'create') - add_page_specific_style 'page_bundles/new_namespace'
.group-edit-container.gl-mt-3 .group-edit-container.gl-mt-5
.row
.col-lg-3.group-settings-sidebar
%h4.prepend-top-0
= _('New group')
%p
- group_docs_path = help_page_path('user/group/index')
- group_docs_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: group_docs_path }
= s_('%{group_docs_link_start}Groups%{group_docs_link_end} allow you to manage and collaborate across multiple projects. Members of a group have access to all of its projects.').html_safe % { group_docs_link_start: group_docs_link_start, group_docs_link_end: '</a>'.html_safe }
%p
- subgroup_docs_path = help_page_path('user/group/subgroups/index')
- subgroup_docs_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: subgroup_docs_path }
= s_('Groups can also be nested by creating %{subgroup_docs_link_start}subgroups%{subgroup_docs_link_end}.').html_safe % { subgroup_docs_link_start: subgroup_docs_link_start, subgroup_docs_link_end: '</a>'.html_safe }
%p
= _('Projects that belong to a group are prefixed with the group namespace. Existing projects may be moved into a group.')
.col-lg-9.js-toggle-container .js-new-group-creation{ data: { has_errors: @group.errors.any?.to_s } }
%ul.nav.nav-tabs.nav-links.gitlab-tabs{ role: 'tablist' }
%li.nav-item{ role: 'presentation' }
%a.nav-link.active{ href: '#create-group-pane', id: 'create-group-tab', role: 'tab', data: { toggle: 'tab', track_label: 'create_group', track_event: 'click_tab', track_value: '' } }
%span.d-none.d-sm-block= s_('GroupsNew|Create group')
%span.d-block.d-sm-none= s_('GroupsNew|Create')
%li.nav-item{ role: 'presentation' }
%a.nav-link{ href: '#import-group-pane', id: 'import-group-tab', role: 'tab', data: { toggle: 'tab', track_label: 'import_group', track_event: 'click_tab', track_value: '' } }
%span.d-none.d-sm-block= s_('GroupsNew|Import group')
%span.d-block.d-sm-none= s_('GroupsNew|Import')
.tab-content.gitlab-tab-content.gl-border-none .row{ 'v-cloak': true }
.tab-pane.js-toggle-container{ id: 'create-group-pane', class: active_when(active_tab == 'create'), role: 'tabpanel' } #create-group-pane.tab-pane
= form_for @group, html: { class: 'group-form gl-show-field-errors' } do |f| = form_for @group, html: { class: 'group-form gl-show-field-errors' } do |f|
= render 'new_group_fields', f: f, group_name_id: 'create-group-name' = render 'new_group_fields', f: f, group_name_id: 'create-group-name'
.tab-pane.no-padding.js-toggle-container{ id: 'import-group-pane', class: active_when(active_tab) == 'import', role: 'tabpanel' } #import-group-pane.tab-pane
- if import_sources_enabled? - if import_sources_enabled?
- if Feature.enabled?(:bulk_import) - if Feature.enabled?(:bulk_import)
= render 'import_group_from_another_instance_panel' = render 'import_group_from_another_instance_panel'
.gl-mt-7.gl-border-b-solid.gl-border-gray-100.gl-border-1 .gl-mt-7.gl-border-b-solid.gl-border-gray-100.gl-border-1
= render 'import_group_from_file_panel' = render 'import_group_from_file_panel'
- else - else
.nothing-here-block .nothing-here-block
%h4= s_('GroupsNew|No import options available') %h4= s_('GroupsNew|No import options available')
%p= s_('GroupsNew|Contact an administrator to enable options for importing your group.') %p= s_('GroupsNew|Contact an administrator to enable options for importing your group.')
...@@ -105,7 +105,7 @@ on an existing group's page. ...@@ -105,7 +105,7 @@ on an existing group's page.
![Navigation paths to create a new group](img/new_group_navigation_v13_8.png) ![Navigation paths to create a new group](img/new_group_navigation_v13_8.png)
1. On the New Group page, select the **Import group** tab. 1. On the New Group page, select **Import group**.
![Fill in import details](img/import_panel_v13_8.png) ![Fill in import details](img/import_panel_v13_8.png)
......
...@@ -51,6 +51,7 @@ To create a group: ...@@ -51,6 +51,7 @@ To create a group:
1. From the top menu, either: 1. From the top menu, either:
- Select **Groups > Your Groups**, and on the right, select the **New group** button. - Select **Groups > Your Groups**, and on the right, select the **New group** button.
- To the left of the search box, select the plus sign and then **New group**. - To the left of the search box, select the plus sign and then **New group**.
1. Select **Create group**
1. For the **Group name**, use only: 1. For the **Group name**, use only:
- Alphanumeric characters - Alphanumeric characters
- Emojis - Emojis
......
...@@ -558,9 +558,6 @@ msgstr "" ...@@ -558,9 +558,6 @@ msgstr ""
msgid "%{global_id} is not a valid ID for %{expected_types}." msgid "%{global_id} is not a valid ID for %{expected_types}."
msgstr "" msgstr ""
msgid "%{group_docs_link_start}Groups%{group_docs_link_end} allow you to manage and collaborate across multiple projects. Members of a group have access to all of its projects."
msgstr ""
msgid "%{group_name} activity" msgid "%{group_name} activity"
msgstr "" msgstr ""
...@@ -16085,9 +16082,6 @@ msgstr "" ...@@ -16085,9 +16082,6 @@ msgstr ""
msgid "Groups and subgroups" msgid "Groups and subgroups"
msgstr "" msgstr ""
msgid "Groups can also be nested by creating %{subgroup_docs_link_start}subgroups%{subgroup_docs_link_end}."
msgstr ""
msgid "Groups to synchronize" msgid "Groups to synchronize"
msgstr "" msgstr ""
...@@ -16124,22 +16118,31 @@ msgstr "" ...@@ -16124,22 +16118,31 @@ msgstr ""
msgid "GroupsEmptyState|You can manage your group member’s permissions and access to each project in the group." msgid "GroupsEmptyState|You can manage your group member’s permissions and access to each project in the group."
msgstr "" msgstr ""
msgid "GroupsNew|%{linkStart}Groups%{linkEnd} allow you to manage and collaborate across multiple projects. Members of a group have access to all of its projects. Groups can also be nested by creating subgroups."
msgstr ""
msgid "GroupsNew|Assemble related projects together and grant members access to several projects at once."
msgstr ""
msgid "GroupsNew|Connect instance" msgid "GroupsNew|Connect instance"
msgstr "" msgstr ""
msgid "GroupsNew|Contact an administrator to enable options for importing your group." msgid "GroupsNew|Contact an administrator to enable options for importing your group."
msgstr "" msgstr ""
msgid "GroupsNew|Create" msgid "GroupsNew|Create group"
msgstr "" msgstr ""
msgid "GroupsNew|Create group" msgid "GroupsNew|Create new group"
msgstr ""
msgid "GroupsNew|Export groups with all their related data and move to a new GitLab instance."
msgstr "" msgstr ""
msgid "GroupsNew|GitLab source URL" msgid "GroupsNew|GitLab source URL"
msgstr "" msgstr ""
msgid "GroupsNew|Import" msgid "GroupsNew|Groups can also be nested by creating %{linkStart}subgroups%{linkEnd}."
msgstr "" msgstr ""
msgid "GroupsNew|Import group" msgid "GroupsNew|Import group"
...@@ -26163,9 +26166,6 @@ msgstr "" ...@@ -26163,9 +26166,6 @@ msgstr ""
msgid "Projects shared with %{group_name}" msgid "Projects shared with %{group_name}"
msgstr "" msgstr ""
msgid "Projects that belong to a group are prefixed with the group namespace. Existing projects may be moved into a group."
msgstr ""
msgid "Projects to index" msgid "Projects to index"
msgstr "" msgstr ""
......
...@@ -38,6 +38,14 @@ module QA ...@@ -38,6 +38,14 @@ module QA
fill_element(:import_gitlab_token, token) fill_element(:import_gitlab_token, token)
end end
def click_import_group
click_on 'Import group'
end
def click_create_group
click_on 'Create group'
end
# Connect gitlab instance # Connect gitlab instance
# #
# @param [String] gitlab_url # @param [String] gitlab_url
......
...@@ -29,6 +29,7 @@ module QA ...@@ -29,6 +29,7 @@ module QA
group_show.go_to_new_subgroup group_show.go_to_new_subgroup
Page::Group::New.perform do |group_new| Page::Group::New.perform do |group_new|
group_new.click_create_group
group_new.set_path(path) group_new.set_path(path)
group_new.set_visibility('Public') group_new.set_visibility('Public')
group_new.create group_new.create
......
...@@ -10,9 +10,13 @@ module QA ...@@ -10,9 +10,13 @@ module QA
Page::Dashboard::Groups.perform do |groups| Page::Dashboard::Groups.perform do |groups|
groups.click_new_group groups.click_new_group
expect(groups).to have_content( Page::Group::New.perform do |group_new|
/Create a Mattermost team for this group/ group_new.click_create_group
)
expect(group_new).to have_content(
/Create a Mattermost team for this group/
)
end
end end
end end
end end
......
...@@ -16,6 +16,8 @@ RSpec.describe 'Dashboard Group' do ...@@ -16,6 +16,8 @@ RSpec.describe 'Dashboard Group' do
it 'creates new group', :js do it 'creates new group', :js do
visit dashboard_groups_path visit dashboard_groups_path
find('[data-testid="new-group-button"]').click find('[data-testid="new-group-button"]').click
click_link 'Create group'
new_name = 'Samurai' new_name = 'Samurai'
fill_in 'group_name', with: new_name fill_in 'group_name', with: new_name
......
...@@ -15,7 +15,7 @@ RSpec.describe 'Import/Export - Connect to another instance', :js do ...@@ -15,7 +15,7 @@ RSpec.describe 'Import/Export - Connect to another instance', :js do
visit new_group_path visit new_group_path
find('#import-group-tab').click click_link 'Import group'
end end
context 'when the user provides valid credentials' do context 'when the user provides valid credentials' do
......
...@@ -28,9 +28,9 @@ RSpec.describe 'Import/Export - Group Import', :js do ...@@ -28,9 +28,9 @@ RSpec.describe 'Import/Export - Group Import', :js do
group_name = 'Test Group Import' group_name = 'Test Group Import'
visit new_group_path visit new_group_path
click_link 'Import group'
fill_in :group_name, with: group_name fill_in :import_group_name, with: group_name
find('#import-group-tab').click
expect(page).to have_content 'Import group from file' expect(page).to have_content 'Import group from file'
attach_file(file) do attach_file(file) do
...@@ -51,9 +51,9 @@ RSpec.describe 'Import/Export - Group Import', :js do ...@@ -51,9 +51,9 @@ RSpec.describe 'Import/Export - Group Import', :js do
context 'when modifying the pre-filled path' do context 'when modifying the pre-filled path' do
it 'successfully imports the group' do it 'successfully imports the group' do
visit new_group_path visit new_group_path
click_link 'Import group'
fill_in :group_name, with: 'Test Group Import' fill_in :import_group_name, with: 'Test Group Import'
find('#import-group-tab').click
fill_in :import_group_path, with: 'custom-path' fill_in :import_group_path, with: 'custom-path'
attach_file(file) do attach_file(file) do
...@@ -74,7 +74,7 @@ RSpec.describe 'Import/Export - Group Import', :js do ...@@ -74,7 +74,7 @@ RSpec.describe 'Import/Export - Group Import', :js do
it 'suggests a unique path' do it 'suggests a unique path' do
visit new_group_path visit new_group_path
find('#import-group-tab').click click_link 'Import group'
fill_in :import_group_path, with: 'test-group-import' fill_in :import_group_path, with: 'test-group-import'
expect(page).to have_content 'Group path is already taken. Suggestions: test-group-import1' expect(page).to have_content 'Group path is already taken. Suggestions: test-group-import1'
...@@ -87,9 +87,9 @@ RSpec.describe 'Import/Export - Group Import', :js do ...@@ -87,9 +87,9 @@ RSpec.describe 'Import/Export - Group Import', :js do
it 'displays an error' do it 'displays an error' do
visit new_group_path visit new_group_path
click_link 'Import group'
fill_in :group_name, with: 'Test Group Import' fill_in :import_group_name, with: 'Test Group Import'
find('#import-group-tab').click
attach_file(file) do attach_file(file) do
find('.js-filepicker-button').click find('.js-filepicker-button').click
end end
......
...@@ -15,9 +15,10 @@ RSpec.describe 'Group' do ...@@ -15,9 +15,10 @@ RSpec.describe 'Group' do
end end
end end
describe 'create a group' do describe 'create a group', :js do
before do before do
visit new_group_path visit new_group_path
click_link 'Create group'
end end
describe 'as a non-admin' do describe 'as a non-admin' do
...@@ -50,13 +51,14 @@ RSpec.describe 'Group' do ...@@ -50,13 +51,14 @@ RSpec.describe 'Group' do
fill_in 'Group URL', with: 'space group' fill_in 'Group URL', with: 'space group'
click_button 'Create group' click_button 'Create group'
expect(current_path).to eq(groups_path) expect(current_path).to eq(new_group_path)
expect(page).to have_namespace_error_message expect(page).to have_text('Please choose a group URL with no special characters.')
end end
end end
describe 'with .atom at end of group path' do describe 'with .atom at end of group path' do
it 'renders new group form with validation errors' do it 'renders new group form with validation errors' do
fill_in 'Group name', with: 'test-group'
fill_in 'Group URL', with: 'atom_group.atom' fill_in 'Group URL', with: 'atom_group.atom'
click_button 'Create group' click_button 'Create group'
...@@ -67,6 +69,7 @@ RSpec.describe 'Group' do ...@@ -67,6 +69,7 @@ RSpec.describe 'Group' do
describe 'with .git at end of group path' do describe 'with .git at end of group path' do
it 'renders new group form with validation errors' do it 'renders new group form with validation errors' do
fill_in 'Group name', with: 'test-group'
fill_in 'Group URL', with: 'git_group.git' fill_in 'Group URL', with: 'git_group.git'
click_button 'Create group' click_button 'Create group'
...@@ -109,6 +112,7 @@ RSpec.describe 'Group' do ...@@ -109,6 +112,7 @@ RSpec.describe 'Group' do
stub_mattermost_setting(enabled: mattermost_enabled) stub_mattermost_setting(enabled: mattermost_enabled)
visit new_group_path visit new_group_path
click_link 'Create group'
end end
context 'Mattermost enabled' do context 'Mattermost enabled' do
...@@ -119,7 +123,7 @@ RSpec.describe 'Group' do ...@@ -119,7 +123,7 @@ RSpec.describe 'Group' do
end end
it 'unchecks the checkbox by default' do it 'unchecks the checkbox by default' do
expect(find('#group_create_chat_team')['checked']).to eq(false) expect(find('#group_create_chat_team')).not_to be_checked
end end
it 'updates the team URL on graph path update', :js do it 'updates the team URL on graph path update', :js do
...@@ -147,6 +151,7 @@ RSpec.describe 'Group' do ...@@ -147,6 +151,7 @@ RSpec.describe 'Group' do
stub_application_setting(recaptcha_enabled: true) stub_application_setting(recaptcha_enabled: true)
allow(Gitlab::Recaptcha).to receive(:load_configurations!) allow(Gitlab::Recaptcha).to receive(:load_configurations!)
visit new_group_path visit new_group_path
click_link 'Create group'
end end
it 'renders recaptcha' do it 'renders recaptcha' do
...@@ -159,6 +164,7 @@ RSpec.describe 'Group' do ...@@ -159,6 +164,7 @@ RSpec.describe 'Group' do
stub_feature_flags(recaptcha_on_top_level_group_creation: false) stub_feature_flags(recaptcha_on_top_level_group_creation: false)
stub_application_setting(recaptcha_enabled: true) stub_application_setting(recaptcha_enabled: true)
visit new_group_path visit new_group_path
click_link 'Create group'
end end
it 'does not render recaptcha' do it 'does not render recaptcha' do
...@@ -167,30 +173,30 @@ RSpec.describe 'Group' do ...@@ -167,30 +173,30 @@ RSpec.describe 'Group' do
end end
end end
describe 'create a nested group' do describe 'create a nested group', :js do
let_it_be(:group) { create(:group, path: 'foo') } let_it_be(:group) { create(:group, path: 'foo') }
context 'as admin' do context 'as admin' do
let(:user) { create(:admin) } let(:user) { create(:admin) }
before do before do
visit new_group_path(group, parent_id: group.id) visit new_group_path(parent_id: group.id)
end end
context 'when admin mode is enabled', :enable_admin_mode do context 'when admin mode is enabled', :enable_admin_mode do
it 'creates a nested group' do it 'creates a nested group' do
click_link 'Create group'
fill_in 'Group name', with: 'bar' fill_in 'Group name', with: 'bar'
fill_in 'Group URL', with: 'bar'
click_button 'Create group' click_button 'Create group'
expect(current_path).to eq(group_path('foo/bar')) expect(current_path).to eq(group_path('foo/bar'))
expect(page).to have_content("Group 'bar' was successfully created.") expect(page).to have_selector 'h1', text: 'bar'
end end
end end
context 'when admin mode is disabled' do context 'when admin mode is disabled' do
it 'is not allowed' do it 'is not allowed' do
expect(page).to have_gitlab_http_status(:not_found) expect(page).not_to have_button('Create group')
end end
end end
end end
...@@ -203,14 +209,14 @@ RSpec.describe 'Group' do ...@@ -203,14 +209,14 @@ RSpec.describe 'Group' do
sign_out(:user) sign_out(:user)
sign_in(user) sign_in(user)
visit new_group_path(group, parent_id: group.id) visit new_group_path(parent_id: group.id)
click_link 'Create group'
fill_in 'Group name', with: 'bar' fill_in 'Group name', with: 'bar'
fill_in 'Group URL', with: 'bar'
click_button 'Create group' click_button 'Create group'
expect(current_path).to eq(group_path('foo/bar')) expect(current_path).to eq(group_path('foo/bar'))
expect(page).to have_content("Group 'bar' was successfully created.") expect(page).to have_selector 'h1', text: 'bar'
end end
end end
...@@ -221,7 +227,7 @@ RSpec.describe 'Group' do ...@@ -221,7 +227,7 @@ RSpec.describe 'Group' do
end end
context 'when creating subgroup' do context 'when creating subgroup' do
let(:path) { new_group_path(group, parent_id: group.id) } let(:path) { new_group_path(parent_id: group.id) }
it 'does not render recaptcha' do it 'does not render recaptcha' do
visit path visit path
...@@ -237,6 +243,7 @@ RSpec.describe 'Group' do ...@@ -237,6 +243,7 @@ RSpec.describe 'Group' do
before do before do
group.add_owner(user) group.add_owner(user)
visit new_group_path(parent_id: group.id) visit new_group_path(parent_id: group.id)
click_link 'Create group'
end end
it 'shows a message if group url is available' do it 'shows a message if group url is available' do
...@@ -255,14 +262,15 @@ RSpec.describe 'Group' do ...@@ -255,14 +262,15 @@ RSpec.describe 'Group' do
end end
end end
it 'checks permissions to avoid exposing groups by parent_id' do it 'checks permissions to avoid exposing groups by parent_id', :js do
group = create(:group, :private, path: 'secret-group') group = create(:group, :private, path: 'secret-group')
sign_out(:user) sign_out(:user)
sign_in(create(:user)) sign_in(create(:user))
visit new_group_path(parent_id: group.id) visit new_group_path(parent_id: group.id)
expect(page).not_to have_content('secret-group') expect(page).to have_title('Not Found')
expect(page).to have_content('Page Not Found')
end end
describe 'group edit', :js do describe 'group edit', :js do
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment