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
3dcaf9dc
Commit
3dcaf9dc
authored
Apr 12, 2021
by
Tom Quirk
Committed by
Miguel Rincon
Apr 12, 2021
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Add search functionality to Jira Connect App namespaces
parent
9b6dd658
Changes
6
Hide whitespace changes
Inline
Side-by-side
Showing
6 changed files
with
159 additions
and
60 deletions
+159
-60
app/assets/javascripts/jira_connect/api.js
app/assets/javascripts/jira_connect/api.js
+2
-1
app/assets/javascripts/jira_connect/components/groups_list.vue
...ssets/javascripts/jira_connect/components/groups_list.vue
+55
-41
app/assets/javascripts/jira_connect/components/groups_list_item.vue
.../javascripts/jira_connect/components/groups_list_item.vue
+10
-3
app/assets/stylesheets/page_bundles/jira_connect.scss
app/assets/stylesheets/page_bundles/jira_connect.scss
+1
-2
changelogs/unreleased/324263-add-a-way-to-filter-search-for-a-namespace.yml
...sed/324263-add-a-way-to-filter-search-for-a-namespace.yml
+5
-0
spec/frontend/jira_connect/components/groups_list_spec.js
spec/frontend/jira_connect/components/groups_list_spec.js
+86
-13
No files found.
app/assets/javascripts/jira_connect/api.js
View file @
3dcaf9dc
...
@@ -39,11 +39,12 @@ export const removeSubscription = async (removePath) => {
...
@@ -39,11 +39,12 @@ export const removeSubscription = async (removePath) => {
});
});
};
};
export
const
fetchGroups
=
async
(
groupsPath
,
{
page
,
perPage
})
=>
{
export
const
fetchGroups
=
async
(
groupsPath
,
{
page
,
perPage
,
search
})
=>
{
return
axios
.
get
(
groupsPath
,
{
return
axios
.
get
(
groupsPath
,
{
params
:
{
params
:
{
page
,
page
,
per_page
:
perPage
,
per_page
:
perPage
,
search
,
},
},
});
});
};
};
app/assets/javascripts/jira_connect/components/groups_list.vue
View file @
3dcaf9dc
<
script
>
<
script
>
import
{
Gl
Tabs
,
GlTab
,
GlLoadingIcon
,
GlPagination
,
GlAlert
}
from
'
@gitlab/ui
'
;
import
{
Gl
LoadingIcon
,
GlPagination
,
GlAlert
,
GlSearchBoxByType
}
from
'
@gitlab/ui
'
;
import
{
fetchGroups
}
from
'
~/jira_connect/api
'
;
import
{
fetchGroups
}
from
'
~/jira_connect/api
'
;
import
{
defaultPerPage
}
from
'
~/jira_connect/constants
'
;
import
{
defaultPerPage
}
from
'
~/jira_connect/constants
'
;
import
{
parseIntPagination
,
normalizeHeaders
}
from
'
~/lib/utils/common_utils
'
;
import
{
parseIntPagination
,
normalizeHeaders
}
from
'
~/lib/utils/common_utils
'
;
...
@@ -8,11 +8,10 @@ import GroupsListItem from './groups_list_item.vue';
...
@@ -8,11 +8,10 @@ import GroupsListItem from './groups_list_item.vue';
export
default
{
export
default
{
components
:
{
components
:
{
GlTabs
,
GlTab
,
GlLoadingIcon
,
GlLoadingIcon
,
GlPagination
,
GlPagination
,
GlAlert
,
GlAlert
,
GlSearchBoxByType
,
GroupsListItem
,
GroupsListItem
,
},
},
inject
:
{
inject
:
{
...
@@ -23,7 +22,8 @@ export default {
...
@@ -23,7 +22,8 @@ export default {
data
()
{
data
()
{
return
{
return
{
groups
:
[],
groups
:
[],
isLoading
:
false
,
isLoadingInitial
:
true
,
isLoadingMore
:
false
,
page
:
1
,
page
:
1
,
perPage
:
defaultPerPage
,
perPage
:
defaultPerPage
,
totalItems
:
0
,
totalItems
:
0
,
...
@@ -31,15 +31,18 @@ export default {
...
@@ -31,15 +31,18 @@ export default {
};
};
},
},
mounted
()
{
mounted
()
{
this
.
loadGroups
();
return
this
.
loadGroups
().
finally
(()
=>
{
this
.
isLoadingInitial
=
false
;
});
},
},
methods
:
{
methods
:
{
loadGroups
()
{
loadGroups
(
{
searchTerm
}
=
{}
)
{
this
.
isLoading
=
true
;
this
.
isLoading
More
=
true
;
fetchGroups
(
this
.
groupsPath
,
{
return
fetchGroups
(
this
.
groupsPath
,
{
page
:
this
.
page
,
page
:
this
.
page
,
perPage
:
this
.
perPage
,
perPage
:
this
.
perPage
,
search
:
searchTerm
,
})
})
.
then
((
response
)
=>
{
.
then
((
response
)
=>
{
const
{
page
,
total
}
=
parseIntPagination
(
normalizeHeaders
(
response
.
headers
));
const
{
page
,
total
}
=
parseIntPagination
(
normalizeHeaders
(
response
.
headers
));
...
@@ -51,50 +54,61 @@ export default {
...
@@ -51,50 +54,61 @@ export default {
this
.
errorMessage
=
s__
(
'
Integrations|Failed to load namespaces. Please try again.
'
);
this
.
errorMessage
=
s__
(
'
Integrations|Failed to load namespaces. Please try again.
'
);
})
})
.
finally
(()
=>
{
.
finally
(()
=>
{
this
.
isLoading
=
false
;
this
.
isLoading
More
=
false
;
});
});
},
},
onGroupSearch
(
searchTerm
)
{
return
this
.
loadGroups
({
searchTerm
});
},
},
},
};
};
</
script
>
</
script
>
<
template
>
<
template
>
<div>
<div>
<gl-alert
v-if=
"errorMessage"
class=
"gl-mb-
6
"
variant=
"danger"
@
dismiss=
"errorMessage = null"
>
<gl-alert
v-if=
"errorMessage"
class=
"gl-mb-
5
"
variant=
"danger"
@
dismiss=
"errorMessage = null"
>
{{
errorMessage
}}
{{
errorMessage
}}
</gl-alert>
</gl-alert>
<gl-tabs>
<gl-search-box-by-type
<gl-tab
:title=
"__('Groups and subgroups')"
class=
"gl-pt-3"
>
class=
"gl-mb-5"
<gl-loading-icon
v-if=
"isLoading"
size=
"md"
/>
debounce=
"500"
<div
v-else-if=
"groups.length === 0"
class=
"gl-text-center"
>
:placeholder=
"__('Search by name')"
<h5>
{{
s__
(
'
Integrations|No available namespaces.
'
)
}}
</h5>
:is-loading=
"isLoadingMore"
<p
class=
"gl-mt-5"
>
@
input=
"onGroupSearch"
{{
/>
s__
(
'
Integrations|You must have owner or maintainer permissions to link namespaces.
'
)
}}
<gl-loading-icon
v-if=
"isLoadingInitial"
size=
"md"
/>
</p>
<div
v-else-if=
"groups.length === 0"
class=
"gl-text-center"
>
</div>
<h5>
{{
s__
(
'
Integrations|No available namespaces.
'
)
}}
</h5>
<ul
v-else
class=
"gl-list-style-none gl-pl-0"
>
<p
class=
"gl-mt-5"
>
<groups-list-item
{{
s__
(
'
Integrations|You must have owner or maintainer permissions to link namespaces.
'
)
}}
v-for=
"group in groups"
</p>
:key=
"group.id"
</div>
:group=
"group"
<ul
@
error=
"errorMessage = $event"
v-else
/>
class=
"gl-list-style-none gl-pl-0 gl-border-t-1 gl-border-t-solid gl-border-t-gray-100"
</ul>
:class=
"
{ 'gl-opacity-5': isLoadingMore }"
data-testid="groups-list"
>
<groups-list-item
v-for=
"group in groups"
:key=
"group.id"
:group=
"group"
:disabled=
"isLoadingMore"
@
error=
"errorMessage = $event"
/>
</ul>
<div
class=
"gl-display-flex gl-justify-content-center gl-mt-5"
>
<div
class=
"gl-display-flex gl-justify-content-center gl-mt-5"
>
<gl-pagination
<gl-pagination
v-if=
"totalItems > perPage && groups.length > 0"
v-if=
"totalItems > perPage && groups.length > 0"
v-model=
"page"
v-model=
"page"
class=
"gl-mb-0"
class=
"gl-mb-0"
:per-page=
"perPage"
:per-page=
"perPage"
:total-items=
"totalItems"
:total-items=
"totalItems"
@
input=
"loadGroups"
@
input=
"loadGroups"
/>
/>
</div>
</div>
</gl-tab>
</gl-tabs>
</div>
</div>
</
template
>
</
template
>
app/assets/javascripts/jira_connect/components/groups_list_item.vue
View file @
3dcaf9dc
...
@@ -21,6 +21,11 @@ export default {
...
@@ -21,6 +21,11 @@ export default {
type
:
Object
,
type
:
Object
,
required
:
true
,
required
:
true
,
},
},
disabled
:
{
type
:
Boolean
,
required
:
false
,
default
:
false
,
},
},
},
data
()
{
data
()
{
return
{
return
{
...
@@ -60,7 +65,7 @@ export default {
...
@@ -60,7 +65,7 @@ export default {
</
script
>
</
script
>
<
template
>
<
template
>
<li
class=
"gl-border-b-1 gl-border-b-solid gl-border-b-gray-
2
00"
>
<li
class=
"gl-border-b-1 gl-border-b-solid gl-border-b-gray-
1
00"
>
<div
class=
"gl-display-flex gl-align-items-center gl-py-3"
>
<div
class=
"gl-display-flex gl-align-items-center gl-py-3"
>
<gl-icon
name=
"folder-o"
class=
"gl-mr-3"
/>
<gl-icon
name=
"folder-o"
class=
"gl-mr-3"
/>
<div
class=
"gl-display-none gl-flex-shrink-0 gl-sm-display-flex gl-mr-3"
>
<div
class=
"gl-display-none gl-flex-shrink-0 gl-sm-display-flex gl-mr-3"
>
...
@@ -83,11 +88,13 @@ export default {
...
@@ -83,11 +88,13 @@ export default {
<gl-button
<gl-button
category=
"secondary"
category=
"secondary"
variant=
"
success
"
variant=
"
confirm
"
:loading=
"isLoading"
:loading=
"isLoading"
:disabled=
"disabled"
@
click.prevent=
"onClick"
@
click.prevent=
"onClick"
>
{{
__
(
'
Link
'
)
}}
</gl-button
>
>
{{
__
(
'
Link
'
)
}}
</gl-button>
</div>
</div>
</div>
</div>
</li>
</li>
...
...
app/assets/stylesheets/page_bundles/jira_connect.scss
View file @
3dcaf9dc
...
@@ -4,7 +4,6 @@
...
@@ -4,7 +4,6 @@
@import
'bootstrap-vue/src/index'
;
@import
'bootstrap-vue/src/index'
;
@import
'@gitlab/ui/src/scss/utilities'
;
@import
'@gitlab/ui/src/scss/utilities'
;
@import
'@gitlab/ui/src/components/base/alert/alert'
;
// We should only import styles that we actually use.
// We should only import styles that we actually use.
@import
'@gitlab/ui/src/components/base/alert/alert'
;
@import
'@gitlab/ui/src/components/base/alert/alert'
;
...
@@ -16,8 +15,8 @@
...
@@ -16,8 +15,8 @@
@import
'@gitlab/ui/src/components/base/loading_icon/loading_icon'
;
@import
'@gitlab/ui/src/components/base/loading_icon/loading_icon'
;
@import
'@gitlab/ui/src/components/base/modal/modal'
;
@import
'@gitlab/ui/src/components/base/modal/modal'
;
@import
'@gitlab/ui/src/components/base/pagination/pagination'
;
@import
'@gitlab/ui/src/components/base/pagination/pagination'
;
@import
'@gitlab/ui/src/components/base/tabs/tabs/tabs'
;
@import
'@gitlab/ui/src/components/base/tooltip/tooltip'
;
@import
'@gitlab/ui/src/components/base/tooltip/tooltip'
;
@import
'@gitlab/ui/src/components/base/search_box_by_type/search_box_by_type'
;
$atlaskit-border-color
:
#dfe1e6
;
$atlaskit-border-color
:
#dfe1e6
;
$header-height
:
40px
;
$header-height
:
40px
;
...
...
changelogs/unreleased/324263-add-a-way-to-filter-search-for-a-namespace.yml
0 → 100644
View file @
3dcaf9dc
---
title
:
Add search functionality to Jira Connect App namespaces
merge_request
:
57669
author
:
type
:
added
spec/frontend/jira_connect/components/groups_list_spec.js
View file @
3dcaf9dc
import
{
GlAlert
,
GlLoadingIcon
}
from
'
@gitlab/ui
'
;
import
{
GlAlert
,
GlLoadingIcon
,
GlSearchBoxByType
}
from
'
@gitlab/ui
'
;
import
{
shallowMount
}
from
'
@vue/test-utils
'
;
import
{
shallowMount
}
from
'
@vue/test-utils
'
;
import
{
extendedWrapper
}
from
'
helpers/vue_test_utils_helper
'
;
import
waitForPromises
from
'
helpers/wait_for_promises
'
;
import
waitForPromises
from
'
helpers/wait_for_promises
'
;
import
{
fetchGroups
}
from
'
~/jira_connect/api
'
;
import
{
fetchGroups
}
from
'
~/jira_connect/api
'
;
import
GroupsList
from
'
~/jira_connect/components/groups_list.vue
'
;
import
GroupsList
from
'
~/jira_connect/components/groups_list.vue
'
;
import
GroupsListItem
from
'
~/jira_connect/components/groups_list_item.vue
'
;
import
GroupsListItem
from
'
~/jira_connect/components/groups_list_item.vue
'
;
...
@@ -12,20 +12,27 @@ jest.mock('~/jira_connect/api', () => {
...
@@ -12,20 +12,27 @@ jest.mock('~/jira_connect/api', () => {
fetchGroups
:
jest
.
fn
(),
fetchGroups
:
jest
.
fn
(),
};
};
});
});
const
mockGroupsPath
=
'
/groups
'
;
describe
(
'
GroupsList
'
,
()
=>
{
describe
(
'
GroupsList
'
,
()
=>
{
let
wrapper
;
let
wrapper
;
const
mockEmptyResponse
=
{
data
:
[]
};
const
mockEmptyResponse
=
{
data
:
[]
};
const
createComponent
=
(
options
=
{})
=>
{
const
createComponent
=
(
options
=
{})
=>
{
wrapper
=
shallowMount
(
GroupsList
,
{
wrapper
=
extendedWrapper
(
...
options
,
shallowMount
(
GroupsList
,
{
});
provide
:
{
groupsPath
:
mockGroupsPath
,
},
...
options
,
}),
);
};
};
afterEach
(()
=>
{
afterEach
(()
=>
{
wrapper
.
destroy
();
wrapper
.
destroy
();
wrapper
=
null
;
});
});
const
findGlAlert
=
()
=>
wrapper
.
find
(
GlAlert
);
const
findGlAlert
=
()
=>
wrapper
.
find
(
GlAlert
);
...
@@ -33,56 +40,72 @@ describe('GroupsList', () => {
...
@@ -33,56 +40,72 @@ describe('GroupsList', () => {
const
findAllItems
=
()
=>
wrapper
.
findAll
(
GroupsListItem
);
const
findAllItems
=
()
=>
wrapper
.
findAll
(
GroupsListItem
);
const
findFirstItem
=
()
=>
findAllItems
().
at
(
0
);
const
findFirstItem
=
()
=>
findAllItems
().
at
(
0
);
const
findSecondItem
=
()
=>
findAllItems
().
at
(
1
);
const
findSecondItem
=
()
=>
findAllItems
().
at
(
1
);
const
findSearchBox
=
()
=>
wrapper
.
find
(
GlSearchBoxByType
);
const
findGroupsList
=
()
=>
wrapper
.
findByTestId
(
'
groups-list
'
);
describe
(
'
isLoading is true
'
,
()
=>
{
describe
(
'
when groups are loading
'
,
()
=>
{
it
(
'
renders loading icon
'
,
async
()
=>
{
it
(
'
renders loading icon
'
,
async
()
=>
{
fetchGroups
.
mockRe
solvedValue
(
mockEmptyResponse
);
fetchGroups
.
mockRe
turnValue
(
new
Promise
(()
=>
{})
);
createComponent
();
createComponent
();
wrapper
.
setData
({
isLoading
:
true
});
await
wrapper
.
vm
.
$nextTick
();
await
wrapper
.
vm
.
$nextTick
();
expect
(
findGlLoadingIcon
().
exists
()).
toBe
(
true
);
expect
(
findGlLoadingIcon
().
exists
()).
toBe
(
true
);
});
});
});
});
describe
(
'
error fetching group
s
'
,
()
=>
{
describe
(
'
when groups fetch fail
s
'
,
()
=>
{
it
(
'
renders error message
'
,
async
()
=>
{
it
(
'
renders error message
'
,
async
()
=>
{
fetchGroups
.
mockRejectedValue
();
fetchGroups
.
mockRejectedValue
();
createComponent
();
createComponent
();
await
waitForPromises
();
await
waitForPromises
();
expect
(
findGlLoadingIcon
().
exists
()).
toBe
(
false
);
expect
(
findGlAlert
().
exists
()).
toBe
(
true
);
expect
(
findGlAlert
().
exists
()).
toBe
(
true
);
expect
(
findGlAlert
().
text
()).
toBe
(
'
Failed to load namespaces. Please try again.
'
);
expect
(
findGlAlert
().
text
()).
toBe
(
'
Failed to load namespaces. Please try again.
'
);
});
});
});
});
describe
(
'
no groups returned
'
,
()
=>
{
describe
(
'
with
no groups returned
'
,
()
=>
{
it
(
'
renders empty state
'
,
async
()
=>
{
it
(
'
renders empty state
'
,
async
()
=>
{
fetchGroups
.
mockResolvedValue
(
mockEmptyResponse
);
fetchGroups
.
mockResolvedValue
(
mockEmptyResponse
);
createComponent
();
createComponent
();
await
waitForPromises
();
await
waitForPromises
();
expect
(
findGlLoadingIcon
().
exists
()).
toBe
(
false
);
expect
(
wrapper
.
text
()).
toContain
(
'
No available namespaces
'
);
expect
(
wrapper
.
text
()).
toContain
(
'
No available namespaces
'
);
});
});
});
});
describe
(
'
with groups returned
'
,
()
=>
{
describe
(
'
with groups returned
'
,
()
=>
{
beforeEach
(
async
()
=>
{
beforeEach
(
async
()
=>
{
fetchGroups
.
mockResolvedValue
({
data
:
[
mockGroup1
,
mockGroup2
]
});
fetchGroups
.
mockResolvedValue
({
headers
:
{
'
X-PAGE
'
:
1
,
'
X-TOTAL
'
:
2
},
data
:
[
mockGroup1
,
mockGroup2
],
});
createComponent
();
createComponent
();
await
waitForPromises
();
await
waitForPromises
();
});
});
it
(
'
renders groups list
'
,
()
=>
{
it
(
'
renders groups list
'
,
()
=>
{
expect
(
findAllItems
()
.
length
).
toBe
(
2
);
expect
(
findAllItems
()
).
toHaveLength
(
2
);
expect
(
findFirstItem
().
props
(
'
group
'
)).
toBe
(
mockGroup1
);
expect
(
findFirstItem
().
props
(
'
group
'
)).
toBe
(
mockGroup1
);
expect
(
findSecondItem
().
props
(
'
group
'
)).
toBe
(
mockGroup2
);
expect
(
findSecondItem
().
props
(
'
group
'
)).
toBe
(
mockGroup2
);
});
});
it
(
'
sets GroupListItem `disabled` prop to `false`
'
,
()
=>
{
findAllItems
().
wrappers
.
forEach
((
groupListItem
)
=>
{
expect
(
groupListItem
.
props
(
'
disabled
'
)).
toBe
(
false
);
});
});
it
(
'
does not set opacity of the groups list
'
,
()
=>
{
expect
(
findGroupsList
().
classes
()).
not
.
toContain
(
'
gl-opacity-5
'
);
});
it
(
'
shows error message on $emit from item
'
,
async
()
=>
{
it
(
'
shows error message on $emit from item
'
,
async
()
=>
{
const
errorMessage
=
'
error message
'
;
const
errorMessage
=
'
error message
'
;
...
@@ -93,5 +116,55 @@ describe('GroupsList', () => {
...
@@ -93,5 +116,55 @@ describe('GroupsList', () => {
expect
(
findGlAlert
().
exists
()).
toBe
(
true
);
expect
(
findGlAlert
().
exists
()).
toBe
(
true
);
expect
(
findGlAlert
().
text
()).
toContain
(
errorMessage
);
expect
(
findGlAlert
().
text
()).
toContain
(
errorMessage
);
});
});
describe
(
'
when searching groups
'
,
()
=>
{
const
mockSearchTeam
=
'
mock search term
'
;
describe
(
'
while groups are loading
'
,
()
=>
{
beforeEach
(
async
()
=>
{
fetchGroups
.
mockClear
();
fetchGroups
.
mockReturnValue
(
new
Promise
(()
=>
{}));
findSearchBox
().
vm
.
$emit
(
'
input
'
,
mockSearchTeam
);
await
wrapper
.
vm
.
$nextTick
();
});
it
(
'
calls `fetchGroups` with search term
'
,
()
=>
{
expect
(
fetchGroups
).
toHaveBeenCalledWith
(
mockGroupsPath
,
{
page
:
1
,
perPage
:
10
,
search
:
mockSearchTeam
,
});
});
it
(
'
disables GroupListItems
'
,
async
()
=>
{
findAllItems
().
wrappers
.
forEach
((
groupListItem
)
=>
{
expect
(
groupListItem
.
props
(
'
disabled
'
)).
toBe
(
true
);
});
});
it
(
'
sets opacity of the groups list
'
,
()
=>
{
expect
(
findGroupsList
().
classes
()).
toContain
(
'
gl-opacity-5
'
);
});
it
(
'
sets loading prop of ths search box
'
,
()
=>
{
expect
(
findSearchBox
().
props
(
'
isLoading
'
)).
toBe
(
true
);
});
});
describe
(
'
when group search finishes loading
'
,
()
=>
{
beforeEach
(
async
()
=>
{
fetchGroups
.
mockResolvedValue
({
data
:
[
mockGroup1
]
});
findSearchBox
().
vm
.
$emit
(
'
input
'
);
await
waitForPromises
();
});
it
(
'
renders new groups list
'
,
()
=>
{
expect
(
findAllItems
()).
toHaveLength
(
1
);
expect
(
findFirstItem
().
props
(
'
group
'
)).
toBe
(
mockGroup1
);
});
});
});
});
});
});
});
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