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
7523392c
Commit
7523392c
authored
Aug 12, 2021
by
Natalia Tepluhina
Committed by
Vitaly Slobodin
Aug 12, 2021
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Refactor DropdownContentsEditView component to use GraphQL + Apollo
parent
8df6be82
Changes
16
Show whitespace changes
Inline
Side-by-side
Showing
16 changed files
with
407 additions
and
661 deletions
+407
-661
app/assets/javascripts/sidebar/components/labels/sidebar_labels.vue
.../javascripts/sidebar/components/labels/sidebar_labels.vue
+8
-7
app/assets/javascripts/sidebar/mount_sidebar.js
app/assets/javascripts/sidebar/mount_sidebar.js
+2
-0
app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue
...onents/sidebar/labels_select_widget/dropdown_contents.vue
+32
-2
app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue
...ar/labels_select_widget/dropdown_contents_labels_view.vue
+160
-100
app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/project_labels.query.graphql
...labels_select_widget/graphql/project_labels.query.graphql
+12
-0
app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue
...nents/sidebar/labels_select_widget/labels_select_root.vue
+15
-17
app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/store/actions.js
.../components/sidebar/labels_select_widget/store/actions.js
+0
-22
app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/store/mutation_types.js
...ents/sidebar/labels_select_widget/store/mutation_types.js
+0
-8
app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/store/mutations.js
...omponents/sidebar/labels_select_widget/store/mutations.js
+0
-21
spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view_spec.js
...abels_select_widget/dropdown_contents_create_view_spec.js
+2
-2
spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view_spec.js
...abels_select_widget/dropdown_contents_labels_view_spec.js
+145
-289
spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_spec.js
...ts/sidebar/labels_select_widget/dropdown_contents_spec.js
+6
-1
spec/frontend/vue_shared/components/sidebar/labels_select_widget/labels_select_root_spec.js
...s/sidebar/labels_select_widget/labels_select_root_spec.js
+0
-52
spec/frontend/vue_shared/components/sidebar/labels_select_widget/mock_data.js
...ared/components/sidebar/labels_select_widget/mock_data.js
+25
-0
spec/frontend/vue_shared/components/sidebar/labels_select_widget/store/actions_spec.js
...onents/sidebar/labels_select_widget/store/actions_spec.js
+0
-88
spec/frontend/vue_shared/components/sidebar/labels_select_widget/store/mutations_spec.js
...ents/sidebar/labels_select_widget/store/mutations_spec.js
+0
-52
No files found.
app/assets/javascripts/sidebar/components/labels/sidebar_labels.vue
View file @
7523392c
...
@@ -55,12 +55,13 @@ export default {
...
@@ -55,12 +55,13 @@ export default {
},
},
getUpdateVariables
(
dropdownLabels
)
{
getUpdateVariables
(
dropdownLabels
)
{
const
currentLabelIds
=
this
.
selectedLabels
.
map
((
label
)
=>
label
.
id
);
const
currentLabelIds
=
this
.
selectedLabels
.
map
((
label
)
=>
label
.
id
);
const
userAddedLabelIds
=
dropdownLabels
const
dropdownLabelIds
=
dropdownLabels
.
map
((
label
)
=>
label
.
id
);
.
filter
((
label
)
=>
label
.
set
)
const
userAddedLabelIds
=
this
.
glFeatures
.
labelsWidget
.
map
((
label
)
=>
label
.
id
);
?
difference
(
dropdownLabelIds
,
currentLabelIds
)
const
userRemovedLabelIds
=
dropdownLabels
:
dropdownLabels
.
filter
((
label
)
=>
label
.
set
).
map
((
label
)
=>
label
.
id
);
.
filter
((
label
)
=>
!
label
.
set
)
const
userRemovedLabelIds
=
this
.
glFeatures
.
labelsWidget
.
map
((
label
)
=>
label
.
id
);
?
difference
(
currentLabelIds
,
dropdownLabelIds
)
:
dropdownLabels
.
filter
((
label
)
=>
!
label
.
set
).
map
((
label
)
=>
label
.
id
);
const
labelIds
=
difference
(
union
(
currentLabelIds
,
userAddedLabelIds
),
userRemovedLabelIds
);
const
labelIds
=
difference
(
union
(
currentLabelIds
,
userAddedLabelIds
),
userRemovedLabelIds
);
...
@@ -155,7 +156,7 @@ export default {
...
@@ -155,7 +156,7 @@ export default {
:labels-manage-path=
"labelsManagePath"
:labels-manage-path=
"labelsManagePath"
:labels-select-in-progress=
"isLabelsSelectInProgress"
:labels-select-in-progress=
"isLabelsSelectInProgress"
:selected-labels=
"selectedLabels"
:selected-labels=
"selectedLabels"
:variant=
"$options.
sidebar
"
:variant=
"$options.
variant
"
data-qa-selector=
"labels_block"
data-qa-selector=
"labels_block"
@
onDropdownClose=
"handleDropdownClose"
@
onDropdownClose=
"handleDropdownClose"
@
onLabelRemove=
"handleLabelRemove"
@
onLabelRemove=
"handleLabelRemove"
...
...
app/assets/javascripts/sidebar/mount_sidebar.js
View file @
7523392c
...
@@ -24,6 +24,7 @@ import SidebarDropdownWidget from '~/sidebar/components/sidebar_dropdown_widget.
...
@@ -24,6 +24,7 @@ import SidebarDropdownWidget from '~/sidebar/components/sidebar_dropdown_widget.
import
SidebarTodoWidget
from
'
~/sidebar/components/todo_toggle/sidebar_todo_widget.vue
'
;
import
SidebarTodoWidget
from
'
~/sidebar/components/todo_toggle/sidebar_todo_widget.vue
'
;
import
{
apolloProvider
}
from
'
~/sidebar/graphql
'
;
import
{
apolloProvider
}
from
'
~/sidebar/graphql
'
;
import
trackShowInviteMemberLink
from
'
~/sidebar/track_invite_members
'
;
import
trackShowInviteMemberLink
from
'
~/sidebar/track_invite_members
'
;
import
{
DropdownVariant
}
from
'
~/vue_shared/components/sidebar/labels_select_vue/constants
'
;
import
Translate
from
'
../vue_shared/translate
'
;
import
Translate
from
'
../vue_shared/translate
'
;
import
SidebarAssignees
from
'
./components/assignees/sidebar_assignees.vue
'
;
import
SidebarAssignees
from
'
./components/assignees/sidebar_assignees.vue
'
;
import
CopyEmailToClipboard
from
'
./components/copy_email_to_clipboard.vue
'
;
import
CopyEmailToClipboard
from
'
./components/copy_email_to_clipboard.vue
'
;
...
@@ -256,6 +257,7 @@ export function mountSidebarLabels() {
...
@@ -256,6 +257,7 @@ export function mountSidebarLabels() {
allowLabelEdit
:
parseBoolean
(
el
.
dataset
.
canEdit
),
allowLabelEdit
:
parseBoolean
(
el
.
dataset
.
canEdit
),
allowScopedLabels
:
parseBoolean
(
el
.
dataset
.
allowScopedLabels
),
allowScopedLabels
:
parseBoolean
(
el
.
dataset
.
allowScopedLabels
),
initiallySelectedLabels
:
JSON
.
parse
(
el
.
dataset
.
selectedLabels
),
initiallySelectedLabels
:
JSON
.
parse
(
el
.
dataset
.
selectedLabels
),
variant
:
DropdownVariant
.
Sidebar
,
},
},
render
:
(
createElement
)
=>
createElement
(
SidebarLabels
),
render
:
(
createElement
)
=>
createElement
(
SidebarLabels
),
});
});
...
...
app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue
View file @
7523392c
...
@@ -21,9 +21,29 @@ export default {
...
@@ -21,9 +21,29 @@ export default {
type
:
String
,
type
:
String
,
required
:
true
,
required
:
true
,
},
},
selectedLabels
:
{
type
:
Array
,
required
:
true
,
},
allowMultiselect
:
{
type
:
Boolean
,
required
:
true
,
},
labelsListTitle
:
{
type
:
String
,
required
:
true
,
},
footerCreateLabelTitle
:
{
type
:
String
,
required
:
true
,
},
footerManageLabelTitle
:
{
type
:
String
,
required
:
true
,
},
},
},
computed
:
{
computed
:
{
...
mapState
([
'
showDropdownContentsCreateView
'
,
'
labelsListTitle
'
]),
...
mapState
([
'
showDropdownContentsCreateView
'
]),
...
mapGetters
([
'
isDropdownVariantSidebar
'
,
'
isDropdownVariantEmbedded
'
]),
...
mapGetters
([
'
isDropdownVariantSidebar
'
,
'
isDropdownVariantEmbedded
'
]),
dropdownContentsView
()
{
dropdownContentsView
()
{
if
(
this
.
showDropdownContentsCreateView
)
{
if
(
this
.
showDropdownContentsCreateView
)
{
...
@@ -75,6 +95,16 @@ export default {
...
@@ -75,6 +95,16 @@ export default {
@
click=
"toggleDropdownContents"
@
click=
"toggleDropdownContents"
/>
/>
</div>
</div>
<component
:is=
"dropdownContentsView"
@
hideCreateView=
"toggleDropdownContentsCreateView"
/>
<component
:is=
"dropdownContentsView"
:selected-labels=
"selectedLabels"
:allow-multiselect=
"allowMultiselect"
:labels-list-title=
"labelsListTitle"
:footer-create-label-title=
"footerCreateLabelTitle"
:footer-manage-label-title=
"footerManageLabelTitle"
@
hideCreateView=
"toggleDropdownContentsCreateView"
@
closeDropdown=
"$emit('closeDropdown', $event)"
@
toggleDropdownContentsCreateView=
"toggleDropdownContentsCreateView"
/>
</div>
</div>
</
template
>
</
template
>
app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue
View file @
7523392c
<
script
>
<
script
>
import
{
Gl
IntersectionObserver
,
Gl
LoadingIcon
,
GlSearchBoxByType
,
GlLink
}
from
'
@gitlab/ui
'
;
import
{
GlLoadingIcon
,
GlSearchBoxByType
,
GlLink
}
from
'
@gitlab/ui
'
;
import
fuzzaldrinPlus
from
'
fuzzaldrin-plus
'
;
import
fuzzaldrinPlus
from
'
fuzzaldrin-plus
'
;
import
{
mapState
,
mapGetters
,
mapActions
}
from
'
vuex
'
;
import
{
debounce
}
from
'
lodash
'
;
import
createFlash
from
'
~/flash
'
;
import
{
getIdFromGraphQLId
}
from
'
~/graphql_shared/utils
'
;
import
{
DEFAULT_DEBOUNCE_AND_THROTTLE_MS
}
from
'
~/lib/utils/constants
'
;
import
{
UP_KEY_CODE
,
DOWN_KEY_CODE
,
ENTER_KEY_CODE
,
ESC_KEY_CODE
}
from
'
~/lib/utils/keycodes
'
;
import
{
UP_KEY_CODE
,
DOWN_KEY_CODE
,
ENTER_KEY_CODE
,
ESC_KEY_CODE
}
from
'
~/lib/utils/keycodes
'
;
import
{
__
}
from
'
~/locale
'
;
import
{
DropdownVariant
}
from
'
./constants
'
;
import
projectLabelsQuery
from
'
./graphql/project_labels.query.graphql
'
;
import
LabelItem
from
'
./label_item.vue
'
;
import
LabelItem
from
'
./label_item.vue
'
;
export
default
{
export
default
{
components
:
{
components
:
{
GlIntersectionObserver
,
GlLoadingIcon
,
GlLoadingIcon
,
GlSearchBoxByType
,
GlSearchBoxByType
,
GlLink
,
GlLink
,
LabelItem
,
LabelItem
,
},
},
inject
:
[
'
projectPath
'
,
'
allowLabelCreate
'
,
'
labelsManagePath
'
,
'
variant
'
],
props
:
{
selectedLabels
:
{
type
:
Array
,
required
:
true
,
},
allowMultiselect
:
{
type
:
Boolean
,
required
:
true
,
},
labelsListTitle
:
{
type
:
String
,
required
:
true
,
},
footerCreateLabelTitle
:
{
type
:
String
,
required
:
true
,
},
footerManageLabelTitle
:
{
type
:
String
,
required
:
true
,
},
},
data
()
{
data
()
{
return
{
return
{
searchKey
:
''
,
searchKey
:
''
,
labels
:
[],
currentHighlightItem
:
-
1
,
currentHighlightItem
:
-
1
,
localSelectedLabels
:
[...
this
.
selectedLabels
],
};
},
apollo
:
{
labels
:
{
query
:
projectLabelsQuery
,
variables
()
{
return
{
fullPath
:
this
.
projectPath
,
searchTerm
:
this
.
searchKey
,
};
};
},
},
skip
()
{
return
this
.
searchKey
.
length
===
1
;
},
update
:
(
data
)
=>
data
.
workspace
?.
labels
?.
nodes
||
[],
async
result
()
{
if
(
this
.
$refs
.
searchInput
)
{
await
this
.
$nextTick
();
this
.
$refs
.
searchInput
.
focusInput
();
}
},
error
()
{
createFlash
({
message
:
__
(
'
Error fetching labels.
'
)
});
},
},
},
computed
:
{
computed
:
{
...
mapState
([
isDropdownVariantSidebar
()
{
'
allowLabelCreate
'
,
return
this
.
variant
===
DropdownVariant
.
Sidebar
;
'
allowMultiselect
'
,
},
'
labelsManagePath
'
,
isDropdownVariantEmbedded
()
{
'
labels
'
,
return
this
.
variant
===
DropdownVariant
.
Embedded
;
'
labelsFetchInProgress
'
,
},
'
labelsListTitle
'
,
labelsFetchInProgress
()
{
'
footerCreateLabelTitle
'
,
return
this
.
$apollo
.
queries
.
labels
.
loading
;
'
footerManageLabelTitle
'
,
},
]),
localSelectedLabelsIds
()
{
...
mapGetters
([
'
selectedLabelsList
'
,
'
isDropdownVariantSidebar
'
,
'
isDropdownVariantEmbedded
'
]),
return
this
.
localSelectedLabels
.
map
((
label
)
=>
label
.
id
);
},
visibleLabels
()
{
visibleLabels
()
{
if
(
this
.
searchKey
)
{
if
(
this
.
searchKey
)
{
return
fuzzaldrinPlus
.
filter
(
this
.
labels
,
this
.
searchKey
,
{
return
fuzzaldrinPlus
.
filter
(
this
.
labels
,
this
.
searchKey
,
{
...
@@ -55,17 +108,16 @@ export default {
...
@@ -55,17 +108,16 @@ export default {
}
}
},
},
},
},
created
()
{
this
.
debouncedSearchKeyUpdate
=
debounce
(
this
.
setSearchKey
,
DEFAULT_DEBOUNCE_AND_THROTTLE_MS
);
},
beforeDestroy
()
{
this
.
$emit
(
'
closeDropdown
'
,
this
.
localSelectedLabels
);
this
.
debouncedSearchKeyUpdate
.
cancel
();
},
methods
:
{
methods
:
{
...
mapActions
([
'
toggleDropdownContents
'
,
'
toggleDropdownContentsCreateView
'
,
'
fetchLabels
'
,
'
receiveLabelsSuccess
'
,
'
updateSelectedLabels
'
,
'
toggleDropdownContents
'
,
]),
isLabelSelected
(
label
)
{
isLabelSelected
(
label
)
{
return
this
.
selectedLabelsList
.
includes
(
label
.
id
);
return
this
.
localSelectedLabelsIds
.
includes
(
getIdFromGraphQLId
(
label
.
id
)
);
},
},
/**
/**
* This method scrolls item from dropdown into
* This method scrolls item from dropdown into
...
@@ -86,23 +138,17 @@ export default {
...
@@ -86,23 +138,17 @@ export default {
}
}
}
}
},
},
handleComponentAppear
()
{
updateSelectedLabels
(
label
)
{
// We can avoid putting `catch` block here
if
(
this
.
isLabelSelected
(
label
))
{
// as failure is handled within actions.js already.
this
.
localSelectedLabels
=
this
.
localSelectedLabels
.
filter
(
return
this
.
fetchLabels
().
then
(()
=>
{
({
id
})
=>
id
!==
getIdFromGraphQLId
(
label
.
id
),
this
.
$refs
.
searchInput
.
focusInput
();
);
}
else
{
this
.
localSelectedLabels
.
push
({
...
label
,
id
:
getIdFromGraphQLId
(
label
.
id
),
});
});
},
}
/**
* We want to remove loaded labels to ensure component
* fetches fresh set of labels every time when shown.
*/
handleComponentDisappear
()
{
this
.
receiveLabelsSuccess
([]);
},
handleCreateLabelClick
()
{
this
.
receiveLabelsSuccess
([]);
this
.
toggleDropdownContentsCreateView
();
},
},
/**
/**
* This method enables keyboard navigation support for
* This method enables keyboard navigation support for
...
@@ -117,10 +163,10 @@ export default {
...
@@ -117,10 +163,10 @@ export default {
)
{
)
{
this
.
currentHighlightItem
+=
1
;
this
.
currentHighlightItem
+=
1
;
}
else
if
(
e
.
keyCode
===
ENTER_KEY_CODE
&&
this
.
currentHighlightItem
>
-
1
)
{
}
else
if
(
e
.
keyCode
===
ENTER_KEY_CODE
&&
this
.
currentHighlightItem
>
-
1
)
{
this
.
updateSelectedLabels
(
[
this
.
visibleLabels
[
this
.
currentHighlightItem
]
]);
this
.
updateSelectedLabels
(
this
.
visibleLabels
[
this
.
currentHighlightItem
]);
this
.
searchKey
=
''
;
this
.
searchKey
=
''
;
}
else
if
(
e
.
keyCode
===
ESC_KEY_CODE
)
{
}
else
if
(
e
.
keyCode
===
ESC_KEY_CODE
)
{
this
.
toggleDropdownContents
(
);
this
.
$emit
(
'
closeDropdown
'
,
this
.
localSelectedLabels
);
}
}
if
(
e
.
keyCode
!==
ESC_KEY_CODE
)
{
if
(
e
.
keyCode
!==
ESC_KEY_CODE
)
{
...
@@ -132,40 +178,54 @@ export default {
...
@@ -132,40 +178,54 @@ export default {
}
}
},
},
handleLabelClick
(
label
)
{
handleLabelClick
(
label
)
{
this
.
updateSelectedLabels
([
label
]);
this
.
updateSelectedLabels
(
label
);
if
(
!
this
.
allowMultiselect
)
this
.
toggleDropdownContents
();
if
(
!
this
.
allowMultiselect
)
{
this
.
$emit
(
'
closeDropdown
'
,
this
.
localSelectedLabels
);
}
},
setSearchKey
(
value
)
{
this
.
searchKey
=
value
;
},
},
},
},
};
};
</
script
>
</
script
>
<
template
>
<
template
>
<gl-intersection-observer
@
appear=
"handleComponentAppear"
@
disappear=
"handleComponentDisappear"
>
<div
<div
class=
"labels-select-contents-list js-labels-list"
@
keydown=
"handleKeyDown"
>
class=
"labels-select-contents-list js-labels-list"
data-testid=
"dropdown-wrapper"
@
keydown=
"handleKeyDown"
>
<div
class=
"dropdown-input"
@
click.stop=
"() =>
{}">
<div
class=
"dropdown-input"
@
click.stop=
"() =>
{}">
<gl-search-box-by-type
<gl-search-box-by-type
ref=
"searchInput"
ref=
"searchInput"
v-model
=
"searchKey"
:value
=
"searchKey"
:disabled=
"labelsFetchInProgress"
:disabled=
"labelsFetchInProgress"
data-qa-selector=
"dropdown_input_field"
data-qa-selector=
"dropdown_input_field"
data-testid=
"dropdown-input-field"
@
input=
"debouncedSearchKeyUpdate"
/>
/>
</div>
</div>
<div
ref=
"labelsListContainer"
class=
"dropdown-content"
data-testid=
"dropdown-content"
>
<div
ref=
"labelsListContainer"
class=
"dropdown-content"
data-testid=
"dropdown-content"
>
<gl-loading-icon
<gl-loading-icon
v-if=
"labelsFetchInProgress"
v-if=
"labelsFetchInProgress"
class=
"labels-fetch-loading gl-align-items-center w-100 h-100
"
class=
"labels-fetch-loading gl-align-items-center gl-w-full gl-h-full
"
size=
"md"
size=
"md"
/>
/>
<ul
v-else
class=
"list-unstyled gl-mb-0 gl-word-break-word
"
>
<ul
v-else
class=
"list-unstyled gl-mb-0 gl-word-break-word"
data-testid=
"labels-list
"
>
<label-item
<label-item
v-for=
"(label, index) in visibleLabels"
v-for=
"(label, index) in visibleLabels"
:key=
"label.id"
:key=
"label.id"
:label=
"label"
:label=
"label"
:is-label-set=
"label.set
"
:is-label-set=
"isLabelSelected(label)
"
:highlight=
"index === currentHighlightItem"
:highlight=
"index === currentHighlightItem"
@
clickLabel=
"handleLabelClick(label)"
@
clickLabel=
"handleLabelClick(label)"
/>
/>
<li
v-show=
"showNoMatchingResultsMessage"
class=
"gl-p-3 gl-text-center"
>
<li
v-show=
"showNoMatchingResultsMessage"
class=
"gl-p-3 gl-text-center"
data-testid=
"no-results"
>
{{
__
(
'
No matching results
'
)
}}
{{
__
(
'
No matching results
'
)
}}
</li>
</li>
</ul>
</ul>
...
@@ -178,8 +238,9 @@ export default {
...
@@ -178,8 +238,9 @@ export default {
<ul
class=
"list-unstyled"
>
<ul
class=
"list-unstyled"
>
<li
v-if=
"allowLabelCreate"
>
<li
v-if=
"allowLabelCreate"
>
<gl-link
<gl-link
class=
"gl-display-flex w-100 flex-row text-break-word label-item"
class=
"gl-display-flex gl-flex-direction-row gl-w-full gl-overflow-break-word label-item"
@
click=
"handleCreateLabelClick"
data-testid=
"create-label-button"
@
click=
"$emit('toggleDropdownContentsCreateView')"
>
>
{{
footerCreateLabelTitle
}}
{{
footerCreateLabelTitle
}}
</gl-link>
</gl-link>
...
@@ -187,7 +248,7 @@ export default {
...
@@ -187,7 +248,7 @@ export default {
<li>
<li>
<gl-link
<gl-link
:href=
"labelsManagePath"
:href=
"labelsManagePath"
class=
"gl-display-flex flex-row text
-break-word label-item"
class=
"gl-display-flex gl-flex-direction-row gl-w-full gl-overflow
-break-word label-item"
>
>
{{
footerManageLabelTitle
}}
{{
footerManageLabelTitle
}}
</gl-link>
</gl-link>
...
@@ -195,5 +256,4 @@ export default {
...
@@ -195,5 +256,4 @@ export default {
</ul>
</ul>
</div>
</div>
</div>
</div>
</gl-intersection-observer>
</
template
>
</
template
>
app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/project_labels.query.graphql
0 → 100644
View file @
7523392c
query
projectLabels
(
$fullPath
:
ID
!,
$searchTerm
:
String
)
{
workspace
:
project
(
fullPath
:
$fullPath
)
{
labels
(
searchTerm
:
$searchTerm
,
includeAncestorGroups
:
true
)
{
nodes
{
id
title
color
description
}
}
}
}
app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue
View file @
7523392c
...
@@ -196,23 +196,6 @@ export default {
...
@@ -196,23 +196,6 @@ export default {
},
},
methods
:
{
methods
:
{
...
mapActions
([
'
setInitialState
'
,
'
toggleDropdownContents
'
]),
...
mapActions
([
'
setInitialState
'
,
'
toggleDropdownContents
'
]),
/**
* This method differentiates between
* dispatched actions and calls necessary method.
*/
handleVuexActionDispatch
(
action
,
state
)
{
if
(
action
.
type
===
'
toggleDropdownContents
'
&&
!
state
.
showDropdownButton
&&
!
state
.
showDropdownContents
)
{
let
filterFn
=
(
label
)
=>
label
.
touched
;
if
(
this
.
isDropdownVariantEmbedded
)
{
filterFn
=
(
label
)
=>
label
.
set
;
}
this
.
handleDropdownClose
(
state
.
labels
.
filter
(
filterFn
));
}
},
/**
/**
* This method stores a mousedown event's target.
* This method stores a mousedown event's target.
* Required by the click listener because the click
* Required by the click listener because the click
...
@@ -276,6 +259,9 @@ export default {
...
@@ -276,6 +259,9 @@ export default {
handleDropdownClose
(
labels
)
{
handleDropdownClose
(
labels
)
{
// Only emit label updates if there are any labels to update
// Only emit label updates if there are any labels to update
// on UI.
// on UI.
if
(
this
.
showDropdownContents
)
{
this
.
toggleDropdownContents
();
}
if
(
labels
.
length
)
this
.
$emit
(
'
updateSelectedLabels
'
,
labels
);
if
(
labels
.
length
)
this
.
$emit
(
'
updateSelectedLabels
'
,
labels
);
this
.
$emit
(
'
onDropdownClose
'
);
this
.
$emit
(
'
onDropdownClose
'
);
},
},
...
@@ -332,8 +318,14 @@ export default {
...
@@ -332,8 +318,14 @@ export default {
<dropdown-contents
<dropdown-contents
v-if=
"dropdownButtonVisible && showDropdownContents"
v-if=
"dropdownButtonVisible && showDropdownContents"
ref=
"dropdownContents"
ref=
"dropdownContents"
:allow-multiselect=
"allowMultiselect"
:labels-list-title=
"labelsListTitle"
:footer-create-label-title=
"footerCreateLabelTitle"
:footer-manage-label-title=
"footerManageLabelTitle"
:render-on-top=
"!contentIsOnViewport"
:render-on-top=
"!contentIsOnViewport"
:labels-create-title=
"labelsCreateTitle"
:labels-create-title=
"labelsCreateTitle"
:selected-labels=
"selectedLabels"
@
closeDropdown=
"handleDropdownClose"
/>
/>
</
template
>
</
template
>
<
template
v-if=
"isDropdownVariantStandalone || isDropdownVariantEmbedded"
>
<
template
v-if=
"isDropdownVariantStandalone || isDropdownVariantEmbedded"
>
...
@@ -341,7 +333,13 @@ export default {
...
@@ -341,7 +333,13 @@ export default {
<dropdown-contents
<dropdown-contents
v-if=
"dropdownButtonVisible && showDropdownContents"
v-if=
"dropdownButtonVisible && showDropdownContents"
ref=
"dropdownContents"
ref=
"dropdownContents"
:allow-multiselect=
"allowMultiselect"
:labels-list-title=
"labelsListTitle"
:footer-create-label-title=
"footerCreateLabelTitle"
:footer-manage-label-title=
"footerManageLabelTitle"
:render-on-top=
"!contentIsOnViewport"
:render-on-top=
"!contentIsOnViewport"
:selected-labels=
"selectedLabels"
@
closeDropdown=
"handleDropdownClose"
/>
/>
</
template
>
</
template
>
</div>
</div>
...
...
app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/store/actions.js
View file @
7523392c
import
createFlash
from
'
~/flash
'
;
import
axios
from
'
~/lib/utils/axios_utils
'
;
import
{
__
}
from
'
~/locale
'
;
import
*
as
types
from
'
./mutation_types
'
;
import
*
as
types
from
'
./mutation_types
'
;
export
const
setInitialState
=
({
commit
},
props
)
=>
commit
(
types
.
SET_INITIAL_STATE
,
props
);
export
const
setInitialState
=
({
commit
},
props
)
=>
commit
(
types
.
SET_INITIAL_STATE
,
props
);
...
@@ -11,24 +8,5 @@ export const toggleDropdownContents = ({ commit }) => commit(types.TOGGLE_DROPDO
...
@@ -11,24 +8,5 @@ export const toggleDropdownContents = ({ commit }) => commit(types.TOGGLE_DROPDO
export
const
toggleDropdownContentsCreateView
=
({
commit
})
=>
export
const
toggleDropdownContentsCreateView
=
({
commit
})
=>
commit
(
types
.
TOGGLE_DROPDOWN_CONTENTS_CREATE_VIEW
);
commit
(
types
.
TOGGLE_DROPDOWN_CONTENTS_CREATE_VIEW
);
export
const
requestLabels
=
({
commit
})
=>
commit
(
types
.
REQUEST_LABELS
);
export
const
receiveLabelsSuccess
=
({
commit
},
labels
)
=>
commit
(
types
.
RECEIVE_SET_LABELS_SUCCESS
,
labels
);
export
const
receiveLabelsFailure
=
({
commit
})
=>
{
commit
(
types
.
RECEIVE_SET_LABELS_FAILURE
);
createFlash
({
message
:
__
(
'
Error fetching labels.
'
),
});
};
export
const
fetchLabels
=
({
state
,
dispatch
})
=>
{
dispatch
(
'
requestLabels
'
);
return
axios
.
get
(
state
.
labelsFetchPath
)
.
then
(({
data
})
=>
{
dispatch
(
'
receiveLabelsSuccess
'
,
data
);
})
.
catch
(()
=>
dispatch
(
'
receiveLabelsFailure
'
));
};
export
const
updateSelectedLabels
=
({
commit
},
labels
)
=>
export
const
updateSelectedLabels
=
({
commit
},
labels
)
=>
commit
(
types
.
UPDATE_SELECTED_LABELS
,
{
labels
});
commit
(
types
.
UPDATE_SELECTED_LABELS
,
{
labels
});
app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/store/mutation_types.js
View file @
7523392c
export
const
SET_INITIAL_STATE
=
'
SET_INITIAL_STATE
'
;
export
const
SET_INITIAL_STATE
=
'
SET_INITIAL_STATE
'
;
export
const
REQUEST_LABELS
=
'
REQUEST_LABELS
'
;
export
const
RECEIVE_LABELS_SUCCESS
=
'
RECEIVE_LABELS_SUCCESS
'
;
export
const
RECEIVE_LABELS_FAILURE
=
'
RECEIVE_LABELS_FAILURE
'
;
export
const
REQUEST_SET_LABELS
=
'
REQUEST_SET_LABELS
'
;
export
const
RECEIVE_SET_LABELS_SUCCESS
=
'
RECEIVE_SET_LABELS_SUCCESS
'
;
export
const
RECEIVE_SET_LABELS_FAILURE
=
'
RECEIVE_SET_LABELS_FAILURE
'
;
export
const
TOGGLE_DROPDOWN_BUTTON
=
'
TOGGLE_DROPDOWN_VISIBILITY
'
;
export
const
TOGGLE_DROPDOWN_BUTTON
=
'
TOGGLE_DROPDOWN_VISIBILITY
'
;
export
const
TOGGLE_DROPDOWN_CONTENTS
=
'
TOGGLE_DROPDOWN_CONTENTS
'
;
export
const
TOGGLE_DROPDOWN_CONTENTS
=
'
TOGGLE_DROPDOWN_CONTENTS
'
;
...
...
app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/store/mutations.js
View file @
7523392c
...
@@ -26,27 +26,6 @@ export default {
...
@@ -26,27 +26,6 @@ export default {
[
types
.
TOGGLE_DROPDOWN_CONTENTS_CREATE_VIEW
](
state
)
{
[
types
.
TOGGLE_DROPDOWN_CONTENTS_CREATE_VIEW
](
state
)
{
state
.
showDropdownContentsCreateView
=
!
state
.
showDropdownContentsCreateView
;
state
.
showDropdownContentsCreateView
=
!
state
.
showDropdownContentsCreateView
;
},
},
[
types
.
REQUEST_LABELS
](
state
)
{
state
.
labelsFetchInProgress
=
true
;
},
[
types
.
RECEIVE_SET_LABELS_SUCCESS
](
state
,
labels
)
{
// Iterate over every label and add a `set` prop
// to determine whether it is already a part of
// selectedLabels array.
const
selectedLabelIds
=
state
.
selectedLabels
.
map
((
label
)
=>
label
.
id
);
state
.
labelsFetchInProgress
=
false
;
state
.
labels
=
labels
.
reduce
((
allLabels
,
label
)
=>
{
allLabels
.
push
({
...
label
,
set
:
selectedLabelIds
.
includes
(
label
.
id
),
});
return
allLabels
;
},
[]);
},
[
types
.
RECEIVE_SET_LABELS_FAILURE
](
state
)
{
state
.
labelsFetchInProgress
=
false
;
},
[
types
.
UPDATE_SELECTED_LABELS
](
state
,
{
labels
})
{
[
types
.
UPDATE_SELECTED_LABELS
](
state
,
{
labels
})
{
// Find the label to update from all the labels
// Find the label to update from all the labels
// and change `set` prop value to represent their current state.
// and change `set` prop value to represent their current state.
...
...
spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view_spec.js
View file @
7523392c
import
{
GlLoadingIcon
,
GlLink
}
from
'
@gitlab/ui
'
;
import
{
GlLoadingIcon
,
GlLink
}
from
'
@gitlab/ui
'
;
import
{
shallowMount
,
createLocalVue
}
from
'
@vue/test-utils
'
;
import
{
shallowMount
,
createLocalVue
}
from
'
@vue/test-utils
'
;
import
Vue
,
{
nextTick
}
from
'
vue
'
;
import
{
nextTick
}
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
waitForPromises
from
'
helpers/wait_for_promises
'
;
import
waitForPromises
from
'
helpers/wait_for_promises
'
;
...
@@ -14,7 +14,7 @@ jest.mock('~/flash');
...
@@ -14,7 +14,7 @@ jest.mock('~/flash');
const
colors
=
Object
.
keys
(
mockSuggestedColors
);
const
colors
=
Object
.
keys
(
mockSuggestedColors
);
const
localVue
=
createLocalVue
();
const
localVue
=
createLocalVue
();
Vue
.
use
(
VueApollo
);
local
Vue
.
use
(
VueApollo
);
const
userRecoverableError
=
{
const
userRecoverableError
=
{
...
createLabelSuccessfulResponse
,
...
createLabelSuccessfulResponse
,
...
...
spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view_spec.js
View file @
7523392c
import
{
Gl
IntersectionObserver
,
GlLoadingIcon
,
GlSearchBoxByType
,
GlLink
}
from
'
@gitlab/ui
'
;
import
{
Gl
LoadingIcon
,
GlSearchBoxByType
}
from
'
@gitlab/ui
'
;
import
{
shallowMount
,
createLocalVue
}
from
'
@vue/test-utils
'
;
import
{
shallowMount
,
createLocalVue
}
from
'
@vue/test-utils
'
;
import
Vuex
from
'
vuex
'
;
import
{
nextTick
}
from
'
vue
'
;
import
{
UP_KEY_CODE
,
DOWN_KEY_CODE
,
ENTER_KEY_CODE
,
ESC_KEY_CODE
}
from
'
~/lib/utils/keycodes
'
;
import
VueApollo
from
'
vue-apollo
'
;
import
createMockApollo
from
'
helpers/mock_apollo_helper
'
;
import
waitForPromises
from
'
helpers/wait_for_promises
'
;
import
createFlash
from
'
~/flash
'
;
import
{
DEFAULT_DEBOUNCE_AND_THROTTLE_MS
}
from
'
~/lib/utils/constants
'
;
import
{
DropdownVariant
}
from
'
~/vue_shared/components/sidebar/labels_select_widget/constants
'
;
import
DropdownContentsLabelsView
from
'
~/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue
'
;
import
DropdownContentsLabelsView
from
'
~/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue
'
;
import
projectLabelsQuery
from
'
~/vue_shared/components/sidebar/labels_select_widget/graphql/project_labels.query.graphql
'
;
import
LabelItem
from
'
~/vue_shared/components/sidebar/labels_select_widget/label_item.vue
'
;
import
LabelItem
from
'
~/vue_shared/components/sidebar/labels_select_widget/label_item.vue
'
;
import
{
mockConfig
,
labelsQueryResponse
}
from
'
./mock_data
'
;
import
*
as
actions
from
'
~/vue_shared/components/sidebar/labels_select_widget/store/actions
'
;
jest
.
mock
(
'
~/flash
'
);
import
*
as
getters
from
'
~/vue_shared/components/sidebar/labels_select_widget/store/getters
'
;
import
mutations
from
'
~/vue_shared/components/sidebar/labels_select_widget/store/mutations
'
;
import
defaultState
from
'
~/vue_shared/components/sidebar/labels_select_widget/store/state
'
;
import
{
mockConfig
,
mockLabels
,
mockRegularLabel
}
from
'
./mock_data
'
;
const
localVue
=
createLocalVue
();
const
localVue
=
createLocalVue
();
localVue
.
use
(
Vuex
);
localVue
.
use
(
VueApollo
);
const
selectedLabels
=
[
{
id
:
28
,
title
:
'
Bug
'
,
description
:
'
Label for bugs
'
,
color
:
'
#FF0000
'
,
textColor
:
'
#FFFFFF
'
,
},
];
describe
(
'
DropdownContentsLabelsView
'
,
()
=>
{
describe
(
'
DropdownContentsLabelsView
'
,
()
=>
{
let
wrapper
;
let
wrapper
;
const
createComponent
=
(
initialState
=
mockConfig
)
=>
{
const
successfulQueryHandler
=
jest
.
fn
().
mockResolvedValue
(
labelsQueryResponse
);
const
store
=
new
Vuex
.
Store
({
getters
,
mutations
,
state
:
{
...
defaultState
(),
footerCreateLabelTitle
:
'
Create label
'
,
footerManageLabelTitle
:
'
Manage labels
'
,
},
actions
:
{
...
actions
,
fetchLabels
:
jest
.
fn
(),
},
});
store
.
dispatch
(
'
setInitialState
'
,
initialState
);
const
createComponent
=
({
store
.
dispatch
(
'
receiveLabelsSuccess
'
,
mockLabels
);
initialState
=
mockConfig
,
queryHandler
=
successfulQueryHandler
,
injected
=
{},
}
=
{})
=>
{
const
mockApollo
=
createMockApollo
([[
projectLabelsQuery
,
queryHandler
]]);
wrapper
=
shallowMount
(
DropdownContentsLabelsView
,
{
wrapper
=
shallowMount
(
DropdownContentsLabelsView
,
{
localVue
,
localVue
,
store
,
apolloProvider
:
mockApollo
,
provide
:
{
projectPath
:
'
test
'
,
iid
:
1
,
allowLabelCreate
:
true
,
labelsManagePath
:
'
/gitlab-org/my-project/-/labels
'
,
variant
:
DropdownVariant
.
Sidebar
,
...
injected
,
},
propsData
:
{
...
initialState
,
selectedLabels
,
},
stubs
:
{
GlSearchBoxByType
,
},
});
});
};
};
beforeEach
(()
=>
{
createComponent
();
});
afterEach
(()
=>
{
afterEach
(()
=>
{
wrapper
.
destroy
();
wrapper
.
destroy
();
wrapper
=
null
;
});
const
findDropdownContent
=
()
=>
wrapper
.
find
(
'
[data-testid="dropdown-content"]
'
);
const
findDropdownFooter
=
()
=>
wrapper
.
find
(
'
[data-testid="dropdown-footer"]
'
);
const
findLoadingIcon
=
()
=>
wrapper
.
find
(
GlLoadingIcon
);
describe
(
'
computed
'
,
()
=>
{
describe
(
'
visibleLabels
'
,
()
=>
{
it
(
'
returns matching labels filtered with `searchKey`
'
,
()
=>
{
wrapper
.
setData
({
searchKey
:
'
bug
'
,
});
expect
(
wrapper
.
vm
.
visibleLabels
.
length
).
toBe
(
1
);
expect
(
wrapper
.
vm
.
visibleLabels
[
0
].
title
).
toBe
(
'
Bug
'
);
});
it
(
'
returns matching labels with fuzzy filtering
'
,
()
=>
{
wrapper
.
setData
({
searchKey
:
'
bg
'
,
});
expect
(
wrapper
.
vm
.
visibleLabels
.
length
).
toBe
(
2
);
expect
(
wrapper
.
vm
.
visibleLabels
[
0
].
title
).
toBe
(
'
Bug
'
);
expect
(
wrapper
.
vm
.
visibleLabels
[
1
].
title
).
toBe
(
'
Boog
'
);
});
it
(
'
returns all labels when `searchKey` is empty
'
,
()
=>
{
wrapper
.
setData
({
searchKey
:
''
,
});
expect
(
wrapper
.
vm
.
visibleLabels
.
length
).
toBe
(
mockLabels
.
length
);
});
});
describe
(
'
showNoMatchingResultsMessage
'
,
()
=>
{
it
.
each
`
searchKey | labels | labelsDescription | returnValue
${
''
}
|
${[]}
|
${
'
empty
'
}
|
${
false
}
${
'
bug
'
}
|
${[]}
|
${
'
empty
'
}
|
${
true
}
${
''
}
|
${
mockLabels
}
|
${
'
not empty
'
}
|
${
false
}
${
'
bug
'
}
|
${
mockLabels
}
|
${
'
not empty
'
}
|
${
false
}
`
(
'
returns $returnValue when searchKey is "$searchKey" and visibleLabels is $labelsDescription
'
,
async
({
searchKey
,
labels
,
returnValue
})
=>
{
wrapper
.
setData
({
searchKey
,
});
});
wrapper
.
vm
.
$store
.
dispatch
(
'
receiveLabelsSuccess
'
,
labels
);
const
findSearchInput
=
()
=>
wrapper
.
findComponent
(
GlSearchBoxByType
);
const
findLabels
=
()
=>
wrapper
.
findAllComponents
(
LabelItem
);
const
findLoadingIcon
=
()
=>
wrapper
.
findComponent
(
GlLoadingIcon
);
await
wrapper
.
vm
.
$nextTick
();
const
findLabelsList
=
()
=>
wrapper
.
find
(
'
[data-testid="labels-list"]
'
);
const
findDropdownWrapper
=
()
=>
wrapper
.
find
(
'
[data-testid="dropdown-wrapper"]
'
);
const
findDropdownFooter
=
()
=>
wrapper
.
find
(
'
[data-testid="dropdown-footer"]
'
);
const
findNoResultsMessage
=
()
=>
wrapper
.
find
(
'
[data-testid="no-results"]
'
);
const
findCreateLabelButton
=
()
=>
wrapper
.
find
(
'
[data-testid="create-label-button"]
'
);
expect
(
wrapper
.
vm
.
showNoMatchingResultsMessage
).
toBe
(
returnValue
);
describe
(
'
when loading labels
'
,
()
=>
{
},
it
(
'
renders disabled search input field
'
,
async
()
=>
{
);
createComponent
(
);
}
);
expect
(
findSearchInput
().
props
(
'
disabled
'
)).
toBe
(
true
);
});
});
describe
(
'
methods
'
,
()
=>
{
it
(
'
renders loading icon
'
,
async
()
=>
{
describe
(
'
isLabelSelected
'
,
()
=>
{
createComponent
();
it
(
'
returns true when provided `label` param is one of the selected labels
'
,
()
=>
{
expect
(
findLoadingIcon
().
exists
()).
toBe
(
true
);
expect
(
wrapper
.
vm
.
isLabelSelected
(
mockRegularLabel
)).
toBe
(
true
);
});
});
it
(
'
returns false when provided `label` param is not one of the selected labels
'
,
()
=>
{
it
(
'
does not render labels list
'
,
async
()
=>
{
expect
(
wrapper
.
vm
.
isLabelSelected
(
mockLabels
[
2
])).
toBe
(
false
);
createComponent
();
expect
(
findLabelsList
().
exists
()).
toBe
(
false
);
});
});
});
});
describe
(
'
handleComponentAppear
'
,
()
=>
{
describe
(
'
when labels are loaded
'
,
()
=>
{
it
(
'
calls `focusInput` on searchInput field
'
,
async
()
=>
{
beforeEach
(
async
()
=>
{
wrapper
.
vm
.
$refs
.
searchInput
.
focusInput
=
jest
.
fn
();
createComponent
();
await
waitForPromises
();
await
wrapper
.
vm
.
handleComponentAppear
();
expect
(
wrapper
.
vm
.
$refs
.
searchInput
.
focusInput
).
toHaveBeenCalled
();
});
});
});
describe
(
'
handleComponentDisappear
'
,
()
=>
{
it
(
'
renders enabled search input field
'
,
async
()
=>
{
it
(
'
calls action `receiveLabelsSuccess` with empty array
'
,
()
=>
{
expect
(
findSearchInput
().
props
(
'
disabled
'
)).
toBe
(
false
);
jest
.
spyOn
(
wrapper
.
vm
,
'
receiveLabelsSuccess
'
);
wrapper
.
vm
.
handleComponentDisappear
();
expect
(
wrapper
.
vm
.
receiveLabelsSuccess
).
toHaveBeenCalledWith
([]);
});
});
});
describe
(
'
handleCreateLabelClick
'
,
()
=>
{
it
(
'
calls actions `receiveLabelsSuccess` with empty array and `toggleDropdownContentsCreateView`
'
,
()
=>
{
jest
.
spyOn
(
wrapper
.
vm
,
'
receiveLabelsSuccess
'
);
jest
.
spyOn
(
wrapper
.
vm
,
'
toggleDropdownContentsCreateView
'
);
wrapper
.
vm
.
handleCreateLabelClick
();
it
(
'
does not render loading icon
'
,
async
()
=>
{
expect
(
findLoadingIcon
().
exists
()).
toBe
(
false
);
expect
(
wrapper
.
vm
.
receiveLabelsSuccess
).
toHaveBeenCalledWith
([]);
expect
(
wrapper
.
vm
.
toggleDropdownContentsCreateView
).
toHaveBeenCalled
();
});
});
});
describe
(
'
handleKeyDown
'
,
()
=>
{
it
(
'
renders labels list
'
,
async
()
=>
{
it
(
'
decreases `currentHighlightItem` value by 1 when Up arrow key is pressed
'
,
()
=>
{
expect
(
findLabelsList
().
exists
()).
toBe
(
true
);
wrapper
.
setData
({
expect
(
findLabels
()).
toHaveLength
(
2
);
currentHighlightItem
:
1
,
});
});
wrapper
.
vm
.
handleKeyDown
({
it
(
'
changes highlighted label correctly on pressing down button
'
,
async
()
=>
{
keyCode
:
UP_KEY_CODE
,
expect
(
findLabels
().
at
(
0
).
attributes
(
'
highlight
'
)).
toBeUndefined
();
});
expect
(
wrapper
.
vm
.
currentHighlightItem
).
toBe
(
0
);
await
findDropdownWrapper
().
trigger
(
'
keydown.down
'
);
}
);
expect
(
findLabels
().
at
(
0
).
attributes
(
'
highlight
'
)).
toBe
(
'
true
'
);
it
(
'
increases `currentHighlightItem` value by 1 when Down arrow key is pressed
'
,
()
=>
{
await
findDropdownWrapper
().
trigger
(
'
keydown.down
'
);
wrapper
.
setData
({
expect
(
findLabels
().
at
(
1
).
attributes
(
'
highlight
'
)).
toBe
(
'
true
'
);
currentHighlightItem
:
1
,
expect
(
findLabels
().
at
(
0
).
attributes
(
'
highlight
'
)).
toBeUndefined
();
});
});
wrapper
.
vm
.
handleKeyDown
({
it
(
'
changes highlighted label correctly on pressing up button
'
,
async
()
=>
{
keyCode
:
DOWN_KEY_CODE
,
await
findDropdownWrapper
().
trigger
(
'
keydown.down
'
);
});
await
findDropdownWrapper
().
trigger
(
'
keydown.down
'
);
expect
(
findLabels
().
at
(
1
).
attributes
(
'
highlight
'
)).
toBe
(
'
true
'
);
expect
(
wrapper
.
vm
.
currentHighlightItem
).
toBe
(
2
);
await
findDropdownWrapper
().
trigger
(
'
keydown.up
'
);
expect
(
findLabels
().
at
(
0
).
attributes
(
'
highlight
'
)).
toBe
(
'
true
'
);
});
});
it
(
'
resets the search text when the Enter key is pressed
'
,
()
=>
{
it
(
'
changes label selected state when Enter is pressed
'
,
async
()
=>
{
wrapper
.
setData
({
expect
(
findLabels
().
at
(
0
).
attributes
(
'
islabelset
'
)).
toBeUndefined
();
currentHighlightItem
:
1
,
await
findDropdownWrapper
().
trigger
(
'
keydown.down
'
);
searchKey
:
'
bug
'
,
await
findDropdownWrapper
().
trigger
(
'
keydown.enter
'
);
});
wrapper
.
vm
.
handleKeyDown
({
expect
(
findLabels
().
at
(
0
).
attributes
(
'
islabelset
'
)).
toBe
(
'
true
'
);
keyCode
:
ENTER_KEY_CODE
,
});
});
expect
(
wrapper
.
vm
.
searchKey
).
toBe
(
''
);
it
(
'
emits `closeDropdown event` when Esc button is pressed
'
,
()
=>
{
}
);
findDropdownWrapper
().
trigger
(
'
keydown.esc
'
);
it
(
'
calls action `updateSelectedLabels` with currently highlighted label when Enter key is pressed
'
,
()
=>
{
expect
(
wrapper
.
emitted
(
'
closeDropdown
'
)).
toEqual
([[
selectedLabels
]]);
jest
.
spyOn
(
wrapper
.
vm
,
'
updateSelectedLabels
'
).
mockImplementation
();
wrapper
.
setData
({
currentHighlightItem
:
1
,
});
});
wrapper
.
vm
.
handleKeyDown
({
keyCode
:
ENTER_KEY_CODE
,
});
});
expect
(
wrapper
.
vm
.
updateSelectedLabels
).
toHaveBeenCalledWith
([
it
(
'
when search returns 0 results
'
,
async
()
=>
{
{
createComponent
({
...
mockLabels
[
1
],
queryHandler
:
jest
.
fn
().
mockResolvedValue
({
set
:
true
,
data
:
{
workspace
:
{
labels
:
{
nodes
:
[],
},
},
]);
},
});
},
}),
it
(
'
calls action `toggleDropdownContents` when Esc key is pressed
'
,
()
=>
{
jest
.
spyOn
(
wrapper
.
vm
,
'
toggleDropdownContents
'
).
mockImplementation
();
wrapper
.
setData
({
currentHighlightItem
:
1
,
});
wrapper
.
vm
.
handleKeyDown
({
keyCode
:
ESC_KEY_CODE
,
});
expect
(
wrapper
.
vm
.
toggleDropdownContents
).
toHaveBeenCalled
();
});
it
(
'
calls action `scrollIntoViewIfNeeded` in next tick when any key is pressed
'
,
()
=>
{
jest
.
spyOn
(
wrapper
.
vm
,
'
scrollIntoViewIfNeeded
'
).
mockImplementation
();
wrapper
.
setData
({
currentHighlightItem
:
1
,
});
wrapper
.
vm
.
handleKeyDown
({
keyCode
:
DOWN_KEY_CODE
,
});
return
wrapper
.
vm
.
$nextTick
(()
=>
{
expect
(
wrapper
.
vm
.
scrollIntoViewIfNeeded
).
toHaveBeenCalled
();
});
});
});
describe
(
'
handleLabelClick
'
,
()
=>
{
beforeEach
(()
=>
{
jest
.
spyOn
(
wrapper
.
vm
,
'
updateSelectedLabels
'
).
mockImplementation
();
});
it
(
'
calls action `updateSelectedLabels` with provided `label` param
'
,
()
=>
{
wrapper
.
vm
.
handleLabelClick
(
mockRegularLabel
);
expect
(
wrapper
.
vm
.
updateSelectedLabels
).
toHaveBeenCalledWith
([
mockRegularLabel
]);
});
it
(
'
calls action `toggleDropdownContents` when `state.allowMultiselect` is false
'
,
()
=>
{
jest
.
spyOn
(
wrapper
.
vm
,
'
toggleDropdownContents
'
);
wrapper
.
vm
.
$store
.
state
.
allowMultiselect
=
false
;
wrapper
.
vm
.
handleLabelClick
(
mockRegularLabel
);
expect
(
wrapper
.
vm
.
toggleDropdownContents
).
toHaveBeenCalled
();
});
});
});
describe
(
'
template
'
,
()
=>
{
it
(
'
renders gl-intersection-observer as component root
'
,
()
=>
{
expect
(
wrapper
.
find
(
GlIntersectionObserver
).
exists
()).
toBe
(
true
);
});
});
findSearchInput
().
vm
.
$emit
(
'
input
'
,
'
123
'
);
await
waitForPromises
();
await
nextTick
();
it
(
'
renders gl-loading-icon component when `labelsFetchInProgress` prop is true
'
,
()
=>
{
expect
(
findNoResultsMessage
().
isVisible
()).
toBe
(
true
);
wrapper
.
vm
.
$store
.
dispatch
(
'
requestLabels
'
);
return
wrapper
.
vm
.
$nextTick
(()
=>
{
const
loadingIconEl
=
findLoadingIcon
();
expect
(
loadingIconEl
.
exists
()).
toBe
(
true
);
expect
(
loadingIconEl
.
attributes
(
'
class
'
)).
toContain
(
'
labels-fetch-loading
'
);
});
});
});
it
(
'
renders label search input element
'
,
()
=>
{
it
(
'
calls `createFlash` when fetching labels failed
'
,
async
()
=>
{
const
searchInputEl
=
wrapper
.
find
(
GlSearchBoxByType
);
createComponent
({
queryHandler
:
jest
.
fn
().
mockRejectedValue
(
'
Houston, we have a problem!
'
)
});
jest
.
advanceTimersByTime
(
DEFAULT_DEBOUNCE_AND_THROTTLE_MS
);
expect
(
searchInputEl
.
exists
()).
toBe
(
true
);
await
waitForPromises
();
expect
(
createFlash
).
toHaveBeenCalled
();
});
});
it
(
'
renders label elements for all labels
'
,
()
=>
{
it
(
'
does not render footer on standalone dropdown
'
,
()
=>
{
expect
(
wrapper
.
findAll
(
LabelItem
)).
toHaveLength
(
mockLabels
.
length
);
createComponent
({
injected
:
{
variant
:
DropdownVariant
.
Standalone
}
});
});
it
(
'
renders label element with `highlight` set to true when value of `currentHighlightItem` is more than -1
'
,
()
=>
{
expect
(
findDropdownFooter
().
exists
()).
toBe
(
false
);
wrapper
.
setData
({
currentHighlightItem
:
0
,
});
});
return
wrapper
.
vm
.
$nextTick
(()
=>
{
it
(
'
renders footer on sidebar dropdown
'
,
()
=>
{
const
labelItemEl
=
findDropdownContent
().
find
(
LabelItem
);
createComponent
();
expect
(
labelItemEl
.
attributes
(
'
highlight
'
)).
toBe
(
'
true
'
);
});
});
it
(
'
renders element containing "No matching results" when `searchKey` does not match with any label
'
,
()
=>
{
expect
(
findDropdownFooter
().
exists
()).
toBe
(
true
);
wrapper
.
setData
({
searchKey
:
'
abc
'
,
});
});
return
wrapper
.
vm
.
$nextTick
(
()
=>
{
it
(
'
renders footer on embedded dropdown
'
,
()
=>
{
const
noMatchEl
=
findDropdownContent
().
find
(
'
li
'
);
createComponent
({
injected
:
{
variant
:
DropdownVariant
.
Embedded
}
}
);
expect
(
noMatchEl
.
isVisible
()).
toBe
(
true
);
expect
(
findDropdownFooter
().
exists
()).
toBe
(
true
);
expect
(
noMatchEl
.
text
()).
toContain
(
'
No matching results
'
);
});
});
});
it
(
'
renders empty content while loading
'
,
()
=>
{
it
(
'
does not render create label button if `allowLabelCreate` is false
'
,
()
=>
{
wrapper
.
vm
.
$store
.
state
.
labelsFetchInProgress
=
true
;
createComponent
({
injected
:
{
allowLabelCreate
:
false
}
})
;
return
wrapper
.
vm
.
$nextTick
(()
=>
{
expect
(
findCreateLabelButton
().
exists
()).
toBe
(
false
);
const
dropdownContent
=
findDropdownContent
();
const
loadingIcon
=
findLoadingIcon
();
expect
(
dropdownContent
.
exists
()).
toBe
(
true
);
expect
(
dropdownContent
.
isVisible
()).
toBe
(
true
);
expect
(
loadingIcon
.
exists
()).
toBe
(
true
);
expect
(
loadingIcon
.
isVisible
()).
toBe
(
true
);
});
});
});
it
(
'
renders footer list items
'
,
()
=>
{
describe
(
'
when `allowLabelCreate` is true
'
,
()
=>
{
const
footerLinks
=
findDropdownFooter
().
findAll
(
GlLink
);
beforeEach
(()
=>
{
const
createLabelLink
=
footerLinks
.
at
(
0
);
createComponent
();
const
manageLabelsLink
=
footerLinks
.
at
(
1
);
expect
(
createLabelLink
.
exists
()).
toBe
(
true
);
expect
(
createLabelLink
.
text
()).
toBe
(
'
Create label
'
);
expect
(
manageLabelsLink
.
exists
()).
toBe
(
true
);
expect
(
manageLabelsLink
.
text
()).
toBe
(
'
Manage labels
'
);
});
});
it
(
'
does not render "Create label" footer link when `state.allowLabelCreate` is `false`
'
,
()
=>
{
it
(
'
renders create label button
'
,
()
=>
{
wrapper
.
vm
.
$store
.
state
.
allowLabelCreate
=
false
;
expect
(
findCreateLabelButton
().
exists
()).
toBe
(
true
);
return
wrapper
.
vm
.
$nextTick
(()
=>
{
const
createLabelLink
=
findDropdownFooter
().
findAll
(
GlLink
).
at
(
0
);
expect
(
createLabelLink
.
text
()).
not
.
toBe
(
'
Create label
'
);
});
});
});
it
(
'
does not render footer list items when `state.variant` is "standalone"
'
,
()
=>
{
it
(
'
emits `toggleDropdownContentsCreateView` event on create label button click
'
,
()
=>
{
createComponent
({
...
mockConfig
,
variant
:
'
standalone
'
});
findCreateLabelButton
().
vm
.
$emit
(
'
click
'
);
expect
(
findDropdownFooter
().
exists
()).
toBe
(
false
);
});
it
(
'
renders footer list items when `state.variant` is "embedded"
'
,
()
=>
{
expect
(
wrapper
.
emitted
(
'
toggleDropdownContentsCreateView
'
)).
toEqual
([[]]);
expect
(
findDropdownFooter
().
exists
()).
toBe
(
true
);
});
});
});
});
});
});
spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_spec.js
View file @
7523392c
...
@@ -5,7 +5,7 @@ import { DropdownVariant } from '~/vue_shared/components/sidebar/labels_select_w
...
@@ -5,7 +5,7 @@ import { DropdownVariant } from '~/vue_shared/components/sidebar/labels_select_w
import
DropdownContents
from
'
~/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue
'
;
import
DropdownContents
from
'
~/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue
'
;
import
labelsSelectModule
from
'
~/vue_shared/components/sidebar/labels_select_widget/store
'
;
import
labelsSelectModule
from
'
~/vue_shared/components/sidebar/labels_select_widget/store
'
;
import
{
mockConfig
}
from
'
./mock_data
'
;
import
{
mockConfig
,
mockLabels
}
from
'
./mock_data
'
;
const
localVue
=
createLocalVue
();
const
localVue
=
createLocalVue
();
localVue
.
use
(
Vuex
);
localVue
.
use
(
Vuex
);
...
@@ -19,6 +19,11 @@ const createComponent = (initialState = mockConfig, defaultProps = {}) => {
...
@@ -19,6 +19,11 @@ const createComponent = (initialState = mockConfig, defaultProps = {}) => {
propsData
:
{
propsData
:
{
...
defaultProps
,
...
defaultProps
,
labelsCreateTitle
:
'
test
'
,
labelsCreateTitle
:
'
test
'
,
selectedLabels
:
mockLabels
,
allowMultiselect
:
true
,
labelsListTitle
:
'
Assign labels
'
,
footerCreateLabelTitle
:
'
create
'
,
footerManageLabelTitle
:
'
manage
'
,
},
},
localVue
,
localVue
,
store
,
store
,
...
...
spec/frontend/vue_shared/components/sidebar/labels_select_widget/labels_select_root_spec.js
View file @
7523392c
...
@@ -50,58 +50,6 @@ describe('LabelsSelectRoot', () => {
...
@@ -50,58 +50,6 @@ describe('LabelsSelectRoot', () => {
});
});
describe
(
'
methods
'
,
()
=>
{
describe
(
'
methods
'
,
()
=>
{
describe
(
'
handleVuexActionDispatch
'
,
()
=>
{
it
(
'
calls `handleDropdownClose` when params `action.type` is `toggleDropdownContents` and state has `showDropdownButton` & `showDropdownContents` props `false`
'
,
()
=>
{
createComponent
();
jest
.
spyOn
(
wrapper
.
vm
,
'
handleDropdownClose
'
).
mockImplementation
();
wrapper
.
vm
.
handleVuexActionDispatch
(
{
type
:
'
toggleDropdownContents
'
},
{
showDropdownButton
:
false
,
showDropdownContents
:
false
,
labels
:
[{
id
:
1
},
{
id
:
2
,
touched
:
true
}],
},
);
expect
(
wrapper
.
vm
.
handleDropdownClose
).
toHaveBeenCalledWith
(
expect
.
arrayContaining
([
{
id
:
2
,
touched
:
true
,
},
]),
);
});
it
(
'
calls `handleDropdownClose` with state.labels filterd using `set` prop when dropdown variant is `embedded`
'
,
()
=>
{
createComponent
({
...
mockConfig
,
variant
:
'
embedded
'
,
});
jest
.
spyOn
(
wrapper
.
vm
,
'
handleDropdownClose
'
).
mockImplementation
();
wrapper
.
vm
.
handleVuexActionDispatch
(
{
type
:
'
toggleDropdownContents
'
},
{
showDropdownButton
:
false
,
showDropdownContents
:
false
,
labels
:
[{
id
:
1
},
{
id
:
2
,
set
:
true
}],
},
);
expect
(
wrapper
.
vm
.
handleDropdownClose
).
toHaveBeenCalledWith
(
expect
.
arrayContaining
([
{
id
:
2
,
set
:
true
,
},
]),
);
});
});
describe
(
'
handleDropdownClose
'
,
()
=>
{
describe
(
'
handleDropdownClose
'
,
()
=>
{
beforeEach
(()
=>
{
beforeEach
(()
=>
{
createComponent
();
createComponent
();
...
...
spec/frontend/vue_shared/components/sidebar/labels_select_widget/mock_data.js
View file @
7523392c
...
@@ -48,6 +48,8 @@ export const mockConfig = {
...
@@ -48,6 +48,8 @@ export const mockConfig = {
labelsManagePath
:
'
/gitlab-org/my-project/-/labels
'
,
labelsManagePath
:
'
/gitlab-org/my-project/-/labels
'
,
labelsFilterBasePath
:
'
/gitlab-org/my-project/issues
'
,
labelsFilterBasePath
:
'
/gitlab-org/my-project/issues
'
,
labelsFilterParam
:
'
label_name
'
,
labelsFilterParam
:
'
label_name
'
,
footerCreateLabelTitle
:
'
create
'
,
footerManageLabelTitle
:
'
manage
'
,
};
};
export
const
mockSuggestedColors
=
{
export
const
mockSuggestedColors
=
{
...
@@ -91,3 +93,26 @@ export const createLabelSuccessfulResponse = {
...
@@ -91,3 +93,26 @@ export const createLabelSuccessfulResponse = {
},
},
},
},
};
};
export
const
labelsQueryResponse
=
{
data
:
{
workspace
:
{
labels
:
{
nodes
:
[
{
color
:
'
#330066
'
,
description
:
null
,
id
:
'
gid://gitlab/ProjectLabel/1
'
,
title
:
'
Label1
'
,
},
{
color
:
'
#2f7b2e
'
,
description
:
null
,
id
:
'
gid://gitlab/ProjectLabel/2
'
,
title
:
'
Label2
'
,
},
],
},
},
},
};
spec/frontend/vue_shared/components/sidebar/labels_select_widget/store/actions_spec.js
View file @
7523392c
import
MockAdapter
from
'
axios-mock-adapter
'
;
import
testAction
from
'
helpers/vuex_action_helper
'
;
import
testAction
from
'
helpers/vuex_action_helper
'
;
import
createFlash
from
'
~/flash
'
;
import
axios
from
'
~/lib/utils/axios_utils
'
;
import
*
as
actions
from
'
~/vue_shared/components/sidebar/labels_select_widget/store/actions
'
;
import
*
as
actions
from
'
~/vue_shared/components/sidebar/labels_select_widget/store/actions
'
;
import
*
as
types
from
'
~/vue_shared/components/sidebar/labels_select_widget/store/mutation_types
'
;
import
*
as
types
from
'
~/vue_shared/components/sidebar/labels_select_widget/store/mutation_types
'
;
import
defaultState
from
'
~/vue_shared/components/sidebar/labels_select_widget/store/state
'
;
import
defaultState
from
'
~/vue_shared/components/sidebar/labels_select_widget/store/state
'
;
...
@@ -72,90 +68,6 @@ describe('LabelsSelect Actions', () => {
...
@@ -72,90 +68,6 @@ describe('LabelsSelect Actions', () => {
});
});
});
});
describe
(
'
requestLabels
'
,
()
=>
{
it
(
'
sets value of `state.labelsFetchInProgress` to `true`
'
,
(
done
)
=>
{
testAction
(
actions
.
requestLabels
,
{},
state
,
[{
type
:
types
.
REQUEST_LABELS
}],
[],
done
);
});
});
describe
(
'
receiveLabelsSuccess
'
,
()
=>
{
it
(
'
sets provided labels to `state.labels`
'
,
(
done
)
=>
{
const
labels
=
[{
id
:
1
},
{
id
:
2
},
{
id
:
3
},
{
id
:
4
}];
testAction
(
actions
.
receiveLabelsSuccess
,
labels
,
state
,
[{
type
:
types
.
RECEIVE_SET_LABELS_SUCCESS
,
payload
:
labels
}],
[],
done
,
);
});
});
describe
(
'
receiveLabelsFailure
'
,
()
=>
{
it
(
'
sets value `state.labelsFetchInProgress` to `false`
'
,
(
done
)
=>
{
testAction
(
actions
.
receiveLabelsFailure
,
{},
state
,
[{
type
:
types
.
RECEIVE_SET_LABELS_FAILURE
}],
[],
done
,
);
});
it
(
'
shows flash error
'
,
()
=>
{
actions
.
receiveLabelsFailure
({
commit
:
()
=>
{}
});
expect
(
createFlash
).
toHaveBeenCalledWith
({
message
:
'
Error fetching labels.
'
});
});
});
describe
(
'
fetchLabels
'
,
()
=>
{
let
mock
;
beforeEach
(()
=>
{
mock
=
new
MockAdapter
(
axios
);
state
.
labelsFetchPath
=
'
labels.json
'
;
});
afterEach
(()
=>
{
mock
.
restore
();
});
describe
(
'
on success
'
,
()
=>
{
it
(
'
dispatches `requestLabels` & `receiveLabelsSuccess` actions
'
,
(
done
)
=>
{
const
labels
=
[{
id
:
1
},
{
id
:
2
},
{
id
:
3
},
{
id
:
4
}];
mock
.
onGet
(
/labels.json/
).
replyOnce
(
200
,
labels
);
testAction
(
actions
.
fetchLabels
,
{},
state
,
[],
[{
type
:
'
requestLabels
'
},
{
type
:
'
receiveLabelsSuccess
'
,
payload
:
labels
}],
done
,
);
});
});
describe
(
'
on failure
'
,
()
=>
{
it
(
'
dispatches `requestLabels` & `receiveLabelsFailure` actions
'
,
(
done
)
=>
{
mock
.
onGet
(
/labels.json/
).
replyOnce
(
500
,
{});
testAction
(
actions
.
fetchLabels
,
{},
state
,
[],
[{
type
:
'
requestLabels
'
},
{
type
:
'
receiveLabelsFailure
'
}],
done
,
);
});
});
});
describe
(
'
updateSelectedLabels
'
,
()
=>
{
describe
(
'
updateSelectedLabels
'
,
()
=>
{
it
(
'
updates `state.labels` based on provided `labels` param
'
,
(
done
)
=>
{
it
(
'
updates `state.labels` based on provided `labels` param
'
,
(
done
)
=>
{
const
labels
=
[{
id
:
1
},
{
id
:
2
},
{
id
:
3
},
{
id
:
4
}];
const
labels
=
[{
id
:
1
},
{
id
:
2
},
{
id
:
3
},
{
id
:
4
}];
...
...
spec/frontend/vue_shared/components/sidebar/labels_select_widget/store/mutations_spec.js
View file @
7523392c
...
@@ -67,58 +67,6 @@ describe('LabelsSelect Mutations', () => {
...
@@ -67,58 +67,6 @@ describe('LabelsSelect Mutations', () => {
});
});
});
});
describe
(
`
${
types
.
REQUEST_LABELS
}
`
,
()
=>
{
it
(
'
sets value of `state.labelsFetchInProgress` to true
'
,
()
=>
{
const
state
=
{
labelsFetchInProgress
:
false
,
};
mutations
[
types
.
REQUEST_LABELS
](
state
);
expect
(
state
.
labelsFetchInProgress
).
toBe
(
true
);
});
});
describe
(
`
${
types
.
RECEIVE_SET_LABELS_SUCCESS
}
`
,
()
=>
{
const
selectedLabels
=
[{
id
:
2
},
{
id
:
4
}];
const
labels
=
[{
id
:
1
},
{
id
:
2
},
{
id
:
3
},
{
id
:
4
}];
it
(
'
sets value of `state.labelsFetchInProgress` to false
'
,
()
=>
{
const
state
=
{
selectedLabels
,
labelsFetchInProgress
:
true
,
};
mutations
[
types
.
RECEIVE_SET_LABELS_SUCCESS
](
state
,
labels
);
expect
(
state
.
labelsFetchInProgress
).
toBe
(
false
);
});
it
(
'
sets provided `labels` to `state.labels` along with `set` prop based on `state.selectedLabels`
'
,
()
=>
{
const
selectedLabelIds
=
selectedLabels
.
map
((
label
)
=>
label
.
id
);
const
state
=
{
selectedLabels
,
labelsFetchInProgress
:
true
,
};
mutations
[
types
.
RECEIVE_SET_LABELS_SUCCESS
](
state
,
labels
);
state
.
labels
.
forEach
((
label
)
=>
{
if
(
selectedLabelIds
.
includes
(
label
.
id
))
{
expect
(
label
.
set
).
toBe
(
true
);
}
});
});
});
describe
(
`
${
types
.
RECEIVE_SET_LABELS_FAILURE
}
`
,
()
=>
{
it
(
'
sets value of `state.labelsFetchInProgress` to false
'
,
()
=>
{
const
state
=
{
labelsFetchInProgress
:
true
,
};
mutations
[
types
.
RECEIVE_SET_LABELS_FAILURE
](
state
);
expect
(
state
.
labelsFetchInProgress
).
toBe
(
false
);
});
});
describe
(
`
${
types
.
UPDATE_SELECTED_LABELS
}
`
,
()
=>
{
describe
(
`
${
types
.
UPDATE_SELECTED_LABELS
}
`
,
()
=>
{
let
labels
;
let
labels
;
...
...
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