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
cb2e71a7
Commit
cb2e71a7
authored
Jun 02, 2021
by
Natalia Tepluhina
Committed by
David O'Regan
Jun 02, 2021
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Introduce a feature flag for labels widget [RUN ALL RSPEC] [RUN AS-IF-FOSS]
parent
8aa0d876
Changes
18
Hide whitespace changes
Inline
Side-by-side
Showing
18 changed files
with
1225 additions
and
0 deletions
+1225
-0
app/assets/javascripts/sidebar/components/labels/sidebar_labels.vue
.../javascripts/sidebar/components/labels/sidebar_labels.vue
+29
-0
app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/constants.js
...ared/components/sidebar/labels_select_widget/constants.js
+5
-0
app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_button.vue
...mponents/sidebar/labels_select_widget/dropdown_button.vue
+42
-0
app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue
...onents/sidebar/labels_select_widget/dropdown_contents.vue
+44
-0
app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue
...ar/labels_select_widget/dropdown_contents_create_view.vue
+119
-0
app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue
...ar/labels_select_widget/dropdown_contents_labels_view.vue
+221
-0
app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_title.vue
...omponents/sidebar/labels_select_widget/dropdown_title.vue
+39
-0
app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_value.vue
...omponents/sidebar/labels_select_widget/dropdown_value.vue
+67
-0
app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/label_item.vue
...ed/components/sidebar/labels_select_widget/label_item.vue
+82
-0
app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue
...nents/sidebar/labels_select_widget/labels_select_root.vue
+327
-0
app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/store/actions.js
.../components/sidebar/labels_select_widget/store/actions.js
+58
-0
app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/store/getters.js
.../components/sidebar/labels_select_widget/store/getters.js
+52
-0
app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/store/index.js
...ed/components/sidebar/labels_select_widget/store/index.js
+12
-0
app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/store/mutation_types.js
...ents/sidebar/labels_select_widget/store/mutation_types.js
+20
-0
app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/store/mutations.js
...omponents/sidebar/labels_select_widget/store/mutations.js
+70
-0
app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/store/state.js
...ed/components/sidebar/labels_select_widget/store/state.js
+29
-0
app/controllers/projects/issues_controller.rb
app/controllers/projects/issues_controller.rb
+1
-0
config/feature_flags/development/labels_widget.yml
config/feature_flags/development/labels_widget.yml
+8
-0
No files found.
app/assets/javascripts/sidebar/components/labels/sidebar_labels.vue
View file @
cb2e71a7
...
...
@@ -10,6 +10,8 @@ import updateMergeRequestLabelsMutation from '~/sidebar/queries/update_merge_req
import
{
toLabelGid
}
from
'
~/sidebar/utils
'
;
import
{
DropdownVariant
}
from
'
~/vue_shared/components/sidebar/labels_select_vue/constants
'
;
import
LabelsSelect
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
'
;
const
mutationMap
=
{
[
IssuableType
.
Issue
]:
{
...
...
@@ -25,8 +27,10 @@ const mutationMap = {
export
default
{
components
:
{
LabelsSelect
,
LabelsSelectWidget
,
},
variant
:
DropdownVariant
.
Sidebar
,
mixins
:
[
glFeatureFlagMixin
()],
inject
:
[
'
allowLabelCreate
'
,
'
allowLabelEdit
'
,
...
...
@@ -135,7 +139,32 @@ export default {
</
script
>
<
template
>
<labels-select-widget
v-if=
"glFeatures.labelsWidget"
class=
"block labels js-labels-block"
:allow-label-remove=
"allowLabelEdit"
:allow-label-create=
"allowLabelCreate"
:allow-label-edit=
"allowLabelEdit"
:allow-multiselect=
"true"
:allow-scoped-labels=
"allowScopedLabels"
:footer-create-label-title=
"__('Create project label')"
:footer-manage-label-title=
"__('Manage project labels')"
:labels-create-title=
"__('Create project label')"
:labels-fetch-path=
"labelsFetchPath"
:labels-filter-base-path=
"projectIssuesPath"
:labels-manage-path=
"labelsManagePath"
:labels-select-in-progress=
"isLabelsSelectInProgress"
:selected-labels=
"selectedLabels"
:variant=
"$options.sidebar"
data-qa-selector=
"labels_block"
@
onDropdownClose=
"handleDropdownClose"
@
onLabelRemove=
"handleLabelRemove"
@
updateSelectedLabels=
"handleUpdateSelectedLabels"
>
{{
__
(
'
None
'
)
}}
</labels-select-widget>
<labels-select
v-else
class=
"block labels js-labels-block"
:allow-label-remove=
"allowLabelEdit"
:allow-label-create=
"allowLabelCreate"
...
...
app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/constants.js
0 → 100644
View file @
cb2e71a7
export
const
DropdownVariant
=
{
Sidebar
:
'
sidebar
'
,
Standalone
:
'
standalone
'
,
Embedded
:
'
embedded
'
,
};
app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_button.vue
0 → 100644
View file @
cb2e71a7
<
script
>
import
{
GlButton
,
GlIcon
}
from
'
@gitlab/ui
'
;
import
{
mapActions
,
mapGetters
}
from
'
vuex
'
;
export
default
{
components
:
{
GlButton
,
GlIcon
,
},
computed
:
{
...
mapGetters
([
'
dropdownButtonText
'
,
'
isDropdownVariantStandalone
'
,
'
isDropdownVariantEmbedded
'
,
]),
},
methods
:
{
...
mapActions
([
'
toggleDropdownContents
'
]),
handleButtonClick
(
e
)
{
if
(
this
.
isDropdownVariantStandalone
||
this
.
isDropdownVariantEmbedded
)
{
this
.
toggleDropdownContents
();
}
if
(
this
.
isDropdownVariantStandalone
)
{
e
.
stopPropagation
();
}
},
},
};
</
script
>
<
template
>
<gl-button
class=
"labels-select-dropdown-button js-dropdown-button w-100 text-left"
@
click=
"handleButtonClick"
>
<span
class=
"dropdown-toggle-text gl-pointer-events-none flex-fill"
>
{{
dropdownButtonText
}}
</span>
<gl-icon
name=
"chevron-down"
class=
"gl-pointer-events-none float-right"
/>
</gl-button>
</
template
>
app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue
0 → 100644
View file @
cb2e71a7
<
script
>
import
{
mapGetters
,
mapState
}
from
'
vuex
'
;
import
DropdownContentsCreateView
from
'
./dropdown_contents_create_view.vue
'
;
import
DropdownContentsLabelsView
from
'
./dropdown_contents_labels_view.vue
'
;
export
default
{
components
:
{
DropdownContentsLabelsView
,
DropdownContentsCreateView
,
},
props
:
{
renderOnTop
:
{
type
:
Boolean
,
required
:
false
,
default
:
false
,
},
},
computed
:
{
...
mapState
([
'
showDropdownContentsCreateView
'
]),
...
mapGetters
([
'
isDropdownVariantSidebar
'
]),
dropdownContentsView
()
{
if
(
this
.
showDropdownContentsCreateView
)
{
return
'
dropdown-contents-create-view
'
;
}
return
'
dropdown-contents-labels-view
'
;
},
directionStyle
()
{
const
bottom
=
this
.
isDropdownVariantSidebar
?
'
3rem
'
:
'
2rem
'
;
return
this
.
renderOnTop
?
{
bottom
}
:
{};
},
},
};
</
script
>
<
template
>
<div
class=
"labels-select-dropdown-contents gl-w-full gl-my-2 gl-py-3 gl-rounded-base gl-absolute"
data-qa-selector=
"labels_dropdown_content"
:style=
"directionStyle"
>
<component
:is=
"dropdownContentsView"
/>
</div>
</
template
>
app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue
0 → 100644
View file @
cb2e71a7
<
script
>
import
{
GlTooltipDirective
,
GlButton
,
GlFormInput
,
GlLink
,
GlLoadingIcon
}
from
'
@gitlab/ui
'
;
import
{
mapState
,
mapActions
}
from
'
vuex
'
;
export
default
{
components
:
{
GlButton
,
GlFormInput
,
GlLink
,
GlLoadingIcon
,
},
directives
:
{
GlTooltip
:
GlTooltipDirective
,
},
data
()
{
return
{
labelTitle
:
''
,
selectedColor
:
''
,
};
},
computed
:
{
...
mapState
([
'
labelsCreateTitle
'
,
'
labelCreateInProgress
'
]),
disableCreate
()
{
return
!
this
.
labelTitle
.
length
||
!
this
.
selectedColor
.
length
||
this
.
labelCreateInProgress
;
},
suggestedColors
()
{
const
colorsMap
=
gon
.
suggested_label_colors
;
return
Object
.
keys
(
colorsMap
).
map
((
color
)
=>
({
[
color
]:
colorsMap
[
color
]
}));
},
},
methods
:
{
...
mapActions
([
'
toggleDropdownContents
'
,
'
toggleDropdownContentsCreateView
'
,
'
createLabel
'
]),
getColorCode
(
color
)
{
return
Object
.
keys
(
color
).
pop
();
},
getColorName
(
color
)
{
return
Object
.
values
(
color
).
pop
();
},
handleColorClick
(
color
)
{
this
.
selectedColor
=
this
.
getColorCode
(
color
);
},
handleCreateClick
()
{
this
.
createLabel
({
title
:
this
.
labelTitle
,
color
:
this
.
selectedColor
,
});
},
},
};
</
script
>
<
template
>
<div
class=
"labels-select-contents-create js-labels-create"
>
<div
class=
"dropdown-title d-flex align-items-center pt-0 pb-2"
>
<gl-button
:aria-label=
"__('Go back')"
variant=
"link"
size=
"small"
class=
"js-btn-back dropdown-header-button p-0"
icon=
"arrow-left"
@
click=
"toggleDropdownContentsCreateView"
/>
<span
class=
"flex-grow-1"
>
{{
labelsCreateTitle
}}
</span>
<gl-button
:aria-label=
"__('Close')"
variant=
"link"
size=
"small"
class=
"dropdown-header-button p-0"
icon=
"close"
@
click=
"toggleDropdownContents"
/>
</div>
<div
class=
"dropdown-input"
>
<gl-form-input
v-model.trim=
"labelTitle"
:placeholder=
"__('Name new label')"
:autofocus=
"true"
/>
</div>
<div
class=
"dropdown-content px-2"
>
<div
class=
"suggest-colors suggest-colors-dropdown mt-0 mb-2"
>
<gl-link
v-for=
"(color, index) in suggestedColors"
:key=
"index"
v-gl-tooltip:tooltipcontainer
:style=
"
{ backgroundColor: getColorCode(color) }"
:title="getColorName(color)"
@click.prevent="handleColorClick(color)"
/>
</div>
<div
class=
"color-input-container gl-display-flex"
>
<span
class=
"dropdown-label-color-preview position-relative position-relative d-inline-block"
:style=
"
{ backgroundColor: selectedColor }"
>
</span>
<gl-form-input
v-model.trim=
"selectedColor"
class=
"gl-rounded-top-left-none gl-rounded-bottom-left-none"
:placeholder=
"__('Use custom color #FF0000')"
/>
</div>
</div>
<div
class=
"dropdown-actions clearfix pt-2 px-2"
>
<gl-button
:disabled=
"disableCreate"
category=
"primary"
variant=
"success"
class=
"float-left d-flex align-items-center"
@
click=
"handleCreateClick"
>
<gl-loading-icon
v-show=
"labelCreateInProgress"
:inline=
"true"
class=
"mr-1"
/>
{{
__
(
'
Create
'
)
}}
</gl-button>
<gl-button
class=
"float-right js-btn-cancel-create"
@
click=
"toggleDropdownContentsCreateView"
>
{{
__
(
'
Cancel
'
)
}}
</gl-button>
</div>
</div>
</
template
>
app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue
0 → 100644
View file @
cb2e71a7
<
script
>
import
{
GlIntersectionObserver
,
GlLoadingIcon
,
GlButton
,
GlSearchBoxByType
,
GlLink
,
}
from
'
@gitlab/ui
'
;
import
fuzzaldrinPlus
from
'
fuzzaldrin-plus
'
;
import
{
mapState
,
mapGetters
,
mapActions
}
from
'
vuex
'
;
import
{
UP_KEY_CODE
,
DOWN_KEY_CODE
,
ENTER_KEY_CODE
,
ESC_KEY_CODE
}
from
'
~/lib/utils/keycodes
'
;
import
LabelItem
from
'
./label_item.vue
'
;
export
default
{
components
:
{
GlIntersectionObserver
,
GlLoadingIcon
,
GlButton
,
GlSearchBoxByType
,
GlLink
,
LabelItem
,
},
data
()
{
return
{
searchKey
:
''
,
currentHighlightItem
:
-
1
,
};
},
computed
:
{
...
mapState
([
'
allowLabelCreate
'
,
'
allowMultiselect
'
,
'
labelsManagePath
'
,
'
labels
'
,
'
labelsFetchInProgress
'
,
'
labelsListTitle
'
,
'
footerCreateLabelTitle
'
,
'
footerManageLabelTitle
'
,
]),
...
mapGetters
([
'
selectedLabelsList
'
,
'
isDropdownVariantSidebar
'
,
'
isDropdownVariantEmbedded
'
]),
visibleLabels
()
{
if
(
this
.
searchKey
)
{
return
fuzzaldrinPlus
.
filter
(
this
.
labels
,
this
.
searchKey
,
{
key
:
[
'
title
'
],
});
}
return
this
.
labels
;
},
showNoMatchingResultsMessage
()
{
return
Boolean
(
this
.
searchKey
)
&&
this
.
visibleLabels
.
length
===
0
;
},
},
watch
:
{
searchKey
(
value
)
{
// When there is search string present
// and there are matching results,
// highlight first item by default.
if
(
value
&&
this
.
visibleLabels
.
length
)
{
this
.
currentHighlightItem
=
0
;
}
},
},
methods
:
{
...
mapActions
([
'
toggleDropdownContents
'
,
'
toggleDropdownContentsCreateView
'
,
'
fetchLabels
'
,
'
receiveLabelsSuccess
'
,
'
updateSelectedLabels
'
,
'
toggleDropdownContents
'
,
]),
isLabelSelected
(
label
)
{
return
this
.
selectedLabelsList
.
includes
(
label
.
id
);
},
/**
* This method scrolls item from dropdown into
* the view if it is off the viewable area of the
* container.
*/
scrollIntoViewIfNeeded
()
{
const
highlightedLabel
=
this
.
$refs
.
labelsListContainer
.
querySelector
(
'
.is-focused
'
);
if
(
highlightedLabel
)
{
const
container
=
this
.
$refs
.
labelsListContainer
.
getBoundingClientRect
();
const
label
=
highlightedLabel
.
getBoundingClientRect
();
if
(
label
.
bottom
>
container
.
bottom
)
{
this
.
$refs
.
labelsListContainer
.
scrollTop
+=
label
.
bottom
-
container
.
bottom
;
}
else
if
(
label
.
top
<
container
.
top
)
{
this
.
$refs
.
labelsListContainer
.
scrollTop
-=
container
.
top
-
label
.
top
;
}
}
},
handleComponentAppear
()
{
// We can avoid putting `catch` block here
// as failure is handled within actions.js already.
return
this
.
fetchLabels
().
then
(()
=>
{
this
.
$refs
.
searchInput
.
focusInput
();
});
},
/**
* 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
* the dropdown.
*/
handleKeyDown
(
e
)
{
if
(
e
.
keyCode
===
UP_KEY_CODE
&&
this
.
currentHighlightItem
>
0
)
{
this
.
currentHighlightItem
-=
1
;
}
else
if
(
e
.
keyCode
===
DOWN_KEY_CODE
&&
this
.
currentHighlightItem
<
this
.
visibleLabels
.
length
-
1
)
{
this
.
currentHighlightItem
+=
1
;
}
else
if
(
e
.
keyCode
===
ENTER_KEY_CODE
&&
this
.
currentHighlightItem
>
-
1
)
{
this
.
updateSelectedLabels
([
this
.
visibleLabels
[
this
.
currentHighlightItem
]]);
this
.
searchKey
=
''
;
}
else
if
(
e
.
keyCode
===
ESC_KEY_CODE
)
{
this
.
toggleDropdownContents
();
}
if
(
e
.
keyCode
!==
ESC_KEY_CODE
)
{
// Scroll the list only after highlighting
// styles are rendered completely.
this
.
$nextTick
(()
=>
{
this
.
scrollIntoViewIfNeeded
();
});
}
},
handleLabelClick
(
label
)
{
this
.
updateSelectedLabels
([
label
]);
if
(
!
this
.
allowMultiselect
)
this
.
toggleDropdownContents
();
},
},
};
</
script
>
<
template
>
<gl-intersection-observer
@
appear=
"handleComponentAppear"
@
disappear=
"handleComponentDisappear"
>
<div
class=
"labels-select-contents-list js-labels-list"
@
keydown=
"handleKeyDown"
>
<div
v-if=
"isDropdownVariantSidebar || isDropdownVariantEmbedded"
class=
"dropdown-title gl-display-flex gl-align-items-center gl-pt-0 gl-pb-3!"
data-testid=
"dropdown-title"
>
<span
class=
"flex-grow-1"
>
{{
labelsListTitle
}}
</span>
<gl-button
:aria-label=
"__('Close')"
variant=
"link"
size=
"small"
class=
"dropdown-header-button gl-p-0!"
icon=
"close"
@
click=
"toggleDropdownContents"
/>
</div>
<div
class=
"dropdown-input"
@
click.stop=
"() =>
{}">
<gl-search-box-by-type
ref=
"searchInput"
v-model=
"searchKey"
:disabled=
"labelsFetchInProgress"
data-qa-selector=
"dropdown_input_field"
/>
</div>
<div
ref=
"labelsListContainer"
class=
"dropdown-content"
data-testid=
"dropdown-content"
>
<gl-loading-icon
v-if=
"labelsFetchInProgress"
class=
"labels-fetch-loading gl-align-items-center w-100 h-100"
size=
"md"
/>
<ul
v-else
class=
"list-unstyled gl-mb-0 gl-word-break-word"
>
<label-item
v-for=
"(label, index) in visibleLabels"
:key=
"label.id"
:label=
"label"
:is-label-set=
"label.set"
:highlight=
"index === currentHighlightItem"
@
clickLabel=
"handleLabelClick(label)"
/>
<li
v-show=
"showNoMatchingResultsMessage"
class=
"gl-p-3 gl-text-center"
>
{{
__
(
'
No matching results
'
)
}}
</li>
</ul>
</div>
<div
v-if=
"isDropdownVariantSidebar || isDropdownVariantEmbedded"
class=
"dropdown-footer"
data-testid=
"dropdown-footer"
>
<ul
class=
"list-unstyled"
>
<li
v-if=
"allowLabelCreate"
>
<gl-link
class=
"gl-display-flex w-100 flex-row text-break-word label-item"
@
click=
"handleCreateLabelClick"
>
{{
footerCreateLabelTitle
}}
</gl-link>
</li>
<li>
<gl-link
:href=
"labelsManagePath"
class=
"gl-display-flex flex-row text-break-word label-item"
>
{{
footerManageLabelTitle
}}
</gl-link>
</li>
</ul>
</div>
</div>
</gl-intersection-observer>
</
template
>
app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_title.vue
0 → 100644
View file @
cb2e71a7
<
script
>
import
{
GlButton
,
GlLoadingIcon
}
from
'
@gitlab/ui
'
;
import
{
mapState
,
mapActions
}
from
'
vuex
'
;
export
default
{
components
:
{
GlButton
,
GlLoadingIcon
,
},
props
:
{
labelsSelectInProgress
:
{
type
:
Boolean
,
required
:
true
,
},
},
computed
:
{
...
mapState
([
'
allowLabelEdit
'
,
'
labelsFetchInProgress
'
]),
},
methods
:
{
...
mapActions
([
'
toggleDropdownContents
'
]),
},
};
</
script
>
<
template
>
<div
class=
"title hide-collapsed gl-mb-3"
>
{{
__
(
'
Labels
'
)
}}
<template
v-if=
"allowLabelEdit"
>
<gl-loading-icon
v-show=
"labelsSelectInProgress"
inline
/>
<gl-button
variant=
"link"
class=
"float-right js-sidebar-dropdown-toggle"
data-qa-selector=
"labels_edit_button"
@
click=
"toggleDropdownContents"
>
{{
__
(
'
Edit
'
)
}}
</gl-button
>
</
template
>
</div>
</template>
app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_value.vue
0 → 100644
View file @
cb2e71a7
<
script
>
import
{
GlLabel
}
from
'
@gitlab/ui
'
;
import
{
mapState
}
from
'
vuex
'
;
import
{
isScopedLabel
}
from
'
~/lib/utils/common_utils
'
;
export
default
{
components
:
{
GlLabel
,
},
props
:
{
disableLabels
:
{
type
:
Boolean
,
required
:
false
,
default
:
false
,
},
},
computed
:
{
...
mapState
([
'
selectedLabels
'
,
'
allowLabelRemove
'
,
'
allowScopedLabels
'
,
'
labelsFilterBasePath
'
,
'
labelsFilterParam
'
,
]),
},
methods
:
{
labelFilterUrl
(
label
)
{
return
`
${
this
.
labelsFilterBasePath
}
?
${
this
.
labelsFilterParam
}
[]=
${
encodeURIComponent
(
label
.
title
,
)}
`
;
},
scopedLabel
(
label
)
{
return
this
.
allowScopedLabels
&&
isScopedLabel
(
label
);
},
},
};
</
script
>
<
template
>
<div
:class=
"
{
'has-labels': selectedLabels.length,
}"
class="hide-collapsed value issuable-show-labels js-value"
>
<span
v-if=
"!selectedLabels.length"
class=
"text-secondary"
>
<slot></slot>
</span>
<template
v-for=
"label in selectedLabels"
v-else
>
<gl-label
:key=
"label.id"
data-qa-selector=
"selected_label_content"
:data-qa-label-name=
"label.title"
:title=
"label.title"
:description=
"label.description"
:background-color=
"label.color"
:target=
"labelFilterUrl(label)"
:scoped=
"scopedLabel(label)"
:show-close-button=
"allowLabelRemove"
:disabled=
"disableLabels"
tooltip-placement=
"top"
@
close=
"$emit('onLabelRemove', label.id)"
/>
</
template
>
</div>
</template>
app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/label_item.vue
0 → 100644
View file @
cb2e71a7
<
script
>
import
{
GlLink
,
GlIcon
}
from
'
@gitlab/ui
'
;
export
default
{
functional
:
true
,
props
:
{
label
:
{
type
:
Object
,
required
:
true
,
},
isLabelSet
:
{
type
:
Boolean
,
required
:
true
,
},
highlight
:
{
type
:
Boolean
,
required
:
false
,
default
:
false
,
},
},
render
(
h
,
{
props
,
listeners
})
{
const
{
label
,
highlight
,
isLabelSet
}
=
props
;
const
labelColorBox
=
h
(
'
span
'
,
{
class
:
'
dropdown-label-box gl-flex-shrink-0 gl-top-0 gl-mr-3
'
,
style
:
{
backgroundColor
:
label
.
color
,
},
attrs
:
{
'
data-testid
'
:
'
label-color-box
'
,
},
});
const
checkedIcon
=
h
(
GlIcon
,
{
class
:
{
'
gl-mr-3 gl-flex-shrink-0
'
:
true
,
hidden
:
!
isLabelSet
,
},
props
:
{
name
:
'
mobile-issue-close
'
,
},
});
const
noIcon
=
h
(
'
span
'
,
{
class
:
{
'
gl-mr-5 gl-pr-3
'
:
true
,
hidden
:
isLabelSet
,
},
attrs
:
{
'
data-testid
'
:
'
no-icon
'
,
},
});
const
labelTitle
=
h
(
'
span
'
,
label
.
title
);
const
labelLink
=
h
(
GlLink
,
{
class
:
'
gl-display-flex gl-align-items-center label-item gl-text-black-normal
'
,
on
:
{
click
:
()
=>
{
listeners
.
clickLabel
(
label
);
},
},
},
[
noIcon
,
checkedIcon
,
labelColorBox
,
labelTitle
],
);
return
h
(
'
li
'
,
{
class
:
{
'
gl-display-block
'
:
true
,
'
gl-text-left
'
:
true
,
'
is-focused
'
:
highlight
,
},
},
[
labelLink
],
);
},
};
</
script
>
app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue
0 → 100644
View file @
cb2e71a7
<
script
>
import
$
from
'
jquery
'
;
import
Vue
from
'
vue
'
;
import
Vuex
,
{
mapState
,
mapActions
,
mapGetters
}
from
'
vuex
'
;
import
{
isInViewport
}
from
'
~/lib/utils/common_utils
'
;
import
{
__
}
from
'
~/locale
'
;
import
DropdownValueCollapsed
from
'
~/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue
'
;
import
{
DropdownVariant
}
from
'
./constants
'
;
import
DropdownButton
from
'
./dropdown_button.vue
'
;
import
DropdownContents
from
'
./dropdown_contents.vue
'
;
import
DropdownTitle
from
'
./dropdown_title.vue
'
;
import
DropdownValue
from
'
./dropdown_value.vue
'
;
import
labelsSelectModule
from
'
./store
'
;
Vue
.
use
(
Vuex
);
export
default
{
store
:
new
Vuex
.
Store
(
labelsSelectModule
()),
components
:
{
DropdownTitle
,
DropdownValue
,
DropdownButton
,
DropdownContents
,
DropdownValueCollapsed
,
},
props
:
{
allowLabelRemove
:
{
type
:
Boolean
,
required
:
false
,
default
:
false
,
},
allowLabelEdit
:
{
type
:
Boolean
,
required
:
false
,
default
:
false
,
},
allowLabelCreate
:
{
type
:
Boolean
,
required
:
false
,
default
:
false
,
},
allowMultiselect
:
{
type
:
Boolean
,
required
:
false
,
default
:
false
,
},
allowScopedLabels
:
{
type
:
Boolean
,
required
:
false
,
default
:
false
,
},
variant
:
{
type
:
String
,
required
:
false
,
default
:
DropdownVariant
.
Sidebar
,
},
selectedLabels
:
{
type
:
Array
,
required
:
false
,
default
:
()
=>
[],
},
labelsSelectInProgress
:
{
type
:
Boolean
,
required
:
false
,
default
:
false
,
},
labelsFetchPath
:
{
type
:
String
,
required
:
false
,
default
:
''
,
},
labelsManagePath
:
{
type
:
String
,
required
:
false
,
default
:
''
,
},
labelsFilterBasePath
:
{
type
:
String
,
required
:
false
,
default
:
''
,
},
labelsFilterParam
:
{
type
:
String
,
required
:
false
,
default
:
'
label_name
'
,
},
dropdownButtonText
:
{
type
:
String
,
required
:
false
,
default
:
__
(
'
Label
'
),
},
labelsListTitle
:
{
type
:
String
,
required
:
false
,
default
:
__
(
'
Assign labels
'
),
},
labelsCreateTitle
:
{
type
:
String
,
required
:
false
,
default
:
__
(
'
Create group label
'
),
},
footerCreateLabelTitle
:
{
type
:
String
,
required
:
false
,
default
:
__
(
'
Create group label
'
),
},
footerManageLabelTitle
:
{
type
:
String
,
required
:
false
,
default
:
__
(
'
Manage group labels
'
),
},
isEditing
:
{
type
:
Boolean
,
required
:
false
,
default
:
false
,
},
},
data
()
{
return
{
contentIsOnViewport
:
true
,
};
},
computed
:
{
...
mapState
([
'
showDropdownButton
'
,
'
showDropdownContents
'
]),
...
mapGetters
([
'
isDropdownVariantSidebar
'
,
'
isDropdownVariantStandalone
'
,
'
isDropdownVariantEmbedded
'
,
]),
dropdownButtonVisible
()
{
return
this
.
isDropdownVariantSidebar
?
this
.
showDropdownButton
:
true
;
},
},
watch
:
{
selectedLabels
(
selectedLabels
)
{
this
.
setInitialState
({
selectedLabels
,
});
},
showDropdownContents
(
showDropdownContents
)
{
this
.
setContentIsOnViewport
(
showDropdownContents
);
},
isEditing
(
newVal
)
{
if
(
newVal
)
{
this
.
toggleDropdownContents
();
}
},
},
mounted
()
{
this
.
setInitialState
({
variant
:
this
.
variant
,
allowLabelRemove
:
this
.
allowLabelRemove
,
allowLabelEdit
:
this
.
allowLabelEdit
,
allowLabelCreate
:
this
.
allowLabelCreate
,
allowMultiselect
:
this
.
allowMultiselect
,
allowScopedLabels
:
this
.
allowScopedLabels
,
dropdownButtonText
:
this
.
dropdownButtonText
,
selectedLabels
:
this
.
selectedLabels
,
labelsFetchPath
:
this
.
labelsFetchPath
,
labelsManagePath
:
this
.
labelsManagePath
,
labelsFilterBasePath
:
this
.
labelsFilterBasePath
,
labelsFilterParam
:
this
.
labelsFilterParam
,
labelsListTitle
:
this
.
labelsListTitle
,
labelsCreateTitle
:
this
.
labelsCreateTitle
,
footerCreateLabelTitle
:
this
.
footerCreateLabelTitle
,
footerManageLabelTitle
:
this
.
footerManageLabelTitle
,
});
this
.
$store
.
subscribeAction
({
after
:
this
.
handleVuexActionDispatch
,
});
document
.
addEventListener
(
'
mousedown
'
,
this
.
handleDocumentMousedown
);
document
.
addEventListener
(
'
click
'
,
this
.
handleDocumentClick
);
},
beforeDestroy
()
{
document
.
removeEventListener
(
'
mousedown
'
,
this
.
handleDocumentMousedown
);
document
.
removeEventListener
(
'
click
'
,
this
.
handleDocumentClick
);
},
methods
:
{
...
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.
* Required by the click listener because the click
* event itself has no reference to this element.
*/
handleDocumentMousedown
({
target
})
{
this
.
mousedownTarget
=
target
;
},
/**
* This method listens for document-wide click event
* and toggle dropdown if user clicks anywhere outside
* the dropdown while dropdown is visible.
*/
handleDocumentClick
({
target
})
{
// We also perform the toggle exception check for the
// last mousedown event's target to avoid hiding the
// box when the mousedown happened inside the box and
// only the mouseup did not.
if
(
this
.
showDropdownContents
&&
!
this
.
preventDropdownToggleOnClick
(
target
)
&&
!
this
.
preventDropdownToggleOnClick
(
this
.
mousedownTarget
)
)
{
this
.
toggleDropdownContents
();
}
},
/**
* This method checks whether a given click target
* should prevent the dropdown from being toggled.
*/
preventDropdownToggleOnClick
(
target
)
{
// This approach of element detection is needed
// as the dropdown wrapper is not using `GlDropdown` as
// it will also require us to use `BDropdownForm`
// which is yet to be implemented in GitLab UI.
const
hasExceptionClass
=
[
'
js-dropdown-button
'
,
'
js-btn-cancel-create
'
,
'
js-sidebar-dropdown-toggle
'
,
].
some
(
(
className
)
=>
target
?.
classList
.
contains
(
className
)
||
target
?.
parentElement
?.
classList
.
contains
(
className
),
);
const
hasExceptionParent
=
[
'
.js-btn-back
'
,
'
.js-labels-list
'
].
some
(
(
className
)
=>
$
(
target
).
parents
(
className
).
length
,
);
const
isInDropdownButtonCollapsed
=
this
.
$refs
.
dropdownButtonCollapsed
?.
$el
.
contains
(
target
);
const
isInDropdownContents
=
this
.
$refs
.
dropdownContents
?.
$el
.
contains
(
target
);
return
(
hasExceptionClass
||
hasExceptionParent
||
isInDropdownButtonCollapsed
||
isInDropdownContents
);
},
handleDropdownClose
(
labels
)
{
// Only emit label updates if there are any labels to update
// on UI.
if
(
labels
.
length
)
this
.
$emit
(
'
updateSelectedLabels
'
,
labels
);
this
.
$emit
(
'
onDropdownClose
'
);
},
handleCollapsedValueClick
()
{
this
.
$emit
(
'
toggleCollapse
'
);
},
setContentIsOnViewport
(
showDropdownContents
)
{
if
(
!
showDropdownContents
)
{
this
.
contentIsOnViewport
=
true
;
return
;
}
this
.
$nextTick
(()
=>
{
if
(
this
.
$refs
.
dropdownContents
)
{
this
.
contentIsOnViewport
=
isInViewport
(
this
.
$refs
.
dropdownContents
.
$el
);
}
});
},
},
};
</
script
>
<
template
>
<div
class=
"labels-select-wrapper position-relative"
:class=
"
{
'is-standalone': isDropdownVariantStandalone,
'is-embedded': isDropdownVariantEmbedded,
}"
>
<template
v-if=
"isDropdownVariantSidebar"
>
<dropdown-value-collapsed
ref=
"dropdownButtonCollapsed"
:labels=
"selectedLabels"
@
onValueClick=
"handleCollapsedValueClick"
/>
<dropdown-title
:allow-label-edit=
"allowLabelEdit"
:labels-select-in-progress=
"labelsSelectInProgress"
/>
<dropdown-value
:disable-labels=
"labelsSelectInProgress"
@
onLabelRemove=
"$emit('onLabelRemove', $event)"
>
<slot></slot>
</dropdown-value>
<dropdown-button
v-show=
"dropdownButtonVisible"
class=
"gl-mt-2"
/>
<dropdown-contents
v-show=
"dropdownButtonVisible && showDropdownContents"
ref=
"dropdownContents"
:render-on-top=
"!contentIsOnViewport"
/>
</
template
>
<
template
v-if=
"isDropdownVariantStandalone || isDropdownVariantEmbedded"
>
<dropdown-button
v-show=
"dropdownButtonVisible"
/>
<dropdown-contents
v-if=
"dropdownButtonVisible && showDropdownContents"
ref=
"dropdownContents"
:render-on-top=
"!contentIsOnViewport"
/>
</
template
>
</div>
</template>
app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/store/actions.js
0 → 100644
View file @
cb2e71a7
import
{
deprecatedCreateFlash
as
flash
}
from
'
~/flash
'
;
import
axios
from
'
~/lib/utils/axios_utils
'
;
import
{
__
}
from
'
~/locale
'
;
import
*
as
types
from
'
./mutation_types
'
;
export
const
setInitialState
=
({
commit
},
props
)
=>
commit
(
types
.
SET_INITIAL_STATE
,
props
);
export
const
toggleDropdownButton
=
({
commit
})
=>
commit
(
types
.
TOGGLE_DROPDOWN_BUTTON
);
export
const
toggleDropdownContents
=
({
commit
})
=>
commit
(
types
.
TOGGLE_DROPDOWN_CONTENTS
);
export
const
toggleDropdownContentsCreateView
=
({
commit
})
=>
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
);
flash
(
__
(
'
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
requestCreateLabel
=
({
commit
})
=>
commit
(
types
.
REQUEST_CREATE_LABEL
);
export
const
receiveCreateLabelSuccess
=
({
commit
})
=>
commit
(
types
.
RECEIVE_CREATE_LABEL_SUCCESS
);
export
const
receiveCreateLabelFailure
=
({
commit
})
=>
{
commit
(
types
.
RECEIVE_CREATE_LABEL_FAILURE
);
flash
(
__
(
'
Error creating label.
'
));
};
export
const
createLabel
=
({
state
,
dispatch
},
label
)
=>
{
dispatch
(
'
requestCreateLabel
'
);
axios
.
post
(
state
.
labelsManagePath
,
{
label
,
})
.
then
(({
data
})
=>
{
if
(
data
.
id
)
{
dispatch
(
'
receiveCreateLabelSuccess
'
);
dispatch
(
'
toggleDropdownContentsCreateView
'
);
}
else
{
// eslint-disable-next-line @gitlab/require-i18n-strings
throw
new
Error
(
'
Error Creating Label
'
);
}
})
.
catch
(()
=>
{
dispatch
(
'
receiveCreateLabelFailure
'
);
});
};
export
const
updateSelectedLabels
=
({
commit
},
labels
)
=>
commit
(
types
.
UPDATE_SELECTED_LABELS
,
{
labels
});
app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/store/getters.js
0 → 100644
View file @
cb2e71a7
import
{
__
,
s__
,
sprintf
}
from
'
~/locale
'
;
import
{
DropdownVariant
}
from
'
../constants
'
;
/**
* Returns string representing current labels
* selection on dropdown button.
*
* @param {object} state
*/
export
const
dropdownButtonText
=
(
state
,
getters
)
=>
{
const
selectedLabels
=
getters
.
isDropdownVariantSidebar
?
state
.
labels
.
filter
((
label
)
=>
label
.
set
)
:
state
.
selectedLabels
;
if
(
!
selectedLabels
.
length
)
{
return
state
.
dropdownButtonText
||
__
(
'
Label
'
);
}
else
if
(
selectedLabels
.
length
>
1
)
{
return
sprintf
(
s__
(
'
LabelSelect|%{firstLabelName} +%{remainingLabelCount} more
'
),
{
firstLabelName
:
selectedLabels
[
0
].
title
,
remainingLabelCount
:
selectedLabels
.
length
-
1
,
});
}
return
selectedLabels
[
0
].
title
;
};
/**
* Returns array containing only label IDs from
* selectedLabels array.
* @param {object} state
*/
export
const
selectedLabelsList
=
(
state
)
=>
state
.
selectedLabels
.
map
((
label
)
=>
label
.
id
);
/**
* Returns boolean representing whether dropdown variant
* is `sidebar`
* @param {object} state
*/
export
const
isDropdownVariantSidebar
=
(
state
)
=>
state
.
variant
===
DropdownVariant
.
Sidebar
;
/**
* Returns boolean representing whether dropdown variant
* is `standalone`
* @param {object} state
*/
export
const
isDropdownVariantStandalone
=
(
state
)
=>
state
.
variant
===
DropdownVariant
.
Standalone
;
/**
* Returns boolean representing whether dropdown variant
* is `embedded`
* @param {object} state
*/
export
const
isDropdownVariantEmbedded
=
(
state
)
=>
state
.
variant
===
DropdownVariant
.
Embedded
;
app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/store/index.js
0 → 100644
View file @
cb2e71a7
import
*
as
actions
from
'
./actions
'
;
import
*
as
getters
from
'
./getters
'
;
import
mutations
from
'
./mutations
'
;
import
state
from
'
./state
'
;
export
default
()
=>
({
namespaced
:
true
,
state
:
state
(),
actions
,
getters
,
mutations
,
});
app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/store/mutation_types.js
0 → 100644
View file @
cb2e71a7
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
REQUEST_CREATE_LABEL
=
'
REQUEST_CREATE_LABEL
'
;
export
const
RECEIVE_CREATE_LABEL_SUCCESS
=
'
RECEIVE_CREATE_LABEL_SUCCESS
'
;
export
const
RECEIVE_CREATE_LABEL_FAILURE
=
'
RECEIVE_CREATE_LABEL_FAILURE
'
;
export
const
TOGGLE_DROPDOWN_BUTTON
=
'
TOGGLE_DROPDOWN_VISIBILITY
'
;
export
const
TOGGLE_DROPDOWN_CONTENTS
=
'
TOGGLE_DROPDOWN_CONTENTS
'
;
export
const
UPDATE_SELECTED_LABELS
=
'
UPDATE_SELECTED_LABELS
'
;
export
const
TOGGLE_DROPDOWN_CONTENTS_CREATE_VIEW
=
'
TOGGLE_DROPDOWN_CONTENTS_CREATE_VIEW
'
;
app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/store/mutations.js
0 → 100644
View file @
cb2e71a7
import
{
DropdownVariant
}
from
'
../constants
'
;
import
*
as
types
from
'
./mutation_types
'
;
export
default
{
[
types
.
SET_INITIAL_STATE
](
state
,
props
)
{
Object
.
assign
(
state
,
{
...
props
});
},
[
types
.
TOGGLE_DROPDOWN_BUTTON
](
state
)
{
state
.
showDropdownButton
=
!
state
.
showDropdownButton
;
},
[
types
.
TOGGLE_DROPDOWN_CONTENTS
](
state
)
{
if
(
state
.
variant
===
DropdownVariant
.
Sidebar
)
{
state
.
showDropdownButton
=
!
state
.
showDropdownButton
;
}
state
.
showDropdownContents
=
!
state
.
showDropdownContents
;
// Ensure that Create View is hidden by default
// when dropdown contents are revealed.
if
(
state
.
showDropdownContents
)
{
state
.
showDropdownContentsCreateView
=
false
;
}
},
[
types
.
TOGGLE_DROPDOWN_CONTENTS_CREATE_VIEW
](
state
)
{
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
.
REQUEST_CREATE_LABEL
](
state
)
{
state
.
labelCreateInProgress
=
true
;
},
[
types
.
RECEIVE_CREATE_LABEL_SUCCESS
](
state
)
{
state
.
labelCreateInProgress
=
false
;
},
[
types
.
RECEIVE_CREATE_LABEL_FAILURE
](
state
)
{
state
.
labelCreateInProgress
=
false
;
},
[
types
.
UPDATE_SELECTED_LABELS
](
state
,
{
labels
})
{
// Find the label to update from all the labels
// and change `set` prop value to represent their current state.
const
labelId
=
labels
.
pop
()?.
id
;
const
candidateLabel
=
state
.
labels
.
find
((
label
)
=>
labelId
===
label
.
id
);
if
(
candidateLabel
)
{
candidateLabel
.
touched
=
true
;
candidateLabel
.
set
=
!
candidateLabel
.
set
;
}
},
};
app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/store/state.js
0 → 100644
View file @
cb2e71a7
export
default
()
=>
({
// Initial Data
labels
:
[],
selectedLabels
:
[],
labelsListTitle
:
''
,
labelsCreateTitle
:
''
,
footerCreateLabelTitle
:
''
,
footerManageLabelTitle
:
''
,
dropdownButtonText
:
''
,
// Paths
namespace
:
''
,
labelsFetchPath
:
''
,
labelsFilterBasePath
:
''
,
// UI Flags
variant
:
''
,
allowLabelRemove
:
false
,
allowLabelCreate
:
false
,
allowLabelEdit
:
false
,
allowScopedLabels
:
false
,
allowMultiselect
:
false
,
showDropdownButton
:
false
,
showDropdownContents
:
false
,
showDropdownContentsCreateView
:
false
,
labelsFetchInProgress
:
false
,
labelCreateInProgress
:
false
,
selectedLabelsUpdated
:
false
,
});
app/controllers/projects/issues_controller.rb
View file @
cb2e71a7
...
...
@@ -55,6 +55,7 @@ class Projects::IssuesController < Projects::ApplicationController
push_to_gon_attributes
(
:features
,
real_time_feature_flag
,
real_time_enabled
)
push_frontend_feature_flag
(
:confidential_notes
,
@project
,
default_enabled: :yaml
)
push_frontend_feature_flag
(
:issue_assignees_widget
,
@project
,
default_enabled: :yaml
)
push_frontend_feature_flag
(
:labels_widget
,
@project
,
default_enabled: :yaml
)
experiment
(
:invite_members_in_comment
,
namespace:
@project
.
root_ancestor
)
do
|
experiment_instance
|
experiment_instance
.
exclude!
unless
helpers
.
can_import_members?
...
...
config/feature_flags/development/labels_widget.yml
0 → 100644
View file @
cb2e71a7
---
name
:
labels_widget
introduced_by_url
:
https://gitlab.com/gitlab-org/gitlab/-/merge_requests/62898
rollout_issue_url
:
https://gitlab.com/gitlab-org/gitlab/-/issues/332327
milestone
:
'
14.0'
type
:
development
group
:
group::project management
default_enabled
:
false
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