Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
G
gitlab-ce
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
1
Merge Requests
1
Analytics
Analytics
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Commits
Issue Boards
Open sidebar
nexedi
gitlab-ce
Commits
714706cf
Commit
714706cf
authored
Dec 02, 2021
by
Coung Ngo
Committed by
Phil Hughes
Dec 02, 2021
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Delay query fetching for New Project page dropdown
parent
18a34cdb
Changes
5
Hide whitespace changes
Inline
Side-by-side
Showing
5 changed files
with
76 additions
and
46 deletions
+76
-46
app/assets/javascripts/lib/utils/url_utility.js
app/assets/javascripts/lib/utils/url_utility.js
+1
-1
app/assets/javascripts/projects/new/components/new_project_url_select.vue
...cripts/projects/new/components/new_project_url_select.vue
+17
-17
ee/app/assets/javascripts/projects/custom_project_templates.js
...p/assets/javascripts/projects/custom_project_templates.js
+2
-1
ee/app/views/users/_custom_project_templates_from_groups.html.haml
...ews/users/_custom_project_templates_from_groups.html.haml
+1
-1
spec/frontend/projects/new/components/new_project_url_select_spec.js
...nd/projects/new/components/new_project_url_select_spec.js
+55
-26
No files found.
app/assets/javascripts/lib/utils/url_utility.js
View file @
714706cf
export
const
DASH_SCOPE
=
'
-
'
;
export
const
DASH_SCOPE
=
'
-
'
;
const
PATH_SEPARATOR
=
'
/
'
;
export
const
PATH_SEPARATOR
=
'
/
'
;
const
PATH_SEPARATOR_LEADING_REGEX
=
new
RegExp
(
`^
${
PATH_SEPARATOR
}
+`
);
const
PATH_SEPARATOR_LEADING_REGEX
=
new
RegExp
(
`^
${
PATH_SEPARATOR
}
+`
);
const
PATH_SEPARATOR_ENDING_REGEX
=
new
RegExp
(
`
${
PATH_SEPARATOR
}
+$`
);
const
PATH_SEPARATOR_ENDING_REGEX
=
new
RegExp
(
`
${
PATH_SEPARATOR
}
+$`
);
const
SHA_REGEX
=
/
[\d
a-f
]{40}
/gi
;
const
SHA_REGEX
=
/
[\d
a-f
]{40}
/gi
;
...
...
app/assets/javascripts/projects/new/components/new_project_url_select.vue
View file @
714706cf
...
@@ -8,7 +8,7 @@ import {
...
@@ -8,7 +8,7 @@ import {
GlDropdownSectionHeader
,
GlDropdownSectionHeader
,
GlSearchBoxByType
,
GlSearchBoxByType
,
}
from
'
@gitlab/ui
'
;
}
from
'
@gitlab/ui
'
;
import
{
joinPaths
}
from
'
~/lib/utils/url_utility
'
;
import
{
joinPaths
,
PATH_SEPARATOR
}
from
'
~/lib/utils/url_utility
'
;
import
{
MINIMUM_SEARCH_LENGTH
}
from
'
~/graphql_shared/constants
'
;
import
{
MINIMUM_SEARCH_LENGTH
}
from
'
~/graphql_shared/constants
'
;
import
{
getIdFromGraphQLId
}
from
'
~/graphql_shared/utils
'
;
import
{
getIdFromGraphQLId
}
from
'
~/graphql_shared/utils
'
;
import
Tracking
from
'
~/tracking
'
;
import
Tracking
from
'
~/tracking
'
;
...
@@ -36,7 +36,9 @@ export default {
...
@@ -36,7 +36,9 @@ export default {
};
};
},
},
skip
()
{
skip
()
{
return
this
.
search
.
length
>
0
&&
this
.
search
.
length
<
MINIMUM_SEARCH_LENGTH
;
const
hasNotEnoughSearchCharacters
=
this
.
search
.
length
>
0
&&
this
.
search
.
length
<
MINIMUM_SEARCH_LENGTH
;
return
this
.
shouldSkipQuery
||
hasNotEnoughSearchCharacters
;
},
},
debounce
:
DEBOUNCE_DELAY
,
debounce
:
DEBOUNCE_DELAY
,
},
},
...
@@ -52,7 +54,7 @@ export default {
...
@@ -52,7 +54,7 @@ export default {
data
()
{
data
()
{
return
{
return
{
currentUser
:
{},
currentUser
:
{},
groupToFilterBy
:
undefined
,
group
Path
ToFilterBy
:
undefined
,
search
:
''
,
search
:
''
,
selectedNamespace
:
this
.
namespaceId
selectedNamespace
:
this
.
namespaceId
?
{
?
{
...
@@ -63,6 +65,7 @@ export default {
...
@@ -63,6 +65,7 @@ export default {
id
:
this
.
userNamespaceId
,
id
:
this
.
userNamespaceId
,
fullPath
:
this
.
userNamespaceFullPath
,
fullPath
:
this
.
userNamespaceFullPath
,
},
},
shouldSkipQuery
:
true
,
};
};
},
},
computed
:
{
computed
:
{
...
@@ -73,10 +76,8 @@ export default {
...
@@ -73,10 +76,8 @@ export default {
return
this
.
currentUser
.
namespace
||
{};
return
this
.
currentUser
.
namespace
||
{};
},
},
filteredGroups
()
{
filteredGroups
()
{
return
this
.
groupToFilterBy
return
this
.
groupPathToFilterBy
?
this
.
userGroups
.
filter
((
group
)
=>
?
this
.
userGroups
.
filter
((
group
)
=>
group
.
fullPath
.
startsWith
(
this
.
groupPathToFilterBy
))
group
.
fullPath
.
startsWith
(
this
.
groupToFilterBy
.
fullPath
),
)
:
this
.
userGroups
;
:
this
.
userGroups
;
},
},
hasGroupMatches
()
{
hasGroupMatches
()
{
...
@@ -85,7 +86,7 @@ export default {
...
@@ -85,7 +86,7 @@ export default {
hasNamespaceMatches
()
{
hasNamespaceMatches
()
{
return
(
return
(
this
.
userNamespace
.
fullPath
?.
toLowerCase
().
includes
(
this
.
search
.
toLowerCase
())
&&
this
.
userNamespace
.
fullPath
?.
toLowerCase
().
includes
(
this
.
search
.
toLowerCase
())
&&
!
this
.
groupToFilterBy
!
this
.
group
Path
ToFilterBy
);
);
},
},
hasNoMatches
()
{
hasNoMatches
()
{
...
@@ -99,7 +100,10 @@ export default {
...
@@ -99,7 +100,10 @@ export default {
eventHub
.
$off
(
'
select-template
'
,
this
.
handleSelectTemplate
);
eventHub
.
$off
(
'
select-template
'
,
this
.
handleSelectTemplate
);
},
},
methods
:
{
methods
:
{
focusInput
()
{
handleDropdownShown
()
{
if
(
this
.
shouldSkipQuery
)
{
this
.
shouldSkipQuery
=
false
;
}
this
.
$refs
.
search
.
focusInput
();
this
.
$refs
.
search
.
focusInput
();
},
},
handleDropdownItemClick
(
namespace
)
{
handleDropdownItemClick
(
namespace
)
{
...
@@ -111,13 +115,9 @@ export default {
...
@@ -111,13 +115,9 @@ export default {
});
});
this
.
setNamespace
(
namespace
);
this
.
setNamespace
(
namespace
);
},
},
handleSelectTemplate
(
groupId
)
{
handleSelectTemplate
(
id
,
fullPath
)
{
this
.
groupToFilterBy
=
this
.
userGroups
.
find
(
this
.
groupPathToFilterBy
=
fullPath
.
split
(
PATH_SEPARATOR
).
shift
();
(
group
)
=>
getIdFromGraphQLId
(
group
.
id
)
===
groupId
,
this
.
setNamespace
({
id
,
fullPath
});
);
if
(
this
.
groupToFilterBy
)
{
this
.
setNamespace
(
this
.
groupToFilterBy
);
}
},
},
setNamespace
({
id
,
fullPath
})
{
setNamespace
({
id
,
fullPath
})
{
this
.
selectedNamespace
=
{
this
.
selectedNamespace
=
{
...
@@ -137,7 +137,7 @@ export default {
...
@@ -137,7 +137,7 @@ export default {
toggle-class=
"gl-rounded-top-right-base! gl-rounded-bottom-right-base! gl-w-20"
toggle-class=
"gl-rounded-top-right-base! gl-rounded-bottom-right-base! gl-w-20"
data-qa-selector=
"select_namespace_dropdown"
data-qa-selector=
"select_namespace_dropdown"
@
show=
"track('activate_form_input',
{ label: trackLabel, property: 'project_path' })"
@
show=
"track('activate_form_input',
{ label: trackLabel, property: 'project_path' })"
@shown="
focusInput
"
@shown="
handleDropdownShown
"
>
>
<gl-search-box-by-type
<gl-search-box-by-type
ref=
"search"
ref=
"search"
...
...
ee/app/assets/javascripts/projects/custom_project_templates.js
View file @
714706cf
...
@@ -57,7 +57,8 @@ const bindEvents = () => {
...
@@ -57,7 +57,8 @@ const bindEvents = () => {
const templateName = $(this).data('template-name');
const templateName = $(this).data('template-name');
if (subgroupId) {
if (subgroupId) {
eventHub.$emit('select-template', groupId);
const subgroupFullPath = $(this).data('subgroup-full-path');
eventHub.$emit('select-template', subgroupId, subgroupFullPath);
$subgroupWithTemplatesIdInput.val(subgroupId);
$subgroupWithTemplatesIdInput.val(subgroupId);
$namespaceSelect.val(groupId).trigger('change');
$namespaceSelect.val(groupId).trigger('change');
...
...
ee/app/views/users/_custom_project_templates_from_groups.html.haml
View file @
714706cf
...
@@ -34,7 +34,7 @@
...
@@ -34,7 +34,7 @@
%a
.btn.gl-button.btn-default.gl-mr-3
{
href:
project_path
(
project
),
rel:
'noopener noreferrer'
,
target:
'_blank'
}
%a
.btn.gl-button.btn-default.gl-mr-3
{
href:
project_path
(
project
),
rel:
'noopener noreferrer'
,
target:
'_blank'
}
=
_
(
'Preview'
)
=
_
(
'Preview'
)
%label
.btn.gl-button.btn-success.custom-template-button.choose-template.gl-mb-0
{
for:
project
.
name
}
%label
.btn.gl-button.btn-success.custom-template-button.choose-template.gl-mb-0
{
for:
project
.
name
}
%input
{
type:
"radio"
,
autocomplete:
"off"
,
name:
"project[template_project_id]"
,
id:
project
.
name
,
value:
project
.
id
,
data:
{
subgroup_id:
project
.
namespace_id
,
template_name:
project
.
name
,
parent_group_id:
namespace_id
||
group
.
parent_id
}
}
%input
{
type:
"radio"
,
autocomplete:
"off"
,
name:
"project[template_project_id]"
,
id:
project
.
name
,
value:
project
.
id
,
data:
{
subgroup_
full_path:
project
.
namespace
.
full_path
,
subgroup_
id:
project
.
namespace_id
,
template_name:
project
.
name
,
parent_group_id:
namespace_id
||
group
.
parent_id
}
}
%span
.qa-use-template-button
%span
.qa-use-template-button
=
_
(
'Use template'
)
=
_
(
'Use template'
)
...
...
spec/frontend/projects/new/components/new_project_url_select_spec.js
View file @
714706cf
...
@@ -5,7 +5,8 @@ import {
...
@@ -5,7 +5,8 @@ import {
GlDropdownSectionHeader
,
GlDropdownSectionHeader
,
GlSearchBoxByType
,
GlSearchBoxByType
,
}
from
'
@gitlab/ui
'
;
}
from
'
@gitlab/ui
'
;
import
{
createLocalVue
,
mount
,
shallowMount
}
from
'
@vue/test-utils
'
;
import
{
mount
,
shallowMount
}
from
'
@vue/test-utils
'
;
import
Vue
from
'
vue
'
;
import
VueApollo
from
'
vue-apollo
'
;
import
VueApollo
from
'
vue-apollo
'
;
import
createMockApollo
from
'
helpers/mock_apollo_helper
'
;
import
createMockApollo
from
'
helpers/mock_apollo_helper
'
;
import
{
mockTracking
,
unmockTracking
}
from
'
helpers/tracking_helper
'
;
import
{
mockTracking
,
unmockTracking
}
from
'
helpers/tracking_helper
'
;
...
@@ -52,8 +53,7 @@ describe('NewProjectUrlSelect component', () => {
...
@@ -52,8 +53,7 @@ describe('NewProjectUrlSelect component', () => {
},
},
};
};
const
localVue
=
createLocalVue
();
Vue
.
use
(
VueApollo
);
localVue
.
use
(
VueApollo
);
const
defaultProvide
=
{
const
defaultProvide
=
{
namespaceFullPath
:
'
h5bp
'
,
namespaceFullPath
:
'
h5bp
'
,
...
@@ -64,17 +64,19 @@ describe('NewProjectUrlSelect component', () => {
...
@@ -64,17 +64,19 @@ describe('NewProjectUrlSelect component', () => {
userNamespaceId
:
'
1
'
,
userNamespaceId
:
'
1
'
,
};
};
let
mockQueryResponse
;
const
mountComponent
=
({
const
mountComponent
=
({
search
=
''
,
search
=
''
,
queryResponse
=
data
,
queryResponse
=
data
,
provide
=
defaultProvide
,
provide
=
defaultProvide
,
mountFn
=
shallowMount
,
mountFn
=
shallowMount
,
}
=
{})
=>
{
}
=
{})
=>
{
const
requestHandlers
=
[[
searchQuery
,
jest
.
fn
().
mockResolvedValue
({
data
:
queryResponse
})]];
mockQueryResponse
=
jest
.
fn
().
mockResolvedValue
({
data
:
queryResponse
});
const
requestHandlers
=
[[
searchQuery
,
mockQueryResponse
]];
const
apolloProvider
=
createMockApollo
(
requestHandlers
);
const
apolloProvider
=
createMockApollo
(
requestHandlers
);
return
mountFn
(
NewProjectUrlSelect
,
{
return
mountFn
(
NewProjectUrlSelect
,
{
localVue
,
apolloProvider
,
apolloProvider
,
provide
,
provide
,
data
()
{
data
()
{
...
@@ -88,12 +90,19 @@ describe('NewProjectUrlSelect component', () => {
...
@@ -88,12 +90,19 @@ describe('NewProjectUrlSelect component', () => {
const
findButtonLabel
=
()
=>
wrapper
.
findComponent
(
GlButton
);
const
findButtonLabel
=
()
=>
wrapper
.
findComponent
(
GlButton
);
const
findDropdown
=
()
=>
wrapper
.
findComponent
(
GlDropdown
);
const
findDropdown
=
()
=>
wrapper
.
findComponent
(
GlDropdown
);
const
findInput
=
()
=>
wrapper
.
findComponent
(
GlSearchBoxByType
);
const
findInput
=
()
=>
wrapper
.
findComponent
(
GlSearchBoxByType
);
const
findHiddenInput
=
()
=>
wrapper
.
find
(
'
input
'
);
const
findHiddenInput
=
()
=>
wrapper
.
find
(
'
[name="project[namespace_id]"]
'
);
const
clickDropdownItem
=
async
()
=>
{
const
clickDropdownItem
=
async
()
=>
{
wrapper
.
findComponent
(
GlDropdownItem
).
vm
.
$emit
(
'
click
'
);
wrapper
.
findComponent
(
GlDropdownItem
).
vm
.
$emit
(
'
click
'
);
await
wrapper
.
vm
.
$nextTick
();
await
wrapper
.
vm
.
$nextTick
();
};
};
const
showDropdown
=
async
()
=>
{
findDropdown
().
vm
.
$emit
(
'
shown
'
);
await
wrapper
.
vm
.
$apollo
.
queries
.
currentUser
.
refetch
();
jest
.
runOnlyPendingTimers
();
};
afterEach
(()
=>
{
afterEach
(()
=>
{
wrapper
.
destroy
();
wrapper
.
destroy
();
});
});
...
@@ -141,20 +150,18 @@ describe('NewProjectUrlSelect component', () => {
...
@@ -141,20 +150,18 @@ describe('NewProjectUrlSelect component', () => {
it
(
'
focuses on the input when the dropdown is opened
'
,
async
()
=>
{
it
(
'
focuses on the input when the dropdown is opened
'
,
async
()
=>
{
wrapper
=
mountComponent
({
mountFn
:
mount
});
wrapper
=
mountComponent
({
mountFn
:
mount
});
jest
.
runOnlyPendingTimers
();
await
wrapper
.
vm
.
$nextTick
();
const
spy
=
jest
.
spyOn
(
findInput
().
vm
,
'
focusInput
'
);
const
spy
=
jest
.
spyOn
(
findInput
().
vm
,
'
focusInput
'
);
findDropdown
().
vm
.
$emit
(
'
shown
'
);
await
showDropdown
(
);
expect
(
spy
).
toHaveBeenCalledTimes
(
1
);
expect
(
spy
).
toHaveBeenCalledTimes
(
1
);
});
});
it
(
'
renders expected dropdown items
'
,
async
()
=>
{
it
(
'
renders expected dropdown items
'
,
async
()
=>
{
wrapper
=
mountComponent
({
mountFn
:
mount
});
wrapper
=
mountComponent
({
mountFn
:
mount
});
jest
.
runOnlyPendingTimers
();
await
wrapper
.
vm
.
$nextTick
();
await
showDropdown
();
const
listItems
=
wrapper
.
findAll
(
'
li
'
);
const
listItems
=
wrapper
.
findAll
(
'
li
'
);
...
@@ -167,15 +174,36 @@ describe('NewProjectUrlSelect component', () => {
...
@@ -167,15 +174,36 @@ describe('NewProjectUrlSelect component', () => {
expect
(
listItems
.
at
(
5
).
text
()).
toBe
(
data
.
currentUser
.
namespace
.
fullPath
);
expect
(
listItems
.
at
(
5
).
text
()).
toBe
(
data
.
currentUser
.
namespace
.
fullPath
);
});
});
describe
(
'
query fetching
'
,
()
=>
{
describe
(
'
on component mount
'
,
()
=>
{
it
(
'
does not fetch query
'
,
()
=>
{
wrapper
=
mountComponent
({
mountFn
:
mount
});
expect
(
mockQueryResponse
).
not
.
toHaveBeenCalled
();
});
});
describe
(
'
on dropdown shown
'
,
()
=>
{
it
(
'
fetches query
'
,
async
()
=>
{
wrapper
=
mountComponent
({
mountFn
:
mount
});
await
showDropdown
();
expect
(
mockQueryResponse
).
toHaveBeenCalled
();
});
});
});
describe
(
'
when selecting from a group template
'
,
()
=>
{
describe
(
'
when selecting from a group template
'
,
()
=>
{
const
groupId
=
getIdFromGraphQLId
(
data
.
currentUser
.
groups
.
nodes
[
1
].
id
)
;
const
{
fullPath
,
id
}
=
data
.
currentUser
.
groups
.
nodes
[
1
]
;
beforeEach
(
async
()
=>
{
beforeEach
(
async
()
=>
{
wrapper
=
mountComponent
({
mountFn
:
mount
});
wrapper
=
mountComponent
({
mountFn
:
mount
});
jest
.
runOnlyPendingTimers
();
await
wrapper
.
vm
.
$nextTick
();
eventHub
.
$emit
(
'
select-template
'
,
groupId
);
// Show dropdown to fetch projects
await
showDropdown
();
eventHub
.
$emit
(
'
select-template
'
,
getIdFromGraphQLId
(
id
),
fullPath
);
});
});
it
(
'
filters the dropdown items to the selected group and children
'
,
async
()
=>
{
it
(
'
filters the dropdown items to the selected group and children
'
,
async
()
=>
{
...
@@ -188,7 +216,7 @@ describe('NewProjectUrlSelect component', () => {
...
@@ -188,7 +216,7 @@ describe('NewProjectUrlSelect component', () => {
});
});
it
(
'
sets the selection to the group
'
,
async
()
=>
{
it
(
'
sets the selection to the group
'
,
async
()
=>
{
expect
(
findDropdown
().
props
(
'
text
'
)).
toBe
(
data
.
currentUser
.
groups
.
nodes
[
1
].
fullPath
);
expect
(
findDropdown
().
props
(
'
text
'
)).
toBe
(
fullPath
);
});
});
});
});
...
@@ -214,12 +242,13 @@ describe('NewProjectUrlSelect component', () => {
...
@@ -214,12 +242,13 @@ describe('NewProjectUrlSelect component', () => {
});
});
it
(
'
emits `update-visibility` event to update the visibility radio options
'
,
async
()
=>
{
it
(
'
emits `update-visibility` event to update the visibility radio options
'
,
async
()
=>
{
wrapper
=
mountComponent
();
wrapper
=
mountComponent
({
mountFn
:
mount
});
jest
.
runOnlyPendingTimers
();
await
wrapper
.
vm
.
$nextTick
();
const
spy
=
jest
.
spyOn
(
eventHub
,
'
$emit
'
);
const
spy
=
jest
.
spyOn
(
eventHub
,
'
$emit
'
);
// Show dropdown to fetch projects
await
showDropdown
();
await
clickDropdownItem
();
await
clickDropdownItem
();
const
namespace
=
data
.
currentUser
.
groups
.
nodes
[
0
];
const
namespace
=
data
.
currentUser
.
groups
.
nodes
[
0
];
...
@@ -233,16 +262,16 @@ describe('NewProjectUrlSelect component', () => {
...
@@ -233,16 +262,16 @@ describe('NewProjectUrlSelect component', () => {
});
});
it
(
'
updates hidden input with selected namespace
'
,
async
()
=>
{
it
(
'
updates hidden input with selected namespace
'
,
async
()
=>
{
wrapper
=
mountComponent
();
wrapper
=
mountComponent
({
mountFn
:
mount
});
jest
.
runOnlyPendingTimers
();
await
wrapper
.
vm
.
$nextTick
();
// Show dropdown to fetch projects
await
showDropdown
();
await
clickDropdownItem
();
await
clickDropdownItem
();
expect
(
findHiddenInput
().
attributes
()).
toMatchObject
({
expect
(
findHiddenInput
().
attributes
(
'
value
'
)).
toBe
(
name
:
'
project[namespace_id]
'
,
getIdFromGraphQLId
(
data
.
currentUser
.
groups
.
nodes
[
0
].
id
).
toString
(),
value
:
getIdFromGraphQLId
(
data
.
currentUser
.
groups
.
nodes
[
0
].
id
).
toString
(),
);
});
});
});
it
(
'
tracks clicking on the dropdown
'
,
()
=>
{
it
(
'
tracks clicking on the dropdown
'
,
()
=>
{
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment