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
81dd621c
Commit
81dd621c
authored
Sep 29, 2021
by
GitLab Bot
Browse files
Options
Browse Files
Download
Plain Diff
Automatic merge of gitlab-org/gitlab master
parents
4190ff47
ce9b3e4f
Changes
21
Hide whitespace changes
Inline
Side-by-side
Showing
21 changed files
with
556 additions
and
208 deletions
+556
-208
.rubocop_manual_todo.yml
.rubocop_manual_todo.yml
+0
-1
app/assets/javascripts/sidebar/components/labels/sidebar_labels.vue
.../javascripts/sidebar/components/labels/sidebar_labels.vue
+16
-14
app/assets/javascripts/sidebar/constants.js
app/assets/javascripts/sidebar/constants.js
+15
-0
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
+50
-25
app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue
...ar/labels_select_widget/dropdown_contents_create_view.vue
+27
-11
app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue
...ar/labels_select_widget/dropdown_contents_labels_view.vue
+78
-52
app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/epic_labels.query.graphql
...ar/labels_select_widget/graphql/epic_labels.query.graphql
+15
-0
app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/group_labels.query.graphql
...r/labels_select_widget/graphql/group_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
+53
-27
ee/app/assets/javascripts/epic/components/epic_form.vue
ee/app/assets/javascripts/epic/components/epic_form.vue
+25
-0
ee/app/assets/javascripts/epic/new_epic_bundle.js
ee/app/assets/javascripts/epic/new_epic_bundle.js
+2
-0
ee/app/controllers/groups/epics_controller.rb
ee/app/controllers/groups/epics_controller.rb
+1
-0
lib/gitlab/email/message/in_product_marketing/base.rb
lib/gitlab/email/message/in_product_marketing/base.rb
+2
-2
lib/gitlab/email/message/in_product_marketing/helper.rb
lib/gitlab/email/message/in_product_marketing/helper.rb
+1
-2
spec/frontend/sidebar/sidebar_labels_spec.js
spec/frontend/sidebar/sidebar_labels_spec.js
+1
-2
spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view_spec.js
...abels_select_widget/dropdown_contents_create_view_spec.js
+36
-14
spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view_spec.js
...abels_select_widget/dropdown_contents_labels_view_spec.js
+23
-11
spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_spec.js
...ts/sidebar/labels_select_widget/dropdown_contents_spec.js
+86
-21
spec/frontend/vue_shared/components/sidebar/labels_select_widget/labels_select_root_spec.js
...s/sidebar/labels_select_widget/labels_select_root_spec.js
+90
-25
spec/frontend/vue_shared/components/sidebar/labels_select_widget/mock_data.js
...ared/components/sidebar/labels_select_widget/mock_data.js
+21
-1
No files found.
.rubocop_manual_todo.yml
View file @
81dd621c
...
...
@@ -2603,7 +2603,6 @@ Rails/IncludeUrlHelper:
-
'
ee/spec/lib/banzai/filter/cross_project_issuable_information_filter_spec.rb'
-
'
ee/spec/lib/banzai/filter/issuable_state_filter_spec.rb'
-
'
lib/gitlab/ci/badge/metadata.rb'
-
'
lib/gitlab/email/message/in_product_marketing/helper.rb'
-
'
spec/helpers/merge_requests_helper_spec.rb'
-
'
spec/helpers/nav/top_nav_helper_spec.rb'
-
'
spec/helpers/notify_helper_spec.rb'
...
...
app/assets/javascripts/sidebar/components/labels/sidebar_labels.vue
View file @
81dd621c
...
...
@@ -53,30 +53,32 @@ export default {
handleDropdownClose
()
{
$
(
this
.
$el
).
trigger
(
'
hidden.gl.dropdown
'
);
},
getUpdateVariables
(
dropdownLabels
)
{
const
currentLabelIds
=
this
.
selectedLabels
.
map
((
label
)
=>
label
.
id
);
const
dropdownLabelIds
=
dropdownLabels
.
map
((
label
)
=>
label
.
id
);
const
userAddedLabelIds
=
this
.
glFeatures
.
labelsWidget
?
difference
(
dropdownLabelIds
,
currentLabelIds
)
:
dropdownLabels
.
filter
((
label
)
=>
label
.
set
).
map
((
label
)
=>
label
.
id
);
const
userRemovedLabelIds
=
this
.
glFeatures
.
labelsWidget
?
difference
(
currentLabelIds
,
dropdownLabelIds
)
:
dropdownLabels
.
filter
((
label
)
=>
!
label
.
set
).
map
((
label
)
=>
label
.
id
);
getUpdateVariables
(
labels
)
{
let
labelIds
=
[];
const
labelIds
=
difference
(
union
(
currentLabelIds
,
userAddedLabelIds
),
userRemovedLabelIds
);
if
(
this
.
glFeatures
.
labelsWidget
)
{
labelIds
=
labels
.
map
(({
id
})
=>
toLabelGid
(
id
));
}
else
{
const
currentLabelIds
=
this
.
selectedLabels
.
map
((
label
)
=>
label
.
id
);
const
userAddedLabelIds
=
labels
.
filter
((
label
)
=>
label
.
set
).
map
((
label
)
=>
label
.
id
);
const
userRemovedLabelIds
=
labels
.
filter
((
label
)
=>
!
label
.
set
).
map
((
label
)
=>
label
.
id
);
labelIds
=
difference
(
union
(
currentLabelIds
,
userAddedLabelIds
),
userRemovedLabelIds
).
map
(
toLabelGid
,
);
}
switch
(
this
.
issuableType
)
{
case
IssuableType
.
Issue
:
return
{
addLabelIds
:
userAddedLabelIds
,
iid
:
this
.
iid
,
projectPath
:
this
.
projectPath
,
removeLabelIds
:
userRemovedL
abelIds
,
l
abelIds
,
};
case
IssuableType
.
MergeRequest
:
return
{
iid
:
this
.
iid
,
labelIds
:
labelIds
.
map
(
toLabelGid
)
,
labelIds
,
operationMode
:
MutationOperationMode
.
Replace
,
projectPath
:
this
.
projectPath
,
};
...
...
@@ -152,8 +154,8 @@ export default {
:labels-select-in-progress=
"isLabelsSelectInProgress"
:selected-labels=
"selectedLabels"
:variant=
"$options.variant"
:issuable-type=
"issuableType"
data-qa-selector=
"labels_block"
@
onDropdownClose=
"handleDropdownClose"
@
onLabelRemove=
"handleLabelRemove"
@
updateSelectedLabels=
"handleUpdateSelectedLabels"
>
...
...
app/assets/javascripts/sidebar/constants.js
View file @
81dd621c
...
...
@@ -31,6 +31,10 @@ import updateIssueSubscriptionMutation from '~/sidebar/queries/update_issue_subs
import
mergeRequestMilestoneMutation
from
'
~/sidebar/queries/update_merge_request_milestone.mutation.graphql
'
;
import
updateMergeRequestSubscriptionMutation
from
'
~/sidebar/queries/update_merge_request_subscription.mutation.graphql
'
;
import
updateAlertAssigneesMutation
from
'
~/vue_shared/alert_details/graphql/mutations/alert_set_assignees.mutation.graphql
'
;
import
epicLabelsQuery
from
'
~/vue_shared/components/sidebar/labels_select_widget/graphql/epic_labels.query.graphql
'
;
import
groupLabelsQuery
from
'
~/vue_shared/components/sidebar/labels_select_widget/graphql/group_labels.query.graphql
'
;
import
issueLabelsQuery
from
'
~/vue_shared/components/sidebar/labels_select_widget/graphql/issue_labels.query.graphql
'
;
import
projectLabelsQuery
from
'
~/vue_shared/components/sidebar/labels_select_widget/graphql/project_labels.query.graphql
'
;
import
getAlertAssignees
from
'
~/vue_shared/components/sidebar/queries/get_alert_assignees.query.graphql
'
;
import
getIssueAssignees
from
'
~/vue_shared/components/sidebar/queries/get_issue_assignees.query.graphql
'
;
import
issueParticipantsQuery
from
'
~/vue_shared/components/sidebar/queries/get_issue_participants.query.graphql
'
;
...
...
@@ -105,6 +109,17 @@ export const referenceQueries = {
},
};
export
const
labelsQueries
=
{
[
IssuableType
.
Issue
]:
{
issuableQuery
:
issueLabelsQuery
,
workspaceQuery
:
projectLabelsQuery
,
},
[
IssuableType
.
Epic
]:
{
issuableQuery
:
epicLabelsQuery
,
workspaceQuery
:
groupLabelsQuery
,
},
};
export
const
dateTypes
=
{
start
:
'
startDate
'
,
due
:
'
dueDate
'
,
...
...
app/assets/javascripts/sidebar/mount_sidebar.js
View file @
81dd621c
...
...
@@ -241,6 +241,7 @@ function mountMilestoneSelect() {
export
function
mountSidebarLabels
()
{
const
el
=
document
.
querySelector
(
'
.js-sidebar-labels
'
);
const
{
fullPath
}
=
getSidebarOptions
();
if
(
!
el
)
{
return
false
;
...
...
@@ -251,6 +252,7 @@ export function mountSidebarLabels() {
apolloProvider
,
provide
:
{
...
el
.
dataset
,
fullPath
,
allowLabelCreate
:
parseBoolean
(
el
.
dataset
.
allowLabelCreate
),
allowLabelEdit
:
parseBoolean
(
el
.
dataset
.
canEdit
),
allowScopedLabels
:
parseBoolean
(
el
.
dataset
.
allowScopedLabels
),
...
...
app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue
View file @
81dd621c
<
script
>
import
{
GlButton
,
GlDropdown
,
GlDropdownItem
,
GlLink
}
from
'
@gitlab/ui
'
;
import
{
__
,
s__
,
sprintf
}
from
'
~/locale
'
;
import
DropdownContentsCreateView
from
'
./dropdown_contents_create_view.vue
'
;
import
DropdownContentsLabelsView
from
'
./dropdown_contents_labels_view.vue
'
;
import
{
isDropdownVariantS
idebar
,
isDropdownVariantEmbedded
}
from
'
./utils
'
;
import
{
isDropdownVariantS
tandalone
}
from
'
./utils
'
;
export
default
{
components
:
{
...
...
@@ -48,10 +48,15 @@ export default {
type
:
String
,
required
:
true
,
},
issuableType
:
{
type
:
String
,
required
:
true
,
},
},
data
()
{
return
{
showDropdownContentsCreateView
:
false
,
localSelectedLabels
:
[...
this
.
selectedLabels
],
};
},
computed
:
{
...
...
@@ -64,28 +69,42 @@ export default {
dropdownTitle
()
{
return
this
.
showDropdownContentsCreateView
?
this
.
labelsCreateTitle
:
this
.
labelsListTitle
;
},
buttonText
()
{
if
(
!
this
.
localSelectedLabels
.
length
)
{
return
this
.
dropdownButtonText
||
__
(
'
Label
'
);
}
else
if
(
this
.
localSelectedLabels
.
length
>
1
)
{
return
sprintf
(
s__
(
'
LabelSelect|%{firstLabelName} +%{remainingLabelCount} more
'
),
{
firstLabelName
:
this
.
localSelectedLabels
[
0
].
title
,
remainingLabelCount
:
this
.
localSelectedLabels
.
length
-
1
,
});
}
return
this
.
localSelectedLabels
[
0
].
title
;
},
showDropdownFooter
()
{
return
(
!
this
.
showDropdownContentsCreateView
&&
(
this
.
isDropdownVariantSidebar
(
this
.
variant
)
||
this
.
isDropdownVariantEmbedded
(
this
.
variant
))
);
return
!
this
.
showDropdownContentsCreateView
&&
!
this
.
isStandalone
;
},
isStandalone
()
{
return
isDropdownVariantStandalone
(
this
.
variant
);
},
},
mounted
()
{
this
.
$refs
.
dropdown
.
show
();
},
methods
:
{
showDropdown
()
{
this
.
$refs
.
dropdown
.
show
();
},
toggleDropdownContentsCreateView
()
{
this
.
showDropdownContentsCreateView
=
!
this
.
showDropdownContentsCreateView
;
},
toggleDropdownContent
()
{
this
.
toggleDropdownContentsCreateView
();
// Required to recalculate dropdown position as its size changes
this
.
$refs
.
dropdown
.
$refs
.
dropdown
.
$_popper
.
scheduleUpdate
();
if
(
this
.
$refs
.
dropdown
?.
$refs
.
dropdown
)
{
this
.
$refs
.
dropdown
.
$refs
.
dropdown
.
$_popper
.
scheduleUpdate
();
}
},
closeDropdown
()
{
this
.
$emit
(
'
setLabels
'
,
this
.
localSelectedLabels
);
this
.
$refs
.
dropdown
.
hide
();
},
isDropdownVariantSidebar
,
isDropdownVariantEmbedded
,
},
};
</
script
>
...
...
@@ -93,14 +112,16 @@ export default {
<
template
>
<gl-dropdown
ref=
"dropdown"
:text=
"
dropdownB
uttonText"
:text=
"
b
uttonText"
class=
"gl-w-full gl-mt-2"
data-qa-selector=
"labels_dropdown_content"
@
hide=
"$emit('setLabels', localSelectedLabels)"
>
<template
#header
>
<div
v-if=
"
isDropdownVariantSidebar(variant) || isDropdownVariantEmbedded(variant)
"
v-if=
"
!isStandalone
"
class=
"dropdown-title gl-display-flex gl-align-items-center gl-pt-0 gl-pb-3!"
data-testid=
"dropdown-header"
>
<gl-button
v-if=
"showDropdownContentsCreateView"
...
...
@@ -119,27 +140,31 @@ export default {
size=
"small"
class=
"dropdown-header-button gl-p-0!"
icon=
"close"
@
click=
"$emit('closeDropdown')"
data-testid=
"close-button"
@
click=
"closeDropdown"
/>
</div>
</
template
>
<component
:is=
"dropdownContentsView"
:selected-labels=
"selectedLabels"
:allow-multiselect=
"allowMultiselect"
@
hideCreateView=
"toggleDropdownContentsCreateView"
@
setLabels=
"$emit('setLabels', $event)"
/>
<
template
#default
>
<component
:is=
"dropdownContentsView"
v-model=
"localSelectedLabels"
:selected-labels=
"selectedLabels"
:allow-multiselect=
"allowMultiselect"
:issuable-type=
"issuableType"
@
hideCreateView=
"toggleDropdownContentsCreateView"
/>
</
template
>
<
template
#footer
>
<div
v-if=
"showDropdownFooter"
data-testid=
"dropdown-footer"
>
<gl-dropdown-item
v-if=
"allowLabelCreate"
data-testid=
"create-label-button"
@
click.
native.captur
e.stop=
"toggleDropdownContent"
@
click.
capture.nativ
e.stop=
"toggleDropdownContent"
>
{{
footerCreateLabelTitle
}}
</gl-dropdown-item>
<gl-dropdown-item
:href=
"labelsManagePath"
@
click
.
native
.
captur
e
.
stop
>
<gl-dropdown-item
:href=
"labelsManagePath"
@
click
.
capture
.
nativ
e
.
stop
>
{{
footerManageLabelTitle
}}
</gl-dropdown-item>
</div>
...
...
app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue
View file @
81dd621c
...
...
@@ -2,9 +2,10 @@
import
{
GlTooltipDirective
,
GlButton
,
GlFormInput
,
GlLink
,
GlLoadingIcon
}
from
'
@gitlab/ui
'
;
import
produce
from
'
immer
'
;
import
createFlash
from
'
~/flash
'
;
import
{
IssuableType
}
from
'
~/issue_show/constants
'
;
import
{
__
}
from
'
~/locale
'
;
import
{
labelsQueries
}
from
'
~/sidebar/constants
'
;
import
createLabelMutation
from
'
./graphql/create_label.mutation.graphql
'
;
import
projectLabelsQuery
from
'
./graphql/project_labels.query.graphql
'
;
const
errorMessage
=
__
(
'
Error creating label.
'
);
...
...
@@ -19,10 +20,16 @@ export default {
GlTooltip
:
GlTooltipDirective
,
},
inject
:
{
project
Path
:
{
full
Path
:
{
default
:
''
,
},
},
props
:
{
issuableType
:
{
type
:
String
,
required
:
true
,
},
},
data
()
{
return
{
labelTitle
:
''
,
...
...
@@ -38,6 +45,19 @@ export default {
const
colorsMap
=
gon
.
suggested_label_colors
;
return
Object
.
keys
(
colorsMap
).
map
((
color
)
=>
({
[
color
]:
colorsMap
[
color
]
}));
},
mutationVariables
()
{
return
this
.
issuableType
===
IssuableType
.
Epic
?
{
title
:
this
.
labelTitle
,
color
:
this
.
selectedColor
,
groupPath
:
this
.
fullPath
,
}
:
{
title
:
this
.
labelTitle
,
color
:
this
.
selectedColor
,
projectPath
:
this
.
fullPath
,
};
},
},
methods
:
{
getColorCode
(
color
)
{
...
...
@@ -51,8 +71,8 @@ export default {
},
updateLabelsInCache
(
store
,
label
)
{
const
sourceData
=
store
.
readQuery
({
query
:
projectLabels
Query
,
variables
:
{
fullPath
:
this
.
project
Path
,
searchTerm
:
''
},
query
:
labelsQueries
[
this
.
issuableType
].
workspace
Query
,
variables
:
{
fullPath
:
this
.
full
Path
,
searchTerm
:
''
},
});
const
collator
=
new
Intl
.
Collator
(
'
en
'
);
...
...
@@ -63,8 +83,8 @@ export default {
});
store
.
writeQuery
({
query
:
projectLabels
Query
,
variables
:
{
fullPath
:
this
.
project
Path
,
searchTerm
:
''
},
query
:
labelsQueries
[
this
.
issuableType
].
workspace
Query
,
variables
:
{
fullPath
:
this
.
full
Path
,
searchTerm
:
''
},
data
,
});
},
...
...
@@ -75,11 +95,7 @@ export default {
data
:
{
labelCreate
},
}
=
await
this
.
$apollo
.
mutate
({
mutation
:
createLabelMutation
,
variables
:
{
title
:
this
.
labelTitle
,
color
:
this
.
selectedColor
,
projectPath
:
this
.
projectPath
,
},
variables
:
this
.
mutationVariables
,
update
:
(
store
,
{
...
...
app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue
View file @
81dd621c
<
script
>
import
{
GlDropdownForm
,
GlDropdownItem
,
GlLoadingIcon
,
GlSearchBoxByType
}
from
'
@gitlab/ui
'
;
import
{
GlDropdownForm
,
GlDropdownItem
,
GlLoadingIcon
,
GlSearchBoxByType
,
GlIntersectionObserver
,
}
from
'
@gitlab/ui
'
;
import
fuzzaldrinPlus
from
'
fuzzaldrin-plus
'
;
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
{
__
}
from
'
~/locale
'
;
import
projectLabelsQuery
from
'
./graphql/project_labels.query.graphql
'
;
import
{
labelsQueries
}
from
'
~/sidebar/constants
'
;
import
LabelItem
from
'
./label_item.vue
'
;
export
default
{
...
...
@@ -15,9 +21,13 @@ export default {
GlDropdownItem
,
GlLoadingIcon
,
GlSearchBoxByType
,
GlIntersectionObserver
,
LabelItem
,
},
inject
:
[
'
projectPath
'
],
inject
:
[
'
fullPath
'
],
model
:
{
prop
:
'
localSelectedLabels
'
,
},
props
:
{
selectedLabels
:
{
type
:
Array
,
...
...
@@ -27,20 +37,29 @@ export default {
type
:
Boolean
,
required
:
true
,
},
issuableType
:
{
type
:
String
,
required
:
true
,
},
localSelectedLabels
:
{
type
:
Array
,
required
:
true
,
},
},
data
()
{
return
{
searchKey
:
''
,
labels
:
[],
localSelectedLabels
:
[...
this
.
selectedLabels
],
};
},
apollo
:
{
labels
:
{
query
:
projectLabelsQuery
,
query
()
{
return
labelsQueries
[
this
.
issuableType
].
workspaceQuery
;
},
variables
()
{
return
{
fullPath
:
this
.
project
Path
,
fullPath
:
this
.
full
Path
,
searchTerm
:
this
.
searchKey
,
};
},
...
...
@@ -50,8 +69,8 @@ export default {
update
:
(
data
)
=>
data
.
workspace
?.
labels
?.
nodes
||
[],
async
result
()
{
if
(
this
.
$refs
.
searchInput
)
{
await
this
.
$nextTick
()
;
this
.
$refs
.
searchInput
.
focusInput
();
await
this
.
$nextTick
;
this
.
focusInputField
();
}
},
error
()
{
...
...
@@ -82,7 +101,6 @@ export default {
this
.
debouncedSearchKeyUpdate
=
debounce
(
this
.
setSearchKey
,
DEFAULT_DEBOUNCE_AND_THROTTLE_MS
);
},
beforeDestroy
()
{
this
.
$emit
(
'
setLabels
'
,
this
.
localSelectedLabels
);
this
.
debouncedSearchKeyUpdate
.
cancel
();
},
methods
:
{
...
...
@@ -109,16 +127,19 @@ export default {
}
},
updateSelectedLabels
(
label
)
{
let
labels
;
if
(
this
.
isLabelSelected
(
label
))
{
this
.
localSelectedLabels
=
this
.
localSelectedLabels
.
filter
(
({
id
})
=>
id
!==
getIdFromGraphQLId
(
label
.
id
),
);
labels
=
this
.
localSelectedLabels
.
filter
(({
id
})
=>
id
!==
getIdFromGraphQLId
(
label
.
id
));
}
else
{
this
.
localSelectedLabels
.
push
({
...
label
,
id
:
getIdFromGraphQLId
(
label
.
id
),
});
labels
=
[
...
this
.
localSelectedLabels
,
{
...
label
,
id
:
getIdFromGraphQLId
(
label
.
id
),
},
];
}
this
.
$emit
(
'
input
'
,
labels
);
},
handleLabelClick
(
label
)
{
this
.
updateSelectedLabels
(
label
);
...
...
@@ -129,46 +150,51 @@ export default {
setSearchKey
(
value
)
{
this
.
searchKey
=
value
;
},
focusInputField
()
{
this
.
$refs
.
searchInput
.
focusInput
();
},
},
};
</
script
>
<
template
>
<gl-dropdown-form
class=
"labels-select-contents-list js-labels-list"
>
<gl-search-box-by-type
ref=
"searchInput"
:value=
"searchKey"
:disabled=
"labelsFetchInProgress"
data-qa-selector=
"dropdown_input_field"
data-testid=
"dropdown-input-field"
@
input=
"debouncedSearchKeyUpdate"
/>
<div
ref=
"labelsListContainer"
data-testid=
"dropdown-content"
>
<gl-loading-icon
v-if=
"labelsFetchInProgress"
class=
"labels-fetch-loading gl-align-items-center gl-w-full gl-h-full"
size=
"md"
<gl-intersection-observer
@
appear=
"focusInputField"
>
<gl-dropdown-form
class=
"labels-select-contents-list js-labels-list"
>
<gl-search-box-by-type
ref=
"searchInput"
:value=
"searchKey"
:disabled=
"labelsFetchInProgress"
data-qa-selector=
"dropdown_input_field"
data-testid=
"dropdown-input-field"
@
input=
"debouncedSearchKeyUpdate"
/>
<template
v-else
>
<gl-dropdown-item
v-for=
"label in visibleLabels"
:key=
"label.id"
:is-checked=
"isLabelSelected(label)"
:is-check-centered=
"true"
:is-check-item=
"true"
data-testid=
"labels-list"
@
click.native.capture.stop=
"handleLabelClick(label)"
>
<label-item
:label=
"label"
/>
</gl-dropdown-item>
<gl-dropdown-item
v-show=
"showNoMatchingResultsMessage"
class=
"gl-p-3 gl-text-center"
data-testid=
"no-results"
>
{{
__
(
'
No matching results
'
)
}}
</gl-dropdown-item>
</
template
>
</div>
</gl-dropdown-form>
<div
ref=
"labelsListContainer"
data-testid=
"dropdown-content"
>
<gl-loading-icon
v-if=
"labelsFetchInProgress"
class=
"labels-fetch-loading gl-align-items-center gl-w-full gl-h-full gl-mb-3"
size=
"md"
/>
<template
v-else
>
<gl-dropdown-item
v-for=
"label in visibleLabels"
:key=
"label.id"
:is-checked=
"isLabelSelected(label)"
:is-check-centered=
"true"
:is-check-item=
"true"
data-testid=
"labels-list"
@
click.native.capture.stop=
"handleLabelClick(label)"
>
<label-item
:label=
"label"
/>
</gl-dropdown-item>
<gl-dropdown-item
v-show=
"showNoMatchingResultsMessage"
class=
"gl-p-3 gl-text-center"
data-testid=
"no-results"
>
{{
__
(
'
No matching results
'
)
}}
</gl-dropdown-item>
</
template
>
</div>
</gl-dropdown-form>
</gl-intersection-observer>
</template>
app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/epic_labels.query.graphql
0 → 100644
View file @
81dd621c
query
epicLabels
(
$fullPath
:
ID
!,
$iid
:
ID
)
{
workspace
:
group
(
fullPath
:
$fullPath
)
{
issuable
:
epic
(
iid
:
$iid
)
{
id
labels
{
nodes
{
id
title
color
description
}
}
}
}
}
app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/group_labels.query.graphql
0 → 100644
View file @
81dd621c
query
groupLabels
(
$fullPath
:
ID
!,
$searchTerm
:
String
)
{
workspace
:
group
(
fullPath
:
$fullPath
)
{
labels
(
searchTerm
:
$searchTerm
,
onlyGroupLabels
:
true
)
{
nodes
{
id
title
color
description
}
}
}
}
app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue
View file @
81dd621c
<
script
>
import
Vue
from
'
vue
'
;
import
Vuex
from
'
vuex
'
;
import
createFlash
from
'
~/flash
'
;
import
{
__
}
from
'
~/locale
'
;
import
SidebarEditableItem
from
'
~/sidebar/components/sidebar_editable_item.vue
'
;
import
{
labelsQueries
}
from
'
~/sidebar/constants
'
;
import
{
DropdownVariant
}
from
'
./constants
'
;
import
DropdownContents
from
'
./dropdown_contents.vue
'
;
import
DropdownValue
from
'
./dropdown_value.vue
'
;
import
DropdownValueCollapsed
from
'
./dropdown_value_collapsed.vue
'
;
import
issueLabelsQuery
from
'
./graphql/issue_labels.query.graphql
'
;
import
{
isDropdownVariantSidebar
,
isDropdownVariantStandalone
,
isDropdownVariantEmbedded
,
}
from
'
./utils
'
;
Vue
.
use
(
Vuex
);
export
default
{
components
:
{
DropdownValue
,
...
...
@@ -23,7 +20,15 @@ export default {
DropdownValueCollapsed
,
SidebarEditableItem
,
},
inject
:
[
'
iid
'
,
'
projectPath
'
,
'
allowLabelEdit
'
],
inject
:
{
iid
:
{
default
:
''
,
},
allowLabelEdit
:
{
default
:
false
,
},
fullPath
:
{},
},
props
:
{
allowLabelRemove
:
{
type
:
Boolean
,
...
...
@@ -90,43 +95,52 @@ export default {
required
:
false
,
default
:
false
,
},
issuableType
:
{
type
:
String
,
required
:
true
,
},
},
data
()
{
return
{
contentIsOnViewport
:
true
,
issueLabels
:
[],
issu
abl
eLabels
:
[],
};
},
computed
:
{
isLoading
()
{
return
this
.
labelsSelectInProgress
||
this
.
$apollo
.
queries
.
issuableLabels
.
loading
;
},
},
apollo
:
{
issueLabels
:
{
query
:
issueLabelsQuery
,
issuableLabels
:
{
query
()
{
return
labelsQueries
[
this
.
issuableType
].
issuableQuery
;
},
skip
()
{
return
!
isDropdownVariantSidebar
(
this
.
variant
);
},
variables
()
{
return
{
iid
:
this
.
iid
,
fullPath
:
this
.
project
Path
,
fullPath
:
this
.
full
Path
,
};
},
update
(
data
)
{
return
data
.
workspace
?.
issuable
?.
labels
.
nodes
||
[];
},
error
()
{
createFlash
({
message
:
__
(
'
Error fetching labels.
'
)
});
},
},
},
methods
:
{
handleDropdownClose
(
labels
)
{
if
(
labels
.
length
)
this
.
$emit
(
'
updateSelectedLabels
'
,
labels
);
this
.
$emit
(
'
onDropdownClose
'
);
},
collapseDropdown
()
{
this
.
$refs
.
editable
.
collapse
();
this
.
$emit
(
'
updateSelectedLabels
'
,
labels
);
this
.
$refs
.
editable
?.
collapse
();
},
handleCollapsedValueClick
()
{
this
.
$emit
(
'
toggleCollapse
'
);
},
showDropdown
()
{
this
.
$nextTick
(()
=>
{
this
.
$refs
.
dropdownContents
.
showDropdown
();
});
},
isDropdownVariantSidebar
,
isDropdownVariantStandalone
,
isDropdownVariantEmbedded
,
...
...
@@ -145,20 +159,19 @@ export default {
<template
v-if=
"isDropdownVariantSidebar(variant)"
>
<dropdown-value-collapsed
ref=
"dropdownButtonCollapsed"
:labels=
"issueLabels"
:labels=
"issu
abl
eLabels"
@
onValueClick=
"handleCollapsedValueClick"
/>
<sidebar-editable-item
ref=
"editable"
:title=
"__('Labels')"
:loading=
"
labelsSelectInProgress
"
:loading=
"
isLoading
"
:can-edit=
"allowLabelEdit"
@
open=
"showDropdown"
>
<template
#collapsed
>
<dropdown-value
:disable-labels=
"labelsSelectInProgress"
:selected-labels=
"issueLabels"
:selected-labels=
"issu
abl
eLabels"
:allow-label-remove=
"allowLabelRemove"
:labels-filter-base-path=
"labelsFilterBasePath"
:labels-filter-param=
"labelsFilterParam"
...
...
@@ -170,7 +183,7 @@ export default {
<
template
#default=
"{ edit }"
>
<dropdown-value
:disable-labels=
"labelsSelectInProgress"
:selected-labels=
"issueLabels"
:selected-labels=
"issu
abl
eLabels"
:allow-label-remove=
"allowLabelRemove"
:labels-filter-base-path=
"labelsFilterBasePath"
:labels-filter-param=
"labelsFilterParam"
...
...
@@ -181,7 +194,6 @@ export default {
</dropdown-value>
<dropdown-contents
v-if=
"edit"
ref=
"dropdownContents"
:dropdown-button-text=
"dropdownButtonText"
:allow-multiselect=
"allowMultiselect"
:labels-list-title=
"labelsListTitle"
...
...
@@ -190,11 +202,25 @@ export default {
:labels-create-title=
"labelsCreateTitle"
:selected-labels=
"selectedLabels"
:variant=
"variant"
@
closeDropdown=
"collapseDropdown
"
:issuable-type=
"issuableType
"
@
setLabels=
"handleDropdownClose"
/>
</
template
>
</sidebar-editable-item>
</template>
<dropdown-contents
v-else
ref=
"dropdownContents"
:allow-multiselect=
"allowMultiselect"
:dropdown-button-text=
"dropdownButtonText"
:labels-list-title=
"labelsListTitle"
:footer-create-label-title=
"footerCreateLabelTitle"
:footer-manage-label-title=
"footerManageLabelTitle"
:labels-create-title=
"labelsCreateTitle"
:selected-labels=
"selectedLabels"
:variant=
"variant"
:issuable-type=
"issuableType"
@
setLabels=
"handleDropdownClose"
/>
</div>
</template>
ee/app/assets/javascripts/epic/components/epic_form.vue
View file @
81dd621c
...
...
@@ -13,6 +13,8 @@ import { visitUrl } from '~/lib/utils/url_utility';
import
{
s__
}
from
'
~/locale
'
;
import
MarkdownField
from
'
~/vue_shared/components/markdown/field.vue
'
;
import
LabelsSelectVue
from
'
~/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue
'
;
import
LabelsSelectWidget
from
'
~/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue
'
;
import
glFeatureFlagMixin
from
'
~/vue_shared/mixins/gl_feature_flags_mixin
'
;
import
createEpic
from
'
../queries/createEpic.mutation.graphql
'
;
export
default
{
...
...
@@ -25,7 +27,9 @@ export default {
GlFormGroup
,
MarkdownField
,
LabelsSelectVue
,
LabelsSelectWidget
,
},
mixins
:
[
glFeatureFlagMixin
()],
inject
:
[
'
groupPath
'
,
'
groupEpicsPath
'
,
...
...
@@ -106,6 +110,11 @@ export default {
this
.
startDateFixed
=
val
;
},
handleUpdateSelectedLabels
(
labels
)
{
if
(
this
.
glFeatures
.
labelsWidget
)
{
this
.
labels
=
labels
;
return
;
}
const
ids
=
[];
const
allLabels
=
[...
labels
,
...
this
.
labels
];
...
...
@@ -177,7 +186,23 @@ export default {
</gl-form-group>
<hr
/>
<gl-form-group
:label=
"__('Labels')"
>
<labels-select-widget
v-if=
"glFeatures.labelsWidget"
class=
"block labels js-labels-block"
:allow-label-create=
"true"
:allow-multiselect=
"true"
:allow-scoped-labels=
"false"
:labels-filter-base-path=
"groupEpicsPath"
:selected-labels=
"labels"
issuable-type=
"epic"
variant=
"embedded"
data-qa-selector=
"labels_block"
@
updateSelectedLabels=
"handleUpdateSelectedLabels"
>
{{ __('None') }}
</labels-select-widget>
<labels-select-vue
v-else
:allow-label-edit=
"false"
:allow-label-create=
"true"
:allow-multiselect=
"true"
...
...
ee/app/assets/javascripts/epic/new_epic_bundle.js
View file @
81dd621c
...
...
@@ -35,6 +35,8 @@ export function initEpicForm() {
apolloProvider
,
provide
:
{
groupPath
,
fullPath
:
groupPath
,
allowLabelCreate
:
true
,
groupEpicsPath
,
labelsFetchPath
,
labelsManagePath
,
...
...
ee/app/controllers/groups/epics_controller.rb
View file @
81dd621c
...
...
@@ -23,6 +23,7 @@ class Groups::EpicsController < Groups::ApplicationController
before_action
do
push_frontend_feature_flag
(
:vue_epics_list
,
@group
,
type: :development
,
default_enabled: :yaml
)
push_frontend_feature_flag
(
:improved_emoji_picker
,
@group
,
type: :development
,
default_enabled: :yaml
)
push_frontend_feature_flag
(
:labels_widget
,
@group
,
default_enabled: :yaml
)
end
feature_category
:epics
...
...
lib/gitlab/email/message/in_product_marketing/base.rb
View file @
81dd621c
...
...
@@ -50,7 +50,7 @@ module Gitlab
def
cta_link
case
format
when
:html
link_to
cta_text
,
group_email_campaigns_url
(
group
,
track:
track
,
series:
series
),
target:
'_blank'
,
rel:
'noopener noreferrer'
ActionController
::
Base
.
helpers
.
link_to
cta_text
,
group_email_campaigns_url
(
group
,
track:
track
,
series:
series
),
target:
'_blank'
,
rel:
'noopener noreferrer'
else
[
cta_text
,
group_email_campaigns_url
(
group
,
track:
track
,
series:
series
)].
join
(
' >> '
)
end
...
...
@@ -89,7 +89,7 @@ module Gitlab
case
format
when
:html
links
.
map
do
|
text
,
link
|
link_to
(
text
,
link
)
ActionController
::
Base
.
helpers
.
link_to
(
text
,
link
)
end
else
'| '
+
links
.
map
do
|
text
,
link
|
...
...
lib/gitlab/email/message/in_product_marketing/helper.rb
View file @
81dd621c
...
...
@@ -7,7 +7,6 @@ module Gitlab
module
Helper
include
ActionView
::
Context
include
ActionView
::
Helpers
::
TagHelper
include
ActionView
::
Helpers
::
UrlHelper
private
...
...
@@ -32,7 +31,7 @@ module Gitlab
def
link
(
text
,
link
)
case
format
when
:html
link_to
text
,
link
ActionController
::
Base
.
helpers
.
link_to
text
,
link
else
"
#{
text
}
(
#{
link
}
)"
end
...
...
spec/frontend/sidebar/sidebar_labels_spec.js
View file @
81dd621c
...
...
@@ -110,10 +110,9 @@ describe('sidebar labels', () => {
mutation
:
updateIssueLabelsMutation
,
variables
:
{
input
:
{
addLabelIds
:
[
40
],
iid
:
defaultProps
.
iid
,
projectPath
:
defaultProps
.
projectPath
,
removeLabelIds
:
[
26
,
55
],
labelIds
:
[
toLabelGid
(
29
),
toLabelGid
(
28
),
toLabelGid
(
27
),
toLabelGid
(
40
)
],
},
},
};
...
...
spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view_spec.js
View file @
81dd621c
...
...
@@ -5,13 +5,14 @@ import VueApollo from 'vue-apollo';
import
createMockApollo
from
'
helpers/mock_apollo_helper
'
;
import
waitForPromises
from
'
helpers/wait_for_promises
'
;
import
createFlash
from
'
~/flash
'
;
import
{
IssuableType
}
from
'
~/issue_show/constants
'
;
import
{
labelsQueries
}
from
'
~/sidebar/constants
'
;
import
DropdownContentsCreateView
from
'
~/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue
'
;
import
createLabelMutation
from
'
~/vue_shared/components/sidebar/labels_select_widget/graphql/create_label.mutation.graphql
'
;
import
projectLabelsQuery
from
'
~/vue_shared/components/sidebar/labels_select_widget/graphql/project_labels.query.graphql
'
;
import
{
mockSuggestedColors
,
createLabelSuccessfulResponse
,
l
abelsQueryResponse
,
workspaceL
abelsQueryResponse
,
}
from
'
./mock_data
'
;
jest
.
mock
(
'
~/flash
'
);
...
...
@@ -47,11 +48,14 @@ describe('DropdownContentsCreateView', () => {
findAllColors
().
at
(
0
).
vm
.
$emit
(
'
click
'
,
new
Event
(
'
mouseclick
'
));
};
const
createComponent
=
({
mutationHandler
=
createLabelSuccessHandler
}
=
{})
=>
{
const
createComponent
=
({
mutationHandler
=
createLabelSuccessHandler
,
issuableType
=
IssuableType
.
Issue
,
}
=
{})
=>
{
const
mockApollo
=
createMockApollo
([[
createLabelMutation
,
mutationHandler
]]);
mockApollo
.
clients
.
defaultClient
.
cache
.
writeQuery
({
query
:
projectLabels
Query
,
data
:
l
abelsQueryResponse
.
data
,
query
:
labelsQueries
[
issuableType
].
workspace
Query
,
data
:
workspaceL
abelsQueryResponse
.
data
,
variables
:
{
fullPath
:
''
,
searchTerm
:
''
,
...
...
@@ -61,6 +65,9 @@ describe('DropdownContentsCreateView', () => {
wrapper
=
shallowMount
(
DropdownContentsCreateView
,
{
localVue
,
apolloProvider
:
mockApollo
,
propsData
:
{
issuableType
,
},
});
};
...
...
@@ -135,15 +142,6 @@ describe('DropdownContentsCreateView', () => {
expect
(
findCreateButton
().
props
(
'
disabled
'
)).
toBe
(
false
);
});
it
(
'
calls a mutation with correct parameters on Create button click
'
,
()
=>
{
findCreateButton
().
vm
.
$emit
(
'
click
'
);
expect
(
createLabelSuccessHandler
).
toHaveBeenCalledWith
({
color
:
'
#009966
'
,
projectPath
:
''
,
title
:
'
Test title
'
,
});
});
it
(
'
renders a loader spinner after Create button click
'
,
async
()
=>
{
findCreateButton
().
vm
.
$emit
(
'
click
'
);
await
nextTick
();
...
...
@@ -162,6 +160,30 @@ describe('DropdownContentsCreateView', () => {
});
});
it
(
'
calls a mutation with `projectPath` variable on the issue
'
,
()
=>
{
createComponent
();
fillLabelAttributes
();
findCreateButton
().
vm
.
$emit
(
'
click
'
);
expect
(
createLabelSuccessHandler
).
toHaveBeenCalledWith
({
color
:
'
#009966
'
,
projectPath
:
''
,
title
:
'
Test title
'
,
});
});
it
(
'
calls a mutation with `groupPath` variable on the epic
'
,
()
=>
{
createComponent
({
issuableType
:
IssuableType
.
Epic
});
fillLabelAttributes
();
findCreateButton
().
vm
.
$emit
(
'
click
'
);
expect
(
createLabelSuccessHandler
).
toHaveBeenCalledWith
({
color
:
'
#009966
'
,
groupPath
:
''
,
title
:
'
Test title
'
,
});
});
it
(
'
calls createFlash is mutation has a user-recoverable error
'
,
async
()
=>
{
createComponent
({
mutationHandler
:
createLabelUserRecoverableErrorHandler
});
fillLabelAttributes
();
...
...
spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view_spec.js
View file @
81dd621c
import
{
GlLoadingIcon
,
GlSearchBoxByType
}
from
'
@gitlab/ui
'
;
import
{
GlLoadingIcon
,
GlSearchBoxByType
,
GlDropdownItem
}
from
'
@gitlab/ui
'
;
import
{
shallowMount
,
createLocalVue
}
from
'
@vue/test-utils
'
;
import
{
nextTick
}
from
'
vue
'
;
import
VueApollo
from
'
vue-apollo
'
;
import
createMockApollo
from
'
helpers/mock_apollo_helper
'
;
import
waitForPromises
from
'
helpers/wait_for_promises
'
;
import
createFlash
from
'
~/flash
'
;
import
{
IssuableType
}
from
'
~/issue_show/constants
'
;
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
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
{
mockConfig
,
l
abelsQueryResponse
}
from
'
./mock_data
'
;
import
{
mockConfig
,
workspaceL
abelsQueryResponse
}
from
'
./mock_data
'
;
jest
.
mock
(
'
~/flash
'
);
const
localVue
=
createLocalVue
();
localVue
.
use
(
VueApollo
);
const
s
electedLabels
=
[
const
localS
electedLabels
=
[
{
id
:
28
,
title
:
'
Bug
'
,
description
:
'
Label for bugs
'
,
color
:
'
#FF0000
'
,
textColor
:
'
#FFFFFF
'
,
color
:
'
#2f7b2e
'
,
description
:
null
,
id
:
'
gid://gitlab/ProjectLabel/2
'
,
title
:
'
Label2
'
,
},
];
describe
(
'
DropdownContentsLabelsView
'
,
()
=>
{
let
wrapper
;
const
successfulQueryHandler
=
jest
.
fn
().
mockResolvedValue
(
labelsQueryResponse
);
const
successfulQueryHandler
=
jest
.
fn
().
mockResolvedValue
(
workspaceLabelsQueryResponse
);
const
findFirstLabel
=
()
=>
wrapper
.
findAllComponents
(
GlDropdownItem
).
at
(
0
);
const
createComponent
=
({
initialState
=
mockConfig
,
...
...
@@ -43,14 +45,15 @@ describe('DropdownContentsLabelsView', () => {
localVue
,
apolloProvider
:
mockApollo
,
provide
:
{
project
Path
:
'
test
'
,
full
Path
:
'
test
'
,
iid
:
1
,
variant
:
DropdownVariant
.
Sidebar
,
...
injected
,
},
propsData
:
{
...
initialState
,
selectedLabels
,
localSelectedLabels
,
issuableType
:
IssuableType
.
Issue
,
},
stubs
:
{
GlSearchBoxByType
,
...
...
@@ -129,6 +132,15 @@ describe('DropdownContentsLabelsView', () => {
createComponent
({
queryHandler
:
jest
.
fn
().
mockRejectedValue
(
'
Houston, we have a problem!
'
)
});
jest
.
advanceTimersByTime
(
DEFAULT_DEBOUNCE_AND_THROTTLE_MS
);
await
waitForPromises
();
expect
(
createFlash
).
toHaveBeenCalled
();
});
it
(
'
emits an `input` event on label click
'
,
async
()
=>
{
createComponent
();
await
waitForPromises
();
findFirstLabel
().
trigger
(
'
click
'
);
expect
(
wrapper
.
emitted
(
'
input
'
)[
0
][
0
]).
toEqual
(
expect
.
arrayContaining
(
localSelectedLabels
));
});
});
spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_spec.js
View file @
81dd621c
import
{
GlDropdown
}
from
'
@gitlab/ui
'
;
import
{
shallowMount
}
from
'
@vue/test-utils
'
;
import
{
nextTick
}
from
'
vue
'
;
import
{
DropdownVariant
}
from
'
~/vue_shared/components/sidebar/labels_select_widget/constants
'
;
import
DropdownContents
from
'
~/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue
'
;
import
DropdownContentsCreateView
from
'
~/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue
'
;
...
...
@@ -8,10 +7,25 @@ import DropdownContentsLabelsView from '~/vue_shared/components/sidebar/labels_s
import
{
mockLabels
}
from
'
./mock_data
'
;
const
showDropdown
=
jest
.
fn
();
const
GlDropdownStub
=
{
template
:
`
<div data-testid="dropdown">
<slot name="header"></slot>
<slot></slot>
<slot name="footer"></slot>
</div>
`
,
methods
:
{
show
:
showDropdown
,
},
};
describe
(
'
DropdownContent
'
,
()
=>
{
let
wrapper
;
const
createComponent
=
({
props
=
{},
injected
=
{}
}
=
{})
=>
{
const
createComponent
=
({
props
=
{},
injected
=
{}
,
data
=
{}
}
=
{})
=>
{
wrapper
=
shallowMount
(
DropdownContents
,
{
propsData
:
{
labelsCreateTitle
:
'
test
'
,
...
...
@@ -22,38 +36,76 @@ describe('DropdownContent', () => {
footerManageLabelTitle
:
'
manage
'
,
dropdownButtonText
:
'
Labels
'
,
variant
:
'
sidebar
'
,
issuableType
:
'
issue
'
,
...
props
,
},
data
()
{
return
{
...
data
,
};
},
provide
:
{
allowLabelCreate
:
true
,
labelsManagePath
:
'
foo/bar
'
,
...
injected
,
},
stubs
:
{
GlDropdown
,
GlDropdown
:
GlDropdownStub
,
},
});
};
beforeEach
(()
=>
{
createComponent
();
});
afterEach
(()
=>
{
wrapper
.
destroy
();
});
const
findCreateView
=
()
=>
wrapper
.
findComponent
(
DropdownContentsCreateView
);
const
findLabelsView
=
()
=>
wrapper
.
findComponent
(
DropdownContentsLabelsView
);
const
findDropdown
=
()
=>
wrapper
.
findComponent
(
GlDropdownStub
);
const
findDropdownFooter
=
()
=>
wrapper
.
find
(
'
[data-testid="dropdown-footer"]
'
);
const
findDropdownHeader
=
()
=>
wrapper
.
find
(
'
[data-testid="dropdown-header"]
'
);
const
findCreateLabelButton
=
()
=>
wrapper
.
find
(
'
[data-testid="create-label-button"]
'
);
const
findGoBackButton
=
()
=>
wrapper
.
find
(
'
[data-testid="go-back-button"]
'
);
it
(
'
calls dropdown `show` method on component mount
'
,
()
=>
{
createComponent
();
expect
(
showDropdown
).
toHaveBeenCalled
();
});
it
(
'
emits `setLabels` event on dropdown hide
'
,
()
=>
{
createComponent
();
findDropdown
().
vm
.
$emit
(
'
hide
'
);
expect
(
wrapper
.
emitted
(
'
setLabels
'
)).
toEqual
([[
mockLabels
]]);
});
it
(
'
does not render header on standalone variant
'
,
()
=>
{
createComponent
({
props
:
{
variant
:
DropdownVariant
.
Standalone
}
});
expect
(
findDropdownHeader
().
exists
()).
toBe
(
false
);
});
it
(
'
renders header on embedded variant
'
,
()
=>
{
createComponent
({
props
:
{
variant
:
DropdownVariant
.
Embedded
}
});
expect
(
findDropdownHeader
().
exists
()).
toBe
(
true
);
});
it
(
'
renders header on sidebar variant
'
,
()
=>
{
createComponent
();
expect
(
findDropdownHeader
().
exists
()).
toBe
(
true
);
});
describe
(
'
Create view
'
,
()
=>
{
beforeEach
(()
=>
{
wrapper
.
vm
.
toggleDropdownContentsCreateView
(
);
createComponent
({
data
:
{
showDropdownContentsCreateView
:
true
}
}
);
});
it
(
'
renders create view when `showDropdownContentsCreateView` prop is `true`
'
,
()
=>
{
expect
(
wrapper
.
findComponent
(
DropdownContentsCreateView
).
exists
()).
toBe
(
true
);
expect
(
findCreateView
(
).
exists
()).
toBe
(
true
);
});
it
(
'
does not render footer
'
,
()
=>
{
...
...
@@ -67,11 +119,31 @@ describe('DropdownContent', () => {
it
(
'
renders go back button
'
,
()
=>
{
expect
(
findGoBackButton
().
exists
()).
toBe
(
true
);
});
it
(
'
changes the view to Labels view on back button click
'
,
async
()
=>
{
findGoBackButton
().
vm
.
$emit
(
'
click
'
,
new
MouseEvent
(
'
click
'
));
await
nextTick
();
expect
(
findCreateView
().
exists
()).
toBe
(
false
);
expect
(
findLabelsView
().
exists
()).
toBe
(
true
);
});
it
(
'
changes the view to Labels view on `hideCreateView` event
'
,
async
()
=>
{
findCreateView
().
vm
.
$emit
(
'
hideCreateView
'
);
await
nextTick
();
expect
(
findCreateView
().
exists
()).
toBe
(
false
);
expect
(
findLabelsView
().
exists
()).
toBe
(
true
);
});
});
describe
(
'
Labels view
'
,
()
=>
{
beforeEach
(()
=>
{
createComponent
();
});
it
(
'
renders labels view when `showDropdownContentsCreateView` when `showDropdownContentsCreateView` prop is `false`
'
,
()
=>
{
expect
(
wrapper
.
findComponent
(
DropdownContentsLabelsView
).
exists
()).
toBe
(
true
);
expect
(
findLabelsView
(
).
exists
()).
toBe
(
true
);
});
it
(
'
renders footer on sidebar dropdown
'
,
()
=>
{
...
...
@@ -109,19 +181,12 @@ describe('DropdownContent', () => {
expect
(
findCreateLabelButton
().
exists
()).
toBe
(
true
);
});
it
(
'
triggers `toggleDropdownContent` method on create label button click
'
,
()
=>
{
jest
.
spyOn
(
wrapper
.
vm
,
'
toggleDropdownContent
'
).
mockImplementation
(()
=>
{});
it
(
'
changes the view to Create on create label button click
'
,
async
()
=>
{
findCreateLabelButton
().
trigger
(
'
click
'
);
expect
(
wrapper
.
vm
.
toggleDropdownContent
).
toHaveBeenCalled
();
await
nextTick
();
expect
(
findLabelsView
().
exists
()).
toBe
(
false
);
});
});
});
describe
(
'
template
'
,
()
=>
{
it
(
'
renders component container element with classes `gl-w-full gl-mt-2` and no styles
'
,
()
=>
{
expect
(
wrapper
.
attributes
(
'
class
'
)).
toContain
(
'
gl-w-full gl-mt-2
'
);
expect
(
wrapper
.
attributes
(
'
style
'
)).
toBeUndefined
();
});
});
});
spec/frontend/vue_shared/components/sidebar/labels_select_widget/labels_select_root_spec.js
View file @
81dd621c
import
{
shallowMount
}
from
'
@vue/test-utils
'
;
import
{
shallowMount
,
createLocalVue
}
from
'
@vue/test-utils
'
;
import
{
nextTick
}
from
'
vue
'
;
import
VueApollo
from
'
vue-apollo
'
;
import
createMockApollo
from
'
helpers/mock_apollo_helper
'
;
import
waitForPromises
from
'
helpers/wait_for_promises
'
;
import
createFlash
from
'
~/flash
'
;
import
{
IssuableType
}
from
'
~/issue_show/constants
'
;
import
SidebarEditableItem
from
'
~/sidebar/components/sidebar_editable_item.vue
'
;
import
DropdownContents
from
'
~/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue
'
;
import
DropdownValue
from
'
~/vue_shared/components/sidebar/labels_select_widget/dropdown_value.vue
'
;
import
DropdownValueCollapsed
from
'
~/vue_shared/components/sidebar/labels_select_widget/dropdown_value_collapsed.vue
'
;
import
issueLabelsQuery
from
'
~/vue_shared/components/sidebar/labels_select_widget/graphql/issue_labels.query.graphql
'
;
import
LabelsSelectRoot
from
'
~/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue
'
;
import
{
mockConfig
,
issuableLabelsQueryResponse
}
from
'
./mock_data
'
;
import
{
mockConfig
}
from
'
./mock_data
'
;
jest
.
mock
(
'
~/flash
'
);
const
localVue
=
createLocalVue
();
localVue
.
use
(
VueApollo
);
const
successfulQueryHandler
=
jest
.
fn
().
mockResolvedValue
(
issuableLabelsQueryResponse
);
const
errorQueryHandler
=
jest
.
fn
().
mockRejectedValue
(
'
Houston, we have a problem
'
);
describe
(
'
LabelsSelectRoot
'
,
()
=>
{
let
wrapper
;
const
createComponent
=
(
config
=
mockConfig
,
slots
=
{})
=>
{
const
findSidebarEditableItem
=
()
=>
wrapper
.
findComponent
(
SidebarEditableItem
);
const
findDropdownValue
=
()
=>
wrapper
.
findComponent
(
DropdownValue
);
const
findDropdownContents
=
()
=>
wrapper
.
findComponent
(
DropdownContents
);
const
expandDropdown
=
()
=>
wrapper
.
vm
.
$refs
.
editable
.
expand
();
const
createComponent
=
({
config
=
mockConfig
,
slots
=
{},
queryHandler
=
successfulQueryHandler
,
}
=
{})
=>
{
const
mockApollo
=
createMockApollo
([[
issueLabelsQuery
,
queryHandler
]]);
wrapper
=
shallowMount
(
LabelsSelectRoot
,
{
slots
,
propsData
:
config
,
apolloProvider
:
mockApollo
,
localVue
,
propsData
:
{
...
config
,
issuableType
:
IssuableType
.
Issue
,
},
stubs
:
{
DropdownContents
,
SidebarEditableItem
,
},
provide
:
{
iid
:
'
1
'
,
project
Path
:
'
test
'
,
full
Path
:
'
test
'
,
canUpdate
:
true
,
allowLabelEdit
:
true
,
allowLabelCreate
:
true
,
labelsManagePath
:
'
test
'
,
},
});
};
...
...
@@ -42,33 +73,67 @@ describe('LabelsSelectRoot', () => {
${
'
embedded
'
}
|
${
'
is-embedded
'
}
`
(
'
renders component root element with CSS class `$cssClass` when `state.variant` is "$variant"
'
,
({
variant
,
cssClass
})
=>
{
async
({
variant
,
cssClass
})
=>
{
createComponent
({
...
mockConfig
,
variant
,
config
:
{
...
mockConfig
,
variant
},
});
return
wrapper
.
vm
.
$nextTick
(()
=>
{
expect
(
wrapper
.
classes
()).
toContain
(
cssClass
);
});
await
nextTick
();
expect
(
wrapper
.
classes
()).
toContain
(
cssClass
);
},
);
it
(
'
renders `dropdown-value-collapsed` component when `allowLabelCreate` prop is `true`
'
,
async
()
=>
{
createComponent
();
await
wrapper
.
vm
.
$nextTick
;
expect
(
wrapper
.
find
(
DropdownValueCollapsed
).
exists
()).
toBe
(
true
);
});
describe
(
'
if dropdown variant is `sidebar`
'
,
()
=>
{
it
(
'
renders sidebar editable item
'
,
()
=>
{
createComponent
();
expect
(
findSidebarEditableItem
().
exists
()).
toBe
(
true
);
});
it
(
'
passes true `loading` prop to sidebar editable item when loading labels
'
,
()
=>
{
createComponent
();
expect
(
findSidebarEditableItem
().
props
(
'
loading
'
)).
toBe
(
true
);
});
it
(
'
renders `dropdown-value` component
'
,
async
()
=>
{
createComponent
(
mockConfig
,
{
default
:
'
None
'
,
describe
(
'
when labels are fetched successfully
'
,
()
=>
{
beforeEach
(
async
()
=>
{
createComponent
();
await
waitForPromises
();
});
it
(
'
passes true `loading` prop to sidebar editable item
'
,
()
=>
{
expect
(
findSidebarEditableItem
().
props
(
'
loading
'
)).
toBe
(
false
);
});
it
(
'
renders dropdown value component when query labels is resolved
'
,
()
=>
{
expect
(
findDropdownValue
().
exists
()).
toBe
(
true
);
expect
(
findDropdownValue
().
props
(
'
selectedLabels
'
)).
toEqual
(
issuableLabelsQueryResponse
.
data
.
workspace
.
issuable
.
labels
.
nodes
,
);
});
it
(
'
emits `onLabelRemove` event on dropdown value label remove event
'
,
()
=>
{
const
label
=
{
id
:
'
gid://gitlab/ProjectLabel/1
'
};
findDropdownValue
().
vm
.
$emit
(
'
onLabelRemove
'
,
label
);
expect
(
wrapper
.
emitted
(
'
onLabelRemove
'
)).
toEqual
([[
label
]]);
});
});
it
(
'
creates flash with error message when query is rejected
'
,
async
()
=>
{
createComponent
({
queryHandler
:
errorQueryHandler
});
await
waitForPromises
();
expect
(
createFlash
).
toHaveBeenCalledWith
({
message
:
'
Error fetching labels.
'
});
});
await
wrapper
.
vm
.
$nextTick
;
});
it
(
'
emits `updateSelectedLabels` event on dropdown contents `setLabels` event if there are labels to update
'
,
async
()
=>
{
const
label
=
{
id
:
'
gid://gitlab/ProjectLabel/1
'
};
createComponent
();
await
waitForPromises
();
const
valueComp
=
wrapper
.
find
(
DropdownValue
);
expandDropdown
();
await
nextTick
();
expect
(
valueComp
.
exists
()).
toBe
(
true
);
expect
(
valueComp
.
text
()).
toBe
(
'
None
'
);
findDropdownContents
().
vm
.
$emit
(
'
setLabels
'
,
[
label
]
);
expect
(
wrapper
.
emitted
(
'
updateSelectedLabels
'
)).
toEqual
([[[
label
]]]
);
});
});
spec/frontend/vue_shared/components/sidebar/labels_select_widget/mock_data.js
View file @
81dd621c
...
...
@@ -86,7 +86,7 @@ export const createLabelSuccessfulResponse = {
},
};
export
const
l
abelsQueryResponse
=
{
export
const
workspaceL
abelsQueryResponse
=
{
data
:
{
workspace
:
{
labels
:
{
...
...
@@ -108,3 +108,23 @@ export const labelsQueryResponse = {
},
},
};
export
const
issuableLabelsQueryResponse
=
{
data
:
{
workspace
:
{
issuable
:
{
id
:
'
1
'
,
labels
:
{
nodes
:
[
{
color
:
'
#330066
'
,
description
:
null
,
id
:
'
gid://gitlab/ProjectLabel/1
'
,
title
:
'
Label1
'
,
},
],
},
},
},
},
};
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