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
e51b3eed
Commit
e51b3eed
authored
Sep 03, 2021
by
Florie Guibert
Committed by
Natalia Tepluhina
Sep 03, 2021
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Refactor labels dropdown to GlDropdown
parent
662760b9
Changes
11
Hide whitespace changes
Inline
Side-by-side
Showing
11 changed files
with
212 additions
and
427 deletions
+212
-427
app/assets/javascripts/sidebar/mount_sidebar.js
app/assets/javascripts/sidebar/mount_sidebar.js
+1
-0
app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_button.vue
...mponents/sidebar/labels_select_widget/dropdown_button.vue
+0
-42
app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue
...onents/sidebar/labels_select_widget/dropdown_contents.vue
+65
-43
app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue
...ar/labels_select_widget/dropdown_contents_labels_view.vue
+30
-103
app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/label_item.vue
...ed/components/sidebar/labels_select_widget/label_item.vue
+2
-13
app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue
...nents/sidebar/labels_select_widget/labels_select_root.vue
+4
-16
spec/features/issues/user_edits_issue_spec.rb
spec/features/issues/user_edits_issue_spec.rb
+1
-0
spec/features/labels_hierarchy_spec.rb
spec/features/labels_hierarchy_spec.rb
+1
-0
spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_button_spec.js
...ents/sidebar/labels_select_widget/dropdown_button_spec.js
+0
-91
spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view_spec.js
...abels_select_widget/dropdown_contents_labels_view_spec.js
+0
-73
spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_spec.js
...ts/sidebar/labels_select_widget/dropdown_contents_spec.js
+108
-46
No files found.
app/assets/javascripts/sidebar/mount_sidebar.js
View file @
e51b3eed
...
@@ -259,6 +259,7 @@ export function mountSidebarLabels() {
...
@@ -259,6 +259,7 @@ export function mountSidebarLabels() {
initiallySelectedLabels
:
JSON
.
parse
(
el
.
dataset
.
selectedLabels
),
initiallySelectedLabels
:
JSON
.
parse
(
el
.
dataset
.
selectedLabels
),
variant
:
DropdownVariant
.
Sidebar
,
variant
:
DropdownVariant
.
Sidebar
,
canUpdate
:
parseBoolean
(
el
.
dataset
.
canEdit
),
canUpdate
:
parseBoolean
(
el
.
dataset
.
canEdit
),
isClassicSidebar
:
true
,
},
},
render
:
(
createElement
)
=>
createElement
(
SidebarLabels
),
render
:
(
createElement
)
=>
createElement
(
SidebarLabels
),
});
});
...
...
app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_button.vue
deleted
100644 → 0
View file @
662760b9
<
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
View file @
e51b3eed
<
script
>
<
script
>
import
{
GlButton
}
from
'
@gitlab/ui
'
;
import
{
GlButton
,
GlDropdown
,
GlDropdownItem
,
GlLink
}
from
'
@gitlab/ui
'
;
import
{
mapActions
,
mapGetters
,
mapState
}
from
'
vuex
'
;
import
{
mapActions
,
mapGetters
,
mapState
}
from
'
vuex
'
;
import
DropdownContentsCreateView
from
'
./dropdown_contents_create_view.vue
'
;
import
DropdownContentsCreateView
from
'
./dropdown_contents_create_view.vue
'
;
...
@@ -10,13 +10,12 @@ export default {
...
@@ -10,13 +10,12 @@ export default {
DropdownContentsLabelsView
,
DropdownContentsLabelsView
,
DropdownContentsCreateView
,
DropdownContentsCreateView
,
GlButton
,
GlButton
,
GlDropdown
,
GlDropdownItem
,
GlLink
,
},
},
inject
:
[
'
allowLabelCreate
'
,
'
labelsManagePath
'
],
props
:
{
props
:
{
renderOnTop
:
{
type
:
Boolean
,
required
:
false
,
default
:
false
,
},
labelsCreateTitle
:
{
labelsCreateTitle
:
{
type
:
String
,
type
:
String
,
required
:
true
,
required
:
true
,
...
@@ -44,67 +43,90 @@ export default {
...
@@ -44,67 +43,90 @@ export default {
},
},
computed
:
{
computed
:
{
...
mapState
([
'
showDropdownContentsCreateView
'
]),
...
mapState
([
'
showDropdownContentsCreateView
'
]),
...
mapGetters
([
'
isDropdownVariantSidebar
'
,
'
isDropdownVariantEmbedded
'
]),
...
mapGetters
([
'
dropdownButtonText
'
,
'
isDropdownVariantSidebar
'
,
'
isDropdownVariantEmbedded
'
]),
dropdownContentsView
()
{
dropdownContentsView
()
{
if
(
this
.
showDropdownContentsCreateView
)
{
if
(
this
.
showDropdownContentsCreateView
)
{
return
'
dropdown-contents-create-view
'
;
return
'
dropdown-contents-create-view
'
;
}
}
return
'
dropdown-contents-labels-view
'
;
return
'
dropdown-contents-labels-view
'
;
},
},
directionStyle
()
{
const
bottom
=
this
.
isDropdownVariantSidebar
?
'
3rem
'
:
'
2rem
'
;
return
this
.
renderOnTop
?
{
bottom
}
:
{};
},
dropdownTitle
()
{
dropdownTitle
()
{
return
this
.
showDropdownContentsCreateView
?
this
.
labelsCreateTitle
:
this
.
labelsListTitle
;
return
this
.
showDropdownContentsCreateView
?
this
.
labelsCreateTitle
:
this
.
labelsListTitle
;
},
},
showDropdownFooter
()
{
return
(
!
this
.
showDropdownContentsCreateView
&&
(
this
.
isDropdownVariantSidebar
||
this
.
isDropdownVariantEmbedded
)
);
},
},
},
methods
:
{
methods
:
{
...
mapActions
([
'
toggleDropdownContentsCreateView
'
]),
...
mapActions
([
'
toggleDropdownContentsCreateView
'
]),
showDropdown
()
{
this
.
$refs
.
dropdown
.
show
();
},
toggleDropdownContent
()
{
this
.
toggleDropdownContentsCreateView
();
// Required to recalculate dropdown position as its size changes
this
.
$refs
.
dropdown
.
$refs
.
dropdown
.
$_popper
.
scheduleUpdate
();
},
},
},
};
};
</
script
>
</
script
>
<
template
>
<
template
>
<div
<gl-dropdown
class=
"labels-select-dropdown-contents gl-w-full gl-my-2 gl-py-3 gl-rounded-base gl-absolute"
ref=
"dropdown"
:text=
"dropdownButtonText"
class=
"gl-w-full gl-mt-2"
data-qa-selector=
"labels_dropdown_content"
data-qa-selector=
"labels_dropdown_content"
:style=
"directionStyle"
>
>
<div
<template
#header
>
v-if=
"isDropdownVariantSidebar || isDropdownVariantEmbedded"
<div
class=
"dropdown-title gl-display-flex gl-align-items-center gl-pt-0 gl-pb-3!"
v-if=
"isDropdownVariantSidebar || isDropdownVariantEmbedded"
data-testid=
"dropdown-title"
class=
"dropdown-title gl-display-flex gl-align-items-center gl-pt-0 gl-pb-3!"
>
>
<gl-button
<gl-button
v-if=
"showDropdownContentsCreateView"
v-if=
"showDropdownContentsCreateView"
:aria-label=
"__('Go back')"
:aria-label=
"__('Go back')"
variant=
"link"
variant=
"link"
size=
"small"
size=
"small"
class=
"js-btn-back dropdown-header-button p-0"
class=
"js-btn-back dropdown-header-button gl-p-0"
icon=
"arrow-left"
icon=
"arrow-left"
@
click.stop=
"toggleDropdownContentsCreateView"
data-testid=
"go-back-button"
/>
@
click.stop=
"toggleDropdownContent"
<span
class=
"flex-grow-1"
>
{{
dropdownTitle
}}
</span>
/>
<gl-button
<span
class=
"gl-flex-grow-1"
>
{{
dropdownTitle
}}
</span>
:aria-label=
"__('Close')"
<gl-button
variant=
"link"
:aria-label=
"__('Close')"
size=
"small"
variant=
"link"
class=
"dropdown-header-button gl-p-0!"
size=
"small"
icon=
"close"
class=
"dropdown-header-button gl-p-0!"
@
click=
"$emit('closeDropdown')"
icon=
"close"
/>
@
click=
"$emit('closeDropdown')"
</div>
/>
</div>
</
template
>
<component
<component
:is=
"dropdownContentsView"
:is=
"dropdownContentsView"
:selected-labels=
"selectedLabels"
:selected-labels=
"selectedLabels"
:allow-multiselect=
"allowMultiselect"
:allow-multiselect=
"allowMultiselect"
:labels-list-title=
"labelsListTitle"
:footer-create-label-title=
"footerCreateLabelTitle"
:footer-manage-label-title=
"footerManageLabelTitle"
@
hideCreateView=
"toggleDropdownContentsCreateView"
@
hideCreateView=
"toggleDropdownContentsCreateView"
@
setLabels=
"$emit('setLabels', $event)"
@
setLabels=
"$emit('setLabels', $event)"
@
toggleDropdownContentsCreateView=
"toggleDropdownContentsCreateView"
/>
/>
</div>
<
template
#footer
>
<div
v-if=
"showDropdownFooter"
data-testid=
"dropdown-footer"
>
<gl-dropdown-item
v-if=
"allowLabelCreate"
data-testid=
"create-label-button"
@
click.native.capture.stop=
"toggleDropdownContent"
>
{{
footerCreateLabelTitle
}}
</gl-dropdown-item>
<gl-dropdown-item
:href=
"labelsManagePath"
>
{{
footerManageLabelTitle
}}
</gl-dropdown-item>
</div>
</
template
>
</gl-dropdown>
</template>
</template>
app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue
View file @
e51b3eed
<
script
>
<
script
>
import
{
Gl
LoadingIcon
,
GlSearchBoxByType
,
GlLink
}
from
'
@gitlab/ui
'
;
import
{
Gl
DropdownForm
,
GlDropdownItem
,
GlLoadingIcon
,
GlSearchBoxByType
}
from
'
@gitlab/ui
'
;
import
fuzzaldrinPlus
from
'
fuzzaldrin-plus
'
;
import
fuzzaldrinPlus
from
'
fuzzaldrin-plus
'
;
import
{
debounce
}
from
'
lodash
'
;
import
{
debounce
}
from
'
lodash
'
;
import
createFlash
from
'
~/flash
'
;
import
createFlash
from
'
~/flash
'
;
import
{
getIdFromGraphQLId
}
from
'
~/graphql_shared/utils
'
;
import
{
getIdFromGraphQLId
}
from
'
~/graphql_shared/utils
'
;
import
{
DEFAULT_DEBOUNCE_AND_THROTTLE_MS
}
from
'
~/lib/utils/constants
'
;
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
{
__
}
from
'
~/locale
'
;
import
{
__
}
from
'
~/locale
'
;
import
{
DropdownVariant
}
from
'
./constants
'
;
import
projectLabelsQuery
from
'
./graphql/project_labels.query.graphql
'
;
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
:
{
GlDropdownForm
,
GlDropdownItem
,
GlLoadingIcon
,
GlLoadingIcon
,
GlSearchBoxByType
,
GlSearchBoxByType
,
GlLink
,
LabelItem
,
LabelItem
,
},
},
inject
:
[
'
projectPath
'
,
'
allowLabelCreate
'
,
'
labelsManagePath
'
,
'
variant
'
],
inject
:
[
'
projectPath
'
],
props
:
{
props
:
{
selectedLabels
:
{
selectedLabels
:
{
type
:
Array
,
type
:
Array
,
...
@@ -28,18 +27,6 @@ export default {
...
@@ -28,18 +27,6 @@ export default {
type
:
Boolean
,
type
:
Boolean
,
required
:
true
,
required
:
true
,
},
},
labelsListTitle
:
{
type
:
String
,
required
:
true
,
},
footerCreateLabelTitle
:
{
type
:
String
,
required
:
true
,
},
footerManageLabelTitle
:
{
type
:
String
,
required
:
true
,
},
},
},
data
()
{
data
()
{
return
{
return
{
...
@@ -74,12 +61,6 @@ export default {
...
@@ -74,12 +61,6 @@ export default {
},
},
},
},
computed
:
{
computed
:
{
isDropdownVariantSidebar
()
{
return
this
.
variant
===
DropdownVariant
.
Sidebar
;
},
isDropdownVariantEmbedded
()
{
return
this
.
variant
===
DropdownVariant
.
Embedded
;
},
labelsFetchInProgress
()
{
labelsFetchInProgress
()
{
return
this
.
$apollo
.
queries
.
labels
.
loading
;
return
this
.
$apollo
.
queries
.
labels
.
loading
;
},
},
...
@@ -150,37 +131,10 @@ export default {
...
@@ -150,37 +131,10 @@ export default {
});
});
}
}
},
},
/**
* 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
.
$emit
(
'
setLabels
'
,
this
.
localSelectedLabels
);
}
if
(
e
.
keyCode
!==
ESC_KEY_CODE
)
{
// Scroll the list only after highlighting
// styles are rendered completely.
this
.
$nextTick
(()
=>
{
this
.
scrollIntoViewIfNeeded
();
});
}
},
handleLabelClick
(
label
)
{
handleLabelClick
(
label
)
{
this
.
updateSelectedLabels
(
label
);
this
.
updateSelectedLabels
(
label
);
if
(
!
this
.
allowMultiselect
)
{
if
(
!
this
.
allowMultiselect
)
{
this
.
$emit
(
'
setLabels
'
,
this
.
localSelectedLabels
);
this
.
$emit
(
'
closeDropdown
'
,
this
.
localSelectedLabels
);
}
}
},
},
setSearchKey
(
value
)
{
setSearchKey
(
value
)
{
...
@@ -191,69 +145,42 @@ export default {
...
@@ -191,69 +145,42 @@ export default {
</
script
>
</
script
>
<
template
>
<
template
>
<div
<gl-dropdown-form
class=
"labels-select-contents-list js-labels-list"
>
class=
"labels-select-contents-list js-labels-list"
<gl-search-box-by-type
data-testid=
"dropdown-wrapper"
ref=
"searchInput"
@
keydown=
"handleKeyDown"
:value=
"searchKey"
>
:disabled=
"labelsFetchInProgress"
<div
class=
"dropdown-input"
@
click.stop=
"() =>
{}">
data-qa-selector=
"dropdown_input_field"
<gl-search-box-by-type
data-testid=
"dropdown-input-field"
ref=
"searchInput"
@
input=
"debouncedSearchKeyUpdate"
:value=
"searchKey"
/>
:disabled=
"labelsFetchInProgress"
<div
ref=
"labelsListContainer"
data-testid=
"dropdown-content"
>
data-qa-selector=
"dropdown_input_field"
data-testid=
"dropdown-input-field"
@
input=
"debouncedSearchKeyUpdate"
/>
</div>
<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 gl-w-full gl-h-full"
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"
data-testid=
"labels-list"
>
<
template
v-else
>
<
label
-item
<
gl-dropdown
-item
v-for=
"(label, index) in visibleLabels"
v-for=
"(label, index) in visibleLabels"
:key=
"label.id"
:key=
"label.id"
:label=
"label"
data-testid=
"labels-list"
:is-label-set=
"isLabelSelected(label)"
@
click.native.capture.stop=
"handleLabelClick(label)"
:highlight=
"index === currentHighlightItem"
>
@
clickLabel=
"handleLabelClick(label)"
<label-item
/>
:label=
"label"
<li
:is-label-set=
"isLabelSelected(label)"
:highlight=
"index === currentHighlightItem"
/>
</gl-dropdown-item>
<gl-dropdown-item
v-show=
"showNoMatchingResultsMessage"
v-show=
"showNoMatchingResultsMessage"
class=
"gl-p-3 gl-text-center"
class=
"gl-p-3 gl-text-center"
data-testid=
"no-results"
data-testid=
"no-results"
>
>
{{
__
(
'
No matching results
'
)
}}
{{
__
(
'
No matching results
'
)
}}
</li>
</gl-dropdown-item>
</ul>
</
template
>
</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 gl-flex-direction-row gl-w-full gl-overflow-break-word label-item"
data-testid=
"create-label-button"
@
click.stop=
"$emit('toggleDropdownContentsCreateView')"
>
{{
footerCreateLabelTitle
}}
</gl-link>
</li>
<li>
<gl-link
:href=
"labelsManagePath"
class=
"gl-display-flex gl-flex-direction-row gl-w-full gl-overflow-break-word label-item"
>
{{
footerManageLabelTitle
}}
</gl-link>
</li>
</ul>
</div>
</div>
</
div
>
</
gl-dropdown-form
>
</template>
</template>
app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/label_item.vue
View file @
e51b3eed
...
@@ -18,7 +18,7 @@ export default {
...
@@ -18,7 +18,7 @@ export default {
default
:
false
,
default
:
false
,
},
},
},
},
render
(
h
,
{
props
,
listeners
})
{
render
(
h
,
{
props
})
{
const
{
label
,
highlight
,
isLabelSet
}
=
props
;
const
{
label
,
highlight
,
isLabelSet
}
=
props
;
const
labelColorBox
=
h
(
'
span
'
,
{
const
labelColorBox
=
h
(
'
span
'
,
{
...
@@ -53,18 +53,7 @@ export default {
...
@@ -53,18 +53,7 @@ export default {
const
labelTitle
=
h
(
'
span
'
,
label
.
title
);
const
labelTitle
=
h
(
'
span
'
,
label
.
title
);
const
labelLink
=
h
(
const
labelLink
=
h
(
GlLink
,
[
noIcon
,
checkedIcon
,
labelColorBox
,
labelTitle
]);
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
(
return
h
(
'
li
'
,
'
li
'
,
...
...
app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue
View file @
e51b3eed
<
script
>
<
script
>
import
Vue
from
'
vue
'
;
import
Vue
from
'
vue
'
;
import
Vuex
,
{
mapState
,
mapActions
,
mapGetters
}
from
'
vuex
'
;
import
Vuex
,
{
mapActions
,
mapGetters
}
from
'
vuex
'
;
import
{
isInViewport
}
from
'
~/lib/utils/common_utils
'
;
import
{
__
}
from
'
~/locale
'
;
import
{
__
}
from
'
~/locale
'
;
import
SidebarEditableItem
from
'
~/sidebar/components/sidebar_editable_item.vue
'
;
import
SidebarEditableItem
from
'
~/sidebar/components/sidebar_editable_item.vue
'
;
import
{
DropdownVariant
}
from
'
./constants
'
;
import
{
DropdownVariant
}
from
'
./constants
'
;
import
DropdownButton
from
'
./dropdown_button.vue
'
;
import
DropdownContents
from
'
./dropdown_contents.vue
'
;
import
DropdownContents
from
'
./dropdown_contents.vue
'
;
import
DropdownValue
from
'
./dropdown_value.vue
'
;
import
DropdownValue
from
'
./dropdown_value.vue
'
;
import
DropdownValueCollapsed
from
'
./dropdown_value_collapsed.vue
'
;
import
DropdownValueCollapsed
from
'
./dropdown_value_collapsed.vue
'
;
...
@@ -18,7 +16,6 @@ export default {
...
@@ -18,7 +16,6 @@ export default {
store
:
new
Vuex
.
Store
(
labelsSelectModule
()),
store
:
new
Vuex
.
Store
(
labelsSelectModule
()),
components
:
{
components
:
{
DropdownValue
,
DropdownValue
,
DropdownButton
,
DropdownContents
,
DropdownContents
,
DropdownValueCollapsed
,
DropdownValueCollapsed
,
SidebarEditableItem
,
SidebarEditableItem
,
...
@@ -137,7 +134,6 @@ export default {
...
@@ -137,7 +134,6 @@ export default {
},
},
},
},
computed
:
{
computed
:
{
...
mapState
([
'
showDropdownContents
'
]),
...
mapGetters
([
...
mapGetters
([
'
isDropdownVariantSidebar
'
,
'
isDropdownVariantSidebar
'
,
'
isDropdownVariantStandalone
'
,
'
isDropdownVariantStandalone
'
,
...
@@ -150,9 +146,6 @@ export default {
...
@@ -150,9 +146,6 @@ export default {
selectedLabels
,
selectedLabels
,
});
});
},
},
showDropdownContents
(
showDropdownContents
)
{
this
.
setContentIsOnViewport
(
showDropdownContents
);
},
isEditing
(
newVal
)
{
isEditing
(
newVal
)
{
if
(
newVal
)
{
if
(
newVal
)
{
this
.
toggleDropdownContents
();
this
.
toggleDropdownContents
();
...
@@ -190,11 +183,9 @@ export default {
...
@@ -190,11 +183,9 @@ export default {
handleCollapsedValueClick
()
{
handleCollapsedValueClick
()
{
this
.
$emit
(
'
toggleCollapse
'
);
this
.
$emit
(
'
toggleCollapse
'
);
},
},
s
etContentIsOnViewport
()
{
s
howDropdown
()
{
this
.
$nextTick
(()
=>
{
this
.
$nextTick
(()
=>
{
if
(
this
.
$refs
.
dropdownContents
)
{
this
.
$refs
.
dropdownContents
.
showDropdown
();
this
.
contentIsOnViewport
=
isInViewport
(
this
.
$refs
.
dropdownContents
.
$el
);
}
});
});
},
},
},
},
...
@@ -219,8 +210,7 @@ export default {
...
@@ -219,8 +210,7 @@ export default {
ref=
"editable"
ref=
"editable"
:title=
"__('Labels')"
:title=
"__('Labels')"
:loading=
"labelsSelectInProgress"
:loading=
"labelsSelectInProgress"
@
open=
"setContentIsOnViewport"
@
open=
"showDropdown"
@
close=
"contentIsOnViewport = true"
>
>
<template
#collapsed
>
<template
#collapsed
>
<dropdown-value
<dropdown-value
...
@@ -248,7 +238,6 @@ export default {
...
@@ -248,7 +238,6 @@ export default {
>
>
<slot></slot>
<slot></slot>
</dropdown-value>
</dropdown-value>
<dropdown-button
/>
<dropdown-contents
<dropdown-contents
v-if=
"edit"
v-if=
"edit"
ref=
"dropdownContents"
ref=
"dropdownContents"
...
@@ -256,7 +245,6 @@ export default {
...
@@ -256,7 +245,6 @@ export default {
:labels-list-title=
"labelsListTitle"
:labels-list-title=
"labelsListTitle"
:footer-create-label-title=
"footerCreateLabelTitle"
:footer-create-label-title=
"footerCreateLabelTitle"
:footer-manage-label-title=
"footerManageLabelTitle"
:footer-manage-label-title=
"footerManageLabelTitle"
:render-on-top=
"!contentIsOnViewport"
:labels-create-title=
"labelsCreateTitle"
:labels-create-title=
"labelsCreateTitle"
:selected-labels=
"selectedLabels"
:selected-labels=
"selectedLabels"
@
closeDropdown=
"collapseDropdown"
@
closeDropdown=
"collapseDropdown"
...
...
spec/features/issues/user_edits_issue_spec.rb
View file @
e51b3eed
...
@@ -15,6 +15,7 @@ RSpec.describe "Issues > User edits issue", :js do
...
@@ -15,6 +15,7 @@ RSpec.describe "Issues > User edits issue", :js do
context
'with authorized user'
do
context
'with authorized user'
do
before
do
before
do
stub_feature_flags
(
labels_widget:
false
)
project
.
add_developer
(
user
)
project
.
add_developer
(
user
)
project_with_milestones
.
add_developer
(
user
)
project_with_milestones
.
add_developer
(
user
)
sign_in
(
user
)
sign_in
(
user
)
...
...
spec/features/labels_hierarchy_spec.rb
View file @
e51b3eed
...
@@ -17,6 +17,7 @@ RSpec.describe 'Labels Hierarchy', :js do
...
@@ -17,6 +17,7 @@ RSpec.describe 'Labels Hierarchy', :js do
let!
(
:project_label_1
)
{
create
(
:label
,
project:
project_1
,
title:
'Label_4'
)
}
let!
(
:project_label_1
)
{
create
(
:label
,
project:
project_1
,
title:
'Label_4'
)
}
before
do
before
do
stub_feature_flags
(
labels_widget:
false
)
grandparent
.
add_owner
(
user
)
grandparent
.
add_owner
(
user
)
sign_in
(
user
)
sign_in
(
user
)
...
...
spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_button_spec.js
deleted
100644 → 0
View file @
662760b9
import
{
GlIcon
,
GlButton
}
from
'
@gitlab/ui
'
;
import
{
shallowMount
,
createLocalVue
}
from
'
@vue/test-utils
'
;
import
Vuex
from
'
vuex
'
;
import
DropdownButton
from
'
~/vue_shared/components/sidebar/labels_select_widget/dropdown_button.vue
'
;
import
labelSelectModule
from
'
~/vue_shared/components/sidebar/labels_select_widget/store
'
;
import
{
mockConfig
}
from
'
./mock_data
'
;
let
store
;
const
localVue
=
createLocalVue
();
localVue
.
use
(
Vuex
);
const
createComponent
=
(
initialState
=
mockConfig
)
=>
{
store
=
new
Vuex
.
Store
(
labelSelectModule
());
store
.
dispatch
(
'
setInitialState
'
,
initialState
);
return
shallowMount
(
DropdownButton
,
{
localVue
,
store
,
});
};
describe
(
'
DropdownButton
'
,
()
=>
{
let
wrapper
;
beforeEach
(()
=>
{
wrapper
=
createComponent
();
});
afterEach
(()
=>
{
wrapper
.
destroy
();
});
const
findDropdownButton
=
()
=>
wrapper
.
find
(
GlButton
);
const
findDropdownText
=
()
=>
wrapper
.
find
(
'
.dropdown-toggle-text
'
);
const
findDropdownIcon
=
()
=>
wrapper
.
find
(
GlIcon
);
describe
(
'
methods
'
,
()
=>
{
describe
(
'
handleButtonClick
'
,
()
=>
{
it
.
each
`
variant | expectPropagationStopped
${
'
standalone
'
}
|
${
true
}
${
'
embedded
'
}
|
${
false
}
`
(
'
toggles dropdown content and handles event propagation when `state.variant` is "$variant"
'
,
({
variant
,
expectPropagationStopped
})
=>
{
const
event
=
{
stopPropagation
:
jest
.
fn
()
};
wrapper
=
createComponent
({
...
mockConfig
,
variant
});
findDropdownButton
().
vm
.
$emit
(
'
click
'
,
event
);
expect
(
store
.
state
.
showDropdownContents
).
toBe
(
true
);
expect
(
event
.
stopPropagation
).
toHaveBeenCalledTimes
(
expectPropagationStopped
?
1
:
0
);
},
);
});
});
describe
(
'
template
'
,
()
=>
{
it
(
'
renders component container element
'
,
()
=>
{
expect
(
wrapper
.
find
(
GlButton
).
element
).
toBe
(
wrapper
.
element
);
});
it
(
'
renders default button text element
'
,
()
=>
{
const
dropdownTextEl
=
findDropdownText
();
expect
(
dropdownTextEl
.
exists
()).
toBe
(
true
);
expect
(
dropdownTextEl
.
text
()).
toBe
(
'
Label
'
);
});
it
(
'
renders provided button text element
'
,
()
=>
{
store
.
state
.
dropdownButtonText
=
'
Custom label
'
;
const
dropdownTextEl
=
findDropdownText
();
return
wrapper
.
vm
.
$nextTick
().
then
(()
=>
{
expect
(
dropdownTextEl
.
text
()).
toBe
(
'
Custom label
'
);
});
});
it
(
'
renders chevron icon element
'
,
()
=>
{
const
iconEl
=
findDropdownIcon
();
expect
(
iconEl
.
exists
()).
toBe
(
true
);
expect
(
iconEl
.
props
(
'
name
'
)).
toBe
(
'
chevron-down
'
);
});
});
});
spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view_spec.js
View file @
e51b3eed
...
@@ -45,8 +45,6 @@ describe('DropdownContentsLabelsView', () => {
...
@@ -45,8 +45,6 @@ describe('DropdownContentsLabelsView', () => {
provide
:
{
provide
:
{
projectPath
:
'
test
'
,
projectPath
:
'
test
'
,
iid
:
1
,
iid
:
1
,
allowLabelCreate
:
true
,
labelsManagePath
:
'
/gitlab-org/my-project/-/labels
'
,
variant
:
DropdownVariant
.
Sidebar
,
variant
:
DropdownVariant
.
Sidebar
,
...
injected
,
...
injected
,
},
},
...
@@ -69,10 +67,7 @@ describe('DropdownContentsLabelsView', () => {
...
@@ -69,10 +67,7 @@ describe('DropdownContentsLabelsView', () => {
const
findLoadingIcon
=
()
=>
wrapper
.
findComponent
(
GlLoadingIcon
);
const
findLoadingIcon
=
()
=>
wrapper
.
findComponent
(
GlLoadingIcon
);
const
findLabelsList
=
()
=>
wrapper
.
find
(
'
[data-testid="labels-list"]
'
);
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
findNoResultsMessage
=
()
=>
wrapper
.
find
(
'
[data-testid="no-results"]
'
);
const
findCreateLabelButton
=
()
=>
wrapper
.
find
(
'
[data-testid="create-label-button"]
'
);
describe
(
'
when loading labels
'
,
()
=>
{
describe
(
'
when loading labels
'
,
()
=>
{
it
(
'
renders disabled search input field
'
,
async
()
=>
{
it
(
'
renders disabled search input field
'
,
async
()
=>
{
...
@@ -109,34 +104,6 @@ describe('DropdownContentsLabelsView', () => {
...
@@ -109,34 +104,6 @@ describe('DropdownContentsLabelsView', () => {
expect
(
findLabelsList
().
exists
()).
toBe
(
true
);
expect
(
findLabelsList
().
exists
()).
toBe
(
true
);
expect
(
findLabels
()).
toHaveLength
(
2
);
expect
(
findLabels
()).
toHaveLength
(
2
);
});
});
it
(
'
changes highlighted label correctly on pressing down button
'
,
async
()
=>
{
expect
(
findLabels
().
at
(
0
).
attributes
(
'
highlight
'
)).
toBeUndefined
();
await
findDropdownWrapper
().
trigger
(
'
keydown.down
'
);
expect
(
findLabels
().
at
(
0
).
attributes
(
'
highlight
'
)).
toBe
(
'
true
'
);
await
findDropdownWrapper
().
trigger
(
'
keydown.down
'
);
expect
(
findLabels
().
at
(
1
).
attributes
(
'
highlight
'
)).
toBe
(
'
true
'
);
expect
(
findLabels
().
at
(
0
).
attributes
(
'
highlight
'
)).
toBeUndefined
();
});
it
(
'
changes highlighted label correctly on pressing up button
'
,
async
()
=>
{
await
findDropdownWrapper
().
trigger
(
'
keydown.down
'
);
await
findDropdownWrapper
().
trigger
(
'
keydown.down
'
);
expect
(
findLabels
().
at
(
1
).
attributes
(
'
highlight
'
)).
toBe
(
'
true
'
);
await
findDropdownWrapper
().
trigger
(
'
keydown.up
'
);
expect
(
findLabels
().
at
(
0
).
attributes
(
'
highlight
'
)).
toBe
(
'
true
'
);
});
it
(
'
changes label selected state when Enter is pressed
'
,
async
()
=>
{
expect
(
findLabels
().
at
(
0
).
attributes
(
'
islabelset
'
)).
toBeUndefined
();
await
findDropdownWrapper
().
trigger
(
'
keydown.down
'
);
await
findDropdownWrapper
().
trigger
(
'
keydown.enter
'
);
expect
(
findLabels
().
at
(
0
).
attributes
(
'
islabelset
'
)).
toBe
(
'
true
'
);
});
});
});
it
(
'
when search returns 0 results
'
,
async
()
=>
{
it
(
'
when search returns 0 results
'
,
async
()
=>
{
...
@@ -164,44 +131,4 @@ describe('DropdownContentsLabelsView', () => {
...
@@ -164,44 +131,4 @@ describe('DropdownContentsLabelsView', () => {
await
waitForPromises
();
await
waitForPromises
();
expect
(
createFlash
).
toHaveBeenCalled
();
expect
(
createFlash
).
toHaveBeenCalled
();
});
});
it
(
'
does not render footer on standalone dropdown
'
,
()
=>
{
createComponent
({
injected
:
{
variant
:
DropdownVariant
.
Standalone
}
});
expect
(
findDropdownFooter
().
exists
()).
toBe
(
false
);
});
it
(
'
renders footer on sidebar dropdown
'
,
()
=>
{
createComponent
();
expect
(
findDropdownFooter
().
exists
()).
toBe
(
true
);
});
it
(
'
renders footer on embedded dropdown
'
,
()
=>
{
createComponent
({
injected
:
{
variant
:
DropdownVariant
.
Embedded
}
});
expect
(
findDropdownFooter
().
exists
()).
toBe
(
true
);
});
it
(
'
does not render create label button if `allowLabelCreate` is false
'
,
()
=>
{
createComponent
({
injected
:
{
allowLabelCreate
:
false
}
});
expect
(
findCreateLabelButton
().
exists
()).
toBe
(
false
);
});
describe
(
'
when `allowLabelCreate` is true
'
,
()
=>
{
beforeEach
(()
=>
{
createComponent
();
});
it
(
'
renders create label button
'
,
()
=>
{
expect
(
findCreateLabelButton
().
exists
()).
toBe
(
true
);
});
it
(
'
emits `toggleDropdownContentsCreateView` event on create label button click
'
,
()
=>
{
findCreateLabelButton
().
vm
.
$emit
(
'
click
'
,
new
MouseEvent
(
'
click
'
));
expect
(
wrapper
.
emitted
(
'
toggleDropdownContentsCreateView
'
)).
toEqual
([[]]);
});
});
});
});
spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_spec.js
View file @
e51b3eed
import
{
shallowMount
,
createLocalVue
}
from
'
@vue/test-utils
'
;
import
{
GlDropdown
}
from
'
@gitlab/ui
'
;
import
{
shallowMount
}
from
'
@vue/test-utils
'
;
import
Vue
from
'
vue
'
;
import
Vuex
from
'
vuex
'
;
import
Vuex
from
'
vuex
'
;
import
{
DropdownVariant
}
from
'
~/vue_shared/components/sidebar/labels_select_widget/constants
'
;
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
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
'
;
import
DropdownContentsLabelsView
from
'
~/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.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
,
mockLabels
}
from
'
./mock_data
'
;
import
{
mockConfig
,
mockLabels
}
from
'
./mock_data
'
;
const
localVue
=
createLocalVue
();
Vue
.
use
(
Vuex
);
localVue
.
use
(
Vuex
);
const
createComponent
=
(
initialState
=
mockConfig
,
defaultProps
=
{})
=>
{
const
store
=
new
Vuex
.
Store
(
labelsSelectModule
());
store
.
dispatch
(
'
setInitialState
'
,
initialState
);
return
shallowMount
(
DropdownContents
,
{
propsData
:
{
...
defaultProps
,
labelsCreateTitle
:
'
test
'
,
selectedLabels
:
mockLabels
,
allowMultiselect
:
true
,
labelsListTitle
:
'
Assign labels
'
,
footerCreateLabelTitle
:
'
create
'
,
footerManageLabelTitle
:
'
manage
'
,
},
localVue
,
store
,
});
};
describe
(
'
DropdownContent
'
,
()
=>
{
describe
(
'
DropdownContent
'
,
()
=>
{
let
wrapper
;
let
wrapper
;
const
createComponent
=
({
initialState
=
mockConfig
,
defaultProps
=
{},
injected
=
{},
}
=
{})
=>
{
const
store
=
new
Vuex
.
Store
(
labelsSelectModule
());
store
.
dispatch
(
'
setInitialState
'
,
initialState
);
wrapper
=
shallowMount
(
DropdownContents
,
{
propsData
:
{
...
defaultProps
,
labelsCreateTitle
:
'
test
'
,
selectedLabels
:
mockLabels
,
allowMultiselect
:
true
,
labelsListTitle
:
'
Assign labels
'
,
footerCreateLabelTitle
:
'
create
'
,
footerManageLabelTitle
:
'
manage
'
,
},
provide
:
{
allowLabelCreate
:
true
,
labelsManagePath
:
'
foo/bar
'
,
...
injected
,
},
store
,
stubs
:
{
GlDropdown
,
},
});
};
beforeEach
(()
=>
{
beforeEach
(()
=>
{
wrapper
=
createComponent
();
createComponent
();
});
});
afterEach
(()
=>
{
afterEach
(()
=>
{
wrapper
.
destroy
();
wrapper
.
destroy
();
});
});
describe
(
'
computed
'
,
()
=>
{
const
findDropdownFooter
=
()
=>
wrapper
.
find
(
'
[data-testid="dropdown-footer"]
'
);
describe
(
'
dropdownContentsView
'
,
()
=>
{
const
findCreateLabelButton
=
()
=>
wrapper
.
find
(
'
[data-testid="create-label-button"]
'
);
it
(
'
returns string "dropdown-contents-create-view" when `showDropdownContentsCreateView` prop is `true`
'
,
()
=>
{
const
findGoBackButton
=
()
=>
wrapper
.
find
(
'
[data-testid="go-back-button"]
'
);
wrapper
.
vm
.
$store
.
dispatch
(
'
toggleDropdownContentsCreateView
'
);
expect
(
wrapper
.
vm
.
dropdownContentsView
).
toBe
(
'
dropdown-contents-create-view
'
);
describe
(
'
Create view
'
,
()
=>
{
});
beforeEach
(()
=>
{
wrapper
.
vm
.
$store
.
dispatch
(
'
toggleDropdownContentsCreateView
'
);
});
it
(
'
returns string "dropdown-contents-labels-view" when `showDropdownContentsCreateView` prop is `false`
'
,
()
=>
{
it
(
'
renders create view when `showDropdownContentsCreateView` prop is `true`
'
,
()
=>
{
expect
(
wrapper
.
vm
.
dropdownContentsView
).
toBe
(
'
dropdown-contents-labels-view
'
);
expect
(
wrapper
.
findComponent
(
DropdownContentsCreateView
).
exists
()).
toBe
(
true
);
});
});
it
(
'
does not render footer
'
,
()
=>
{
expect
(
findDropdownFooter
().
exists
()).
toBe
(
false
);
});
it
(
'
does not render create label button
'
,
()
=>
{
expect
(
findCreateLabelButton
().
exists
()).
toBe
(
false
);
});
it
(
'
renders go back button
'
,
()
=>
{
expect
(
findGoBackButton
().
exists
()).
toBe
(
true
);
});
});
});
});
describe
(
'
template
'
,
()
=>
{
describe
(
'
Labels view
'
,
()
=>
{
it
(
'
renders component container element with class `labels-select-dropdown-contents` and no styles
'
,
()
=>
{
it
(
'
renders labels view when `showDropdownContentsCreateView` when `showDropdownContentsCreateView` prop is `false`
'
,
()
=>
{
expect
(
wrapper
.
attributes
(
'
class
'
)).
toContain
(
'
labels-select-dropdown-contents
'
);
expect
(
wrapper
.
findComponent
(
DropdownContentsLabelsView
).
exists
()).
toBe
(
true
);
expect
(
wrapper
.
attributes
(
'
style
'
)).
toBeUndefined
();
});
it
(
'
renders footer on sidebar dropdown
'
,
()
=>
{
expect
(
findDropdownFooter
().
exists
()).
toBe
(
true
);
});
it
(
'
does not render footer on standalone dropdown
'
,
()
=>
{
createComponent
({
initialState
:
{
...
mockConfig
,
variant
:
DropdownVariant
.
Standalone
}
});
expect
(
findDropdownFooter
().
exists
()).
toBe
(
false
);
});
it
(
'
renders footer on embedded dropdown
'
,
()
=>
{
createComponent
({
initialState
:
{
...
mockConfig
,
variant
:
DropdownVariant
.
Embedded
}
});
expect
(
findDropdownFooter
().
exists
()).
toBe
(
true
);
});
});
describe
(
'
when `renderOnTop` is true
'
,
()
=>
{
it
(
'
does not render go back button
'
,
()
=>
{
it
.
each
`
expect
(
findGoBackButton
().
exists
()).
toBe
(
false
);
variant | expected
});
${
DropdownVariant
.
Sidebar
}
|
${
'
bottom: 3rem
'
}
${
DropdownVariant
.
Standalone
}
|
${
'
bottom: 2rem
'
}
it
(
'
does not render create label button if `allowLabelCreate` is false
'
,
()
=>
{
${
DropdownVariant
.
Embedded
}
|
${
'
bottom: 2rem
'
}
createComponent
({
injected
:
{
allowLabelCreate
:
false
}
});
`
(
'
renders upward for $variant variant
'
,
({
variant
,
expected
})
=>
{
wrapper
=
createComponent
({
...
mockConfig
,
variant
},
{
renderOnTop
:
true
});
expect
(
wrapper
.
attributes
(
'
style
'
)).
toContain
(
expected
);
expect
(
findCreateLabelButton
().
exists
()).
toBe
(
false
);
});
describe
(
'
when `allowLabelCreate` is true
'
,
()
=>
{
beforeEach
(()
=>
{
createComponent
();
});
});
it
(
'
renders create label button
'
,
()
=>
{
expect
(
findCreateLabelButton
().
exists
()).
toBe
(
true
);
});
it
(
'
triggers `toggleDropdownContent` method on create label button click
'
,
()
=>
{
jest
.
spyOn
(
wrapper
.
vm
,
'
toggleDropdownContent
'
).
mockImplementation
(()
=>
{});
findCreateLabelButton
().
trigger
(
'
click
'
);
expect
(
wrapper
.
vm
.
toggleDropdownContent
).
toHaveBeenCalled
();
});
});
});
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
();
});
});
});
});
});
});
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