Commit 8f4b7c72 authored by Mike Greiling's avatar Mike Greiling

Merge branch '26732-update-new-project-page-dropdown' into 'master'

Update Project URL dropdown on New Project page

See merge request gitlab-org/gitlab!68778
parents 9c38b4c0 9ed057bc
export const MINIMUM_SEARCH_LENGTH = 3;
export const TYPE_CI_RUNNER = 'Ci::Runner'; export const TYPE_CI_RUNNER = 'Ci::Runner';
export const TYPE_EPIC = 'Epic'; export const TYPE_EPIC = 'Epic';
export const TYPE_GROUP = 'Group'; export const TYPE_GROUP = 'Group';
......
<script>
import {
GlButton,
GlButtonGroup,
GlDropdown,
GlDropdownItem,
GlDropdownSectionHeader,
GlLoadingIcon,
GlSearchBoxByType,
} from '@gitlab/ui';
import { MINIMUM_SEARCH_LENGTH } from '~/graphql_shared/constants';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import Tracking from '~/tracking';
import { DEBOUNCE_DELAY } from '~/vue_shared/components/filtered_search_bar/constants';
import searchNamespacesWhereUserCanCreateProjectsQuery from '../queries/search_namespaces_where_user_can_create_projects.query.graphql';
export default {
components: {
GlButton,
GlButtonGroup,
GlDropdown,
GlDropdownItem,
GlDropdownSectionHeader,
GlLoadingIcon,
GlSearchBoxByType,
},
mixins: [Tracking.mixin()],
apollo: {
currentUser: {
query: searchNamespacesWhereUserCanCreateProjectsQuery,
variables() {
return {
search: this.search,
};
},
skip() {
return this.search.length > 0 && this.search.length < MINIMUM_SEARCH_LENGTH;
},
debounce: DEBOUNCE_DELAY,
},
},
inject: ['namespaceFullPath', 'namespaceId', 'rootUrl', 'trackLabel'],
data() {
return {
currentUser: {},
search: '',
selectedNamespace: {
id: this.namespaceId,
fullPath: this.namespaceFullPath,
},
};
},
computed: {
userGroups() {
return this.currentUser.groups?.nodes || [];
},
userNamespace() {
return this.currentUser.namespace || {};
},
},
methods: {
handleClick({ id, fullPath }) {
this.selectedNamespace = {
id: getIdFromGraphQLId(id),
fullPath,
};
},
},
};
</script>
<template>
<gl-button-group class="gl-w-full">
<gl-button label>{{ rootUrl }}</gl-button>
<gl-dropdown
class="gl-w-full"
:text="selectedNamespace.fullPath"
toggle-class="gl-rounded-top-right-base! gl-rounded-bottom-right-base!"
data-qa-selector="select_namespace_dropdown"
@show="track('activate_form_input', { label: trackLabel, property: 'project_path' })"
>
<gl-search-box-by-type v-model.trim="search" />
<gl-loading-icon v-if="$apollo.queries.currentUser.loading" />
<template v-else>
<gl-dropdown-section-header>{{ __('Groups') }}</gl-dropdown-section-header>
<gl-dropdown-item v-for="group of userGroups" :key="group.id" @click="handleClick(group)">
{{ group.fullPath }}
</gl-dropdown-item>
<gl-dropdown-section-header>{{ __('Users') }}</gl-dropdown-section-header>
<gl-dropdown-item @click="handleClick(userNamespace)">
{{ userNamespace.fullPath }}
</gl-dropdown-item>
</template>
</gl-dropdown>
<input type="hidden" name="project[namespace_id]" :value="selectedNamespace.id" />
</gl-button-group>
</template>
import Vue from 'vue'; import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
import { parseBoolean } from '~/lib/utils/common_utils'; import { parseBoolean } from '~/lib/utils/common_utils';
import initProjectVisibilitySelector from '../../../project_visibility'; import initProjectVisibilitySelector from '../../../project_visibility';
import initProjectNew from '../../../projects/project_new'; import initProjectNew from '../../../projects/project_new';
import NewProjectCreationApp from './components/app.vue'; import NewProjectCreationApp from './components/app.vue';
import NewProjectUrlSelect from './components/new_project_url_select.vue';
initProjectVisibilitySelector(); function initNewProjectCreation() {
initProjectNew.bindEvents(); const el = document.querySelector('.js-new-project-creation');
function initNewProjectCreation(el) {
const { const {
pushToCreateProjectCommand, pushToCreateProjectCommand,
workingWithProjectsHelpPath, workingWithProjectsHelpPath,
...@@ -29,9 +31,6 @@ function initNewProjectCreation(el) { ...@@ -29,9 +31,6 @@ function initNewProjectCreation(el) {
return new Vue({ return new Vue({
el, el,
components: {
NewProjectCreationApp,
},
provide, provide,
render(h) { render(h) {
return h(NewProjectCreationApp, { props }); return h(NewProjectCreationApp, { props });
...@@ -39,6 +38,31 @@ function initNewProjectCreation(el) { ...@@ -39,6 +38,31 @@ function initNewProjectCreation(el) {
}); });
} }
const el = document.querySelector('.js-new-project-creation'); function initNewProjectUrlSelect() {
const el = document.querySelector('.js-vue-new-project-url-select');
if (!el) {
return undefined;
}
initNewProjectCreation(el); Vue.use(VueApollo);
return new Vue({
el,
apolloProvider: new VueApollo({
defaultClient: createDefaultClient({}, { assumeImmutableResults: true }),
}),
provide: {
namespaceFullPath: el.dataset.namespaceFullPath,
namespaceId: el.dataset.namespaceId,
rootUrl: el.dataset.rootUrl,
trackLabel: el.dataset.trackLabel,
},
render: (createElement) => createElement(NewProjectUrlSelect),
});
}
initProjectVisibilitySelector();
initProjectNew.bindEvents();
initNewProjectCreation();
initNewProjectUrlSelect();
query searchNamespacesWhereUserCanCreateProjects($search: String) {
currentUser {
groups(permissionScope: CREATE_PROJECTS, search: $search) {
nodes {
id
fullPath
}
}
namespace {
id
fullPath
}
}
}
...@@ -14,17 +14,19 @@ ...@@ -14,17 +14,19 @@
%span= s_("Project URL") %span= s_("Project URL")
.input-group.gl-flex-nowrap .input-group.gl-flex-nowrap
- if current_user.can_select_namespace? - if current_user.can_select_namespace?
- namespace_id = namespace_id_from(params)
- if Feature.enabled?(:paginatable_namespace_drop_down_for_project_creation, current_user, default_enabled: :yaml)
.js-vue-new-project-url-select{ data: { namespace_full_path: GroupFinder.new(current_user).execute(id: namespace_id)&.full_path, namespace_id: namespace_id, root_url: root_url, track_label: track_label } }
- else
.input-group-prepend.flex-shrink-0.has-tooltip{ title: root_url } .input-group-prepend.flex-shrink-0.has-tooltip{ title: root_url }
.input-group-text .input-group-text
= root_url = root_url
- namespace_id = namespace_id_from(params)
= f.select(:namespace_id, = f.select(:namespace_id,
namespaces_options_with_developer_maintainer_access(selected: namespace_id, namespaces_options_with_developer_maintainer_access(selected: namespace_id,
display_path: true, display_path: true,
extra_group: namespace_id), extra_group: namespace_id),
{}, {},
{ class: 'select2 js-select-namespace qa-project-namespace-select block-truncated', data: { track_label: "#{track_label}", track_event: "activate_form_input", track_property: "project_path", track_value: "", qa_selector: "select_namespace_dropdown" }}) { class: 'select2 js-select-namespace qa-project-namespace-select block-truncated', data: { track_label: "#{track_label}", track_event: "activate_form_input", track_property: "project_path", track_value: "", qa_selector: "select_namespace_dropdown" }})
- else - else
.input-group-prepend.static-namespace.flex-shrink-0.has-tooltip{ title: user_url(current_user.username) + '/' } .input-group-prepend.static-namespace.flex-shrink-0.has-tooltip{ title: user_url(current_user.username) + '/' }
.input-group-text.border-0 .input-group-text.border-0
......
...@@ -6,6 +6,7 @@ RSpec.describe 'New project', :js do ...@@ -6,6 +6,7 @@ RSpec.describe 'New project', :js do
let(:user) { create(:admin) } let(:user) { create(:admin) }
before do before do
stub_feature_flags(paginatable_namespace_drop_down_for_project_creation: false)
sign_in(user) sign_in(user)
end end
......
...@@ -10,6 +10,7 @@ RSpec.describe 'Import/Export - project import integration test', :js do ...@@ -10,6 +10,7 @@ RSpec.describe 'Import/Export - project import integration test', :js do
let(:export_path) { "#{Dir.tmpdir}/import_file_spec" } let(:export_path) { "#{Dir.tmpdir}/import_file_spec" }
before do before do
stub_feature_flags(paginatable_namespace_drop_down_for_project_creation: false)
stub_uploads_object_storage(FileUploader) stub_uploads_object_storage(FileUploader)
allow_next_instance_of(Gitlab::ImportExport) do |instance| allow_next_instance_of(Gitlab::ImportExport) do |instance|
allow(instance).to receive(:storage_path).and_return(export_path) allow(instance).to receive(:storage_path).and_return(export_path)
......
...@@ -6,6 +6,10 @@ RSpec.describe 'New project', :js do ...@@ -6,6 +6,10 @@ RSpec.describe 'New project', :js do
include Select2Helper include Select2Helper
include Spec::Support::Helpers::Features::TopNavSpecHelpers include Spec::Support::Helpers::Features::TopNavSpecHelpers
before do
stub_feature_flags(paginatable_namespace_drop_down_for_project_creation: false)
end
context 'as a user' do context 'as a user' do
let(:user) { create(:user) } let(:user) { create(:user) }
......
...@@ -6,6 +6,7 @@ RSpec.describe 'User creates a project', :js do ...@@ -6,6 +6,7 @@ RSpec.describe 'User creates a project', :js do
let(:user) { create(:user) } let(:user) { create(:user) }
before do before do
stub_feature_flags(paginatable_namespace_drop_down_for_project_creation: false)
sign_in(user) sign_in(user)
create(:personal_key, user: user) create(:personal_key, user: user)
end end
......
import { GlButton, GlDropdown, GlDropdownItem, GlDropdownSectionHeader } from '@gitlab/ui';
import { createLocalVue, mount, shallowMount } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import NewProjectUrlSelect from '~/pages/projects/new/components/new_project_url_select.vue';
import searchQuery from '~/pages/projects/new/queries/search_namespaces_where_user_can_create_projects.query.graphql';
describe('NewProjectUrlSelect component', () => {
let wrapper;
const data = {
currentUser: {
groups: {
nodes: [
{
id: 'gid://gitlab/Group/26',
fullPath: 'flightjs',
},
{
id: 'gid://gitlab/Group/28',
fullPath: 'h5bp',
},
],
},
namespace: {
id: 'gid://gitlab/Namespace/1',
fullPath: 'root',
},
},
};
const localVue = createLocalVue();
localVue.use(VueApollo);
const requestHandlers = [[searchQuery, jest.fn().mockResolvedValue({ data })]];
const apolloProvider = createMockApollo(requestHandlers);
const provide = {
namespaceFullPath: 'h5bp',
namespaceId: '28',
rootUrl: 'https://gitlab.com/',
trackLabel: 'blank_project',
};
const mountComponent = ({ mountFn = shallowMount } = {}) =>
mountFn(NewProjectUrlSelect, { localVue, apolloProvider, provide });
const findButtonLabel = () => wrapper.findComponent(GlButton);
const findDropdown = () => wrapper.findComponent(GlDropdown);
const findHiddenInput = () => wrapper.find('input');
afterEach(() => {
wrapper.destroy();
});
it('renders the root url as a label', () => {
wrapper = mountComponent();
expect(findButtonLabel().text()).toBe(provide.rootUrl);
expect(findButtonLabel().props('label')).toBe(true);
});
it('renders a dropdown with the initial namespace full path as the text', () => {
wrapper = mountComponent();
expect(findDropdown().props('text')).toBe(provide.namespaceFullPath);
});
it('renders a dropdown with the initial namespace id in the hidden input', () => {
wrapper = mountComponent();
expect(findHiddenInput().attributes('value')).toBe(provide.namespaceId);
});
it('renders expected dropdown items', async () => {
wrapper = mountComponent({ mountFn: mount });
jest.runOnlyPendingTimers();
await wrapper.vm.$nextTick();
const listItems = wrapper.findAll('li');
expect(listItems.at(0).findComponent(GlDropdownSectionHeader).text()).toBe('Groups');
expect(listItems.at(1).text()).toBe(data.currentUser.groups.nodes[0].fullPath);
expect(listItems.at(2).text()).toBe(data.currentUser.groups.nodes[1].fullPath);
expect(listItems.at(3).findComponent(GlDropdownSectionHeader).text()).toBe('Users');
expect(listItems.at(4).text()).toBe(data.currentUser.namespace.fullPath);
});
it('updates hidden input with selected namespace', async () => {
wrapper = mountComponent();
jest.runOnlyPendingTimers();
await wrapper.vm.$nextTick();
wrapper.findComponent(GlDropdownItem).vm.$emit('click');
await wrapper.vm.$nextTick();
expect(findHiddenInput().attributes()).toMatchObject({
name: 'project[namespace_id]',
value: getIdFromGraphQLId(data.currentUser.groups.nodes[0].id).toString(),
});
});
it('tracks clicking on the dropdown', () => {
wrapper = mountComponent();
const trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
findDropdown().vm.$emit('show');
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'activate_form_input', {
label: provide.trackLabel,
property: 'project_path',
});
unmockTracking();
});
});
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