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
97ab8539
Commit
97ab8539
authored
Apr 02, 2019
by
Rajat Jain
Committed by
Jan Provaznik
Apr 04, 2019
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
[frontend] backport of scoped labels
Scoped labels in EE require additional changes in CE code.
parent
d0a0d3d3
Changes
20
Hide whitespace changes
Inline
Side-by-side
Showing
20 changed files
with
467 additions
and
65 deletions
+467
-65
app/assets/javascripts/boards/models/issue.js
app/assets/javascripts/boards/models/issue.js
+11
-1
app/assets/javascripts/labels_select.js
app/assets/javascripts/labels_select.js
+90
-5
app/assets/javascripts/pages/groups/labels/edit/index.js
app/assets/javascripts/pages/groups/labels/edit/index.js
+1
-1
app/assets/javascripts/pages/groups/labels/new/index.js
app/assets/javascripts/pages/groups/labels/new/index.js
+1
-1
app/assets/javascripts/pages/projects/labels/edit/index.js
app/assets/javascripts/pages/projects/labels/edit/index.js
+1
-1
app/assets/javascripts/pages/projects/labels/new/index.js
app/assets/javascripts/pages/projects/labels/new/index.js
+1
-1
app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue
...ipts/vue_shared/components/sidebar/labels_select/base.vue
+18
-1
app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button.vue
...ared/components/sidebar/labels_select/dropdown_button.vue
+12
-0
app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value.vue
...hared/components/sidebar/labels_select/dropdown_value.vue
+40
-15
app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_regular_label.vue
...ts/sidebar/labels_select/dropdown_value_regular_label.vue
+35
-0
app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_scoped_label.vue
...nts/sidebar/labels_select/dropdown_value_scoped_label.vue
+47
-0
app/assets/stylesheets/pages/issuable.scss
app/assets/stylesheets/pages/issuable.scss
+10
-0
app/assets/stylesheets/pages/labels.scss
app/assets/stylesheets/pages/labels.scss
+36
-0
locale/gitlab.pot
locale/gitlab.pot
+3
-0
spec/frontend/labels_select_spec.js
spec/frontend/labels_select_spec.js
+90
-26
spec/javascripts/boards/issue_spec.js
spec/javascripts/boards/issue_spec.js
+2
-0
spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button_spec.js
.../components/sidebar/labels_select/dropdown_button_spec.js
+12
-3
spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js
...ts/sidebar/labels_select/dropdown_value_collapsed_spec.js
+14
-6
spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js
...d/components/sidebar/labels_select/dropdown_value_spec.js
+36
-4
spec/javascripts/vue_shared/components/sidebar/labels_select/mock_data.js
.../vue_shared/components/sidebar/labels_select/mock_data.js
+7
-0
No files found.
app/assets/javascripts/boards/models/issue.js
View file @
97ab8539
...
@@ -119,7 +119,17 @@ class ListIssue {
...
@@ -119,7 +119,17 @@ class ListIssue {
}
}
const
projectPath
=
this
.
project
?
this
.
project
.
path
:
''
;
const
projectPath
=
this
.
project
?
this
.
project
.
path
:
''
;
return
Vue
.
http
.
patch
(
`
${
this
.
path
}
.json`
,
data
);
return
Vue
.
http
.
patch
(
`
${
this
.
path
}
.json`
,
data
).
then
(({
body
=
{}
}
=
{})
=>
{
/**
* Since post implementation of Scoped labels, server can reject
* same key-ed labels. To keep the UI and server Model consistent,
* we're just assigning labels that server echo's back to us when we
* PATCH the said object.
*/
if
(
body
)
{
this
.
labels
=
body
.
labels
;
}
});
}
}
}
}
...
...
app/assets/javascripts/labels_select.js
View file @
97ab8539
...
@@ -11,6 +11,7 @@ import CreateLabelDropdown from './create_label';
...
@@ -11,6 +11,7 @@ import CreateLabelDropdown from './create_label';
import
flash
from
'
./flash
'
;
import
flash
from
'
./flash
'
;
import
ModalStore
from
'
./boards/stores/modal_store
'
;
import
ModalStore
from
'
./boards/stores/modal_store
'
;
import
boardsStore
from
'
./boards/stores/boards_store
'
;
import
boardsStore
from
'
./boards/stores/boards_store
'
;
import
{
isEE
}
from
'
~/lib/utils/common_utils
'
;
export
default
class
LabelsSelect
{
export
default
class
LabelsSelect
{
constructor
(
els
,
options
=
{})
{
constructor
(
els
,
options
=
{})
{
...
@@ -86,8 +87,9 @@ export default class LabelsSelect {
...
@@ -86,8 +87,9 @@ export default class LabelsSelect {
return
this
.
value
;
return
this
.
value
;
})
})
.
get
();
.
get
();
const
scopedLabels
=
$dropdown
.
data
(
'
scopedLabels
'
);
const
scopedLabelsDocumentationLink
=
$dropdown
.
data
(
'
scopedLabelsDocumentationLink
'
);
const
{
handleClick
}
=
options
;
const
{
handleClick
}
=
options
;
$sidebarLabelTooltip
.
tooltip
();
$sidebarLabelTooltip
.
tooltip
();
if
(
$dropdown
.
closest
(
'
.dropdown
'
).
find
(
'
.dropdown-new-label
'
).
length
)
{
if
(
$dropdown
.
closest
(
'
.dropdown
'
).
find
(
'
.dropdown-new-label
'
).
length
)
{
...
@@ -132,8 +134,48 @@ export default class LabelsSelect {
...
@@ -132,8 +134,48 @@ export default class LabelsSelect {
template
=
LabelsSelect
.
getLabelTemplate
({
template
=
LabelsSelect
.
getLabelTemplate
({
labels
:
data
.
labels
,
labels
:
data
.
labels
,
issueUpdateURL
,
issueUpdateURL
,
enableScopedLabels
:
scopedLabels
,
scopedLabelsDocumentationLink
,
});
});
labelCount
=
data
.
labels
.
length
;
labelCount
=
data
.
labels
.
length
;
// EE Specific
if
(
isEE
)
{
/**
* For Scoped labels, the last label selected with the
* same key will be applied to the current issueable.
*
* If these are the labels - priority::1, priority::2; and if
* we apply them in the same order, only priority::2 will stick
* with the issuable.
*
* In the current dropdown implementation, we keep track of all
* the labels selected via a hidden DOM element. Since a User
* can select priority::1 and priority::2 at the same time, the
* DOM will have 2 hidden input and the dropdown will show both
* the items selected but in reality server only applied
* priority::2.
*
* We find all the labels then find all the labels server accepted
* and then remove the excess ones.
*/
const
toRemoveIds
=
Array
.
from
(
$form
.
find
(
"
input[type='hidden'][name='
"
+
fieldName
+
"
']
"
),
)
.
map
(
el
=>
el
.
value
)
.
map
(
Number
);
data
.
labels
.
forEach
(
label
=>
{
const
index
=
toRemoveIds
.
indexOf
(
label
.
id
);
toRemoveIds
.
splice
(
index
,
1
);
});
toRemoveIds
.
forEach
(
id
=>
{
$form
.
find
(
"
input[type='hidden'][name='
"
+
fieldName
+
"
'][value='
"
+
id
+
"
']
"
)
.
remove
();
});
}
}
else
{
}
else
{
template
=
'
<span class="no-value">None</span>
'
;
template
=
'
<span class="no-value">None</span>
'
;
}
}
...
@@ -358,6 +400,7 @@ export default class LabelsSelect {
...
@@ -358,6 +400,7 @@ export default class LabelsSelect {
}
else
{
}
else
{
if
(
!
$dropdown
.
hasClass
(
'
js-filter-bulk-update
'
))
{
if
(
!
$dropdown
.
hasClass
(
'
js-filter-bulk-update
'
))
{
saveLabelData
();
saveLabelData
();
$dropdown
.
data
(
'
glDropdown
'
).
clearMenu
();
}
}
}
}
}
}
...
@@ -471,19 +514,61 @@ export default class LabelsSelect {
...
@@ -471,19 +514,61 @@ export default class LabelsSelect {
// so best approach is to use traditional way of
// so best approach is to use traditional way of
// concatenation
// concatenation
// see: http://2ality.com/2016/05/template-literal-whitespace.html#joining-arrays
// see: http://2ality.com/2016/05/template-literal-whitespace.html#joining-arrays
const
tpl
=
_
.
template
(
const
labelTemplate
=
_
.
template
(
[
[
'
<% _.each(labels, function(label){ %>
'
,
'
<a href="<%- issueUpdateURL.slice(0, issueUpdateURL.lastIndexOf("/")) %>?label_name[]=<%- encodeURIComponent(label.title) %>">
'
,
'
<a href="<%- issueUpdateURL.slice(0, issueUpdateURL.lastIndexOf("/")) %>?label_name[]=<%- encodeURIComponent(label.title) %>">
'
,
'
<span class="badge label has-tooltip color-label"
title="<%- label.description
%>" style="background-color: <%- label.color %>; color: <%- label.text_color %>;">
'
,
'
<span class="badge label has-tooltip color-label"
<%= linkAttrs %> title="<%= tooltipTitleTemplate({ label, isScopedLabel, enableScopedLabels })
%>" style="background-color: <%- label.color %>; color: <%- label.text_color %>;">
'
,
'
<%- label.title %>
'
,
'
<%- label.title %>
'
,
'
</span>
'
,
'
</span>
'
,
'
</a>
'
,
'
</a>
'
,
].
join
(
''
),
);
const
infoIconTemplate
=
_
.
template
(
[
'
<a href="<%= scopedLabelsDocumentationLink %>" class="label scoped-label" target="_blank" rel="noopener">
'
,
'
<i class="fa fa-question-circle" style="background-color: <%- label.color %>; color: <%- label.text_color %>;"></i>
'
,
'
</a>
'
,
].
join
(
''
),
);
const
tooltipTitleTemplate
=
_
.
template
(
[
'
<% if (isScopedLabel(label) && enableScopedLabels) { %>
'
,
"
<span class='font-weight-bold scoped-label-tooltip-title'>Scoped label</span>
"
,
'
<br />
'
,
'
<%- label.description %>
'
,
'
<% } else { %>
'
,
'
<%- label.description %>
'
,
'
<% } %>
'
,
].
join
(
''
),
);
const
isScopedLabel
=
label
=>
label
.
title
.
indexOf
(
'
::
'
)
!==
-
1
;
const
tpl
=
_
.
template
(
[
'
<% _.each(labels, function(label){ %>
'
,
'
<% if (isScopedLabel(label) && enableScopedLabels) { %>
'
,
'
<span class="d-inline-block position-relative scoped-label-wrapper">
'
,
'
<%= labelTemplate({ label, issueUpdateURL, isScopedLabel, enableScopedLabels, tooltipTitleTemplate, linkAttrs:
\'
data-html="true"
\'
}) %>
'
,
'
<%= infoIconTemplate({ label,scopedLabelsDocumentationLink }) %>
'
,
'
</span>
'
,
'
<% } else { %>
'
,
'
<%= labelTemplate({ label, issueUpdateURL, isScopedLabel, enableScopedLabels, tooltipTitleTemplate, linkAttrs: "" }) %>
'
,
'
<% } %>
'
,
'
<% }); %>
'
,
'
<% }); %>
'
,
].
join
(
''
),
].
join
(
''
),
);
);
return
tpl
(
tplData
);
return
tpl
({
...
tplData
,
labelTemplate
,
infoIconTemplate
,
tooltipTitleTemplate
,
isScopedLabel
,
});
}
}
bindEvents
()
{
bindEvents
()
{
...
...
app/assets/javascripts/pages/groups/labels/edit/index.js
View file @
97ab8539
import
Labels
from
'
~
/labels
'
;
import
Labels
from
'
ee_else_ce
/labels
'
;
document
.
addEventListener
(
'
DOMContentLoaded
'
,
()
=>
new
Labels
());
document
.
addEventListener
(
'
DOMContentLoaded
'
,
()
=>
new
Labels
());
app/assets/javascripts/pages/groups/labels/new/index.js
View file @
97ab8539
import
Labels
from
'
~
/labels
'
;
import
Labels
from
'
ee_else_ce
/labels
'
;
document
.
addEventListener
(
'
DOMContentLoaded
'
,
()
=>
new
Labels
());
document
.
addEventListener
(
'
DOMContentLoaded
'
,
()
=>
new
Labels
());
app/assets/javascripts/pages/projects/labels/edit/index.js
View file @
97ab8539
import
Labels
from
'
~
/labels
'
;
import
Labels
from
'
ee_else_ce
/labels
'
;
document
.
addEventListener
(
'
DOMContentLoaded
'
,
()
=>
new
Labels
());
document
.
addEventListener
(
'
DOMContentLoaded
'
,
()
=>
new
Labels
());
app/assets/javascripts/pages/projects/labels/new/index.js
View file @
97ab8539
import
Labels
from
'
~
/labels
'
;
import
Labels
from
'
ee_else_ce
/labels
'
;
document
.
addEventListener
(
'
DOMContentLoaded
'
,
()
=>
new
Labels
());
document
.
addEventListener
(
'
DOMContentLoaded
'
,
()
=>
new
Labels
());
app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue
View file @
97ab8539
...
@@ -75,6 +75,16 @@ export default {
...
@@ -75,6 +75,16 @@ export default {
required
:
false
,
required
:
false
,
default
:
false
,
default
:
false
,
},
},
enableScopedLabels
:
{
type
:
Boolean
,
require
:
false
,
default
:
false
,
},
scopedLabelsDocumentationLink
:
{
type
:
String
,
require
:
false
,
default
:
'
#
'
,
},
},
},
computed
:
{
computed
:
{
hiddenInputName
()
{
hiddenInputName
()
{
...
@@ -123,7 +133,12 @@ export default {
...
@@ -123,7 +133,12 @@ export default {
@
onValueClick=
"handleCollapsedValueClick"
@
onValueClick=
"handleCollapsedValueClick"
/>
/>
<dropdown-title
:can-edit=
"canEdit"
/>
<dropdown-title
:can-edit=
"canEdit"
/>
<dropdown-value
:labels=
"context.labels"
:label-filter-base-path=
"labelFilterBasePath"
>
<dropdown-value
:labels=
"context.labels"
:label-filter-base-path=
"labelFilterBasePath"
:scoped-labels-documentation-link=
"scopedLabelsDocumentationLink"
:enable-scoped-labels=
"enableScopedLabels"
>
<slot></slot>
<slot></slot>
</dropdown-value>
</dropdown-value>
<div
v-if=
"canEdit"
class=
"selectbox js-selectbox"
style=
"display: none;"
>
<div
v-if=
"canEdit"
class=
"selectbox js-selectbox"
style=
"display: none;"
>
...
@@ -142,6 +157,8 @@ export default {
...
@@ -142,6 +157,8 @@ export default {
:namespace=
"namespace"
:namespace=
"namespace"
:labels=
"context.labels"
:labels=
"context.labels"
:show-extra-options=
"!showCreate"
:show-extra-options=
"!showCreate"
:scoped-labels-documentation-link=
"scopedLabelsDocumentationLink"
:enable-scoped-labels=
"enableScopedLabels"
/>
/>
<div
<div
class=
"dropdown-menu dropdown-select dropdown-menu-paging
class=
"dropdown-menu dropdown-select dropdown-menu-paging
...
...
app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button.vue
View file @
97ab8539
...
@@ -31,6 +31,16 @@ export default {
...
@@ -31,6 +31,16 @@ export default {
type
:
Boolean
,
type
:
Boolean
,
required
:
true
,
required
:
true
,
},
},
enableScopedLabels
:
{
type
:
Boolean
,
require
:
false
,
default
:
false
,
},
scopedLabelsDocumentationLink
:
{
type
:
String
,
require
:
false
,
default
:
'
#
'
,
},
},
},
computed
:
{
computed
:
{
dropdownToggleText
()
{
dropdownToggleText
()
{
...
@@ -61,6 +71,8 @@ export default {
...
@@ -61,6 +71,8 @@ export default {
:data-labels="labelsPath"
:data-labels="labelsPath"
:data-namespace-path="namespace"
:data-namespace-path="namespace"
:data-show-any="showExtraOptions"
:data-show-any="showExtraOptions"
:data-scoped-labels="enableScopedLabels"
:data-scoped-labels-documentation-link="scopedLabelsDocumentationLink"
type="button"
type="button"
class="dropdown-menu-toggle wide js-label-select js-multiselect js-context-config-modal"
class="dropdown-menu-toggle wide js-label-select js-multiselect js-context-config-modal"
data-toggle="dropdown"
data-toggle="dropdown"
...
...
app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value.vue
View file @
97ab8539
<
script
>
<
script
>
import
tooltip
from
'
~/vue_shared/directives/tooltip
'
;
import
DropdownValueScopedLabel
from
'
./dropdown_value_scoped_label.vue
'
;
import
DropdownValueRegularLabel
from
'
./dropdown_value_regular_label.vue
'
;
export
default
{
export
default
{
directives
:
{
components
:
{
tooltip
,
DropdownValueScopedLabel
,
DropdownValueRegularLabel
,
},
},
props
:
{
props
:
{
labels
:
{
labels
:
{
...
@@ -14,6 +16,16 @@ export default {
...
@@ -14,6 +16,16 @@ export default {
type
:
String
,
type
:
String
,
required
:
true
,
required
:
true
,
},
},
enableScopedLabels
:
{
type
:
Boolean
,
required
:
false
,
default
:
false
,
},
scopedLabelsDocumentationLink
:
{
type
:
String
,
required
:
false
,
default
:
'
#
'
,
},
},
},
computed
:
{
computed
:
{
isEmpty
()
{
isEmpty
()
{
...
@@ -30,6 +42,12 @@ export default {
...
@@ -30,6 +42,12 @@ export default {
backgroundColor
:
label
.
color
,
backgroundColor
:
label
.
color
,
};
};
},
},
scopedLabelsDescription
({
description
=
''
})
{
return
`<span class="font-weight-bold scoped-label-tooltip-title">Scoped label</span><br />
${
description
}
`
;
},
showScopedLabels
({
title
=
''
})
{
return
this
.
enableScopedLabels
&&
title
.
indexOf
(
'
::
'
)
!==
-
1
;
},
},
},
};
};
</
script
>
</
script
>
...
@@ -44,17 +62,24 @@ export default {
...
@@ -44,17 +62,24 @@ export default {
<span
v-if=
"isEmpty"
class=
"text-secondary"
>
<span
v-if=
"isEmpty"
class=
"text-secondary"
>
<slot>
{{
__
(
'
None
'
)
}}
</slot>
<slot>
{{
__
(
'
None
'
)
}}
</slot>
</span>
</span>
<a
v-for=
"label in labels"
v-else
:key=
"label.id"
:href=
"labelFilterUrl(label)"
>
<span
<template
v-for=
"label in labels"
v-else
>
v-tooltip
<dropdown-value-scoped-label
:style=
"labelStyle(label)"
v-if=
"showScopedLabels(label)"
:title=
"label.description"
:key=
"label.id"
class=
"badge color-label"
:label=
"label"
data-placement=
"bottom"
:label-filter-url=
"labelFilterUrl(label)"
data-container=
"body"
:label-style=
"labelStyle(label)"
>
:scoped-labels-documentation-link=
"scopedLabelsDocumentationLink"
{{
label
.
title
}}
/>
</span>
</a>
<dropdown-value-regular-label
v-else
:key=
"label.id"
:label=
"label"
:label-filter-url=
"labelFilterUrl(label)"
:label-style=
"labelStyle(label)"
/>
</
template
>
</div>
</div>
</template>
</template>
app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_regular_label.vue
0 → 100644
View file @
97ab8539
<
script
>
import
{
GlLink
,
GlTooltip
}
from
'
@gitlab/ui
'
;
export
default
{
components
:
{
GlTooltip
,
GlLink
,
},
props
:
{
label
:
{
type
:
Object
,
required
:
true
,
},
labelStyle
:
{
type
:
Object
,
required
:
true
,
},
labelFilterUrl
:
{
type
:
String
,
required
:
true
,
},
},
};
</
script
>
<
template
>
<a
ref=
"regularLabelRef"
:href=
"labelFilterUrl"
>
<span
:style=
"labelStyle"
class=
"badge color-label"
>
{{
label
.
title
}}
</span>
<gl-tooltip
:target=
"() => $refs.regularLabelRef"
placement=
"top"
boundary=
"viewport"
>
{{
label
.
description
}}
</gl-tooltip>
</a>
</
template
>
app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_scoped_label.vue
0 → 100644
View file @
97ab8539
<
script
>
import
{
GlLink
,
GlTooltip
}
from
'
@gitlab/ui
'
;
export
default
{
components
:
{
GlTooltip
,
GlLink
,
},
props
:
{
label
:
{
type
:
Object
,
required
:
true
,
},
labelStyle
:
{
type
:
Object
,
required
:
true
,
},
scopedLabelsDocumentationLink
:
{
type
:
String
,
required
:
true
,
},
labelFilterUrl
:
{
type
:
String
,
required
:
true
,
},
},
};
</
script
>
<
template
>
<span
class=
"d-inline-block position-relative scoped-label-wrapper"
>
<a
:href=
"labelFilterUrl"
>
<span
:ref=
"`labelTitleRef`"
:style=
"labelStyle"
class=
"badge color-label label"
>
{{
label
.
title
}}
</span>
<gl-tooltip
:target=
"() => $refs.labelTitleRef"
placement=
"top"
boundary=
"viewport"
>
<span
class=
"font-weight-bold scoped-label-tooltip-title"
>
{{
__
(
'
Scoped label
'
)
}}
</span
><br
/>
{{
label
.
description
}}
</gl-tooltip>
</a>
<gl-link
:href=
"scopedLabelsDocumentationLink"
target=
"_blank"
class=
"label scoped-label"
><i
class=
"fa fa-question-circle"
:style=
"labelStyle"
></i
></gl-link>
</span>
</
template
>
app/assets/stylesheets/pages/issuable.scss
View file @
97ab8539
...
@@ -110,6 +110,16 @@
...
@@ -110,6 +110,16 @@
font-size
:
0
;
font-size
:
0
;
margin-bottom
:
-5px
;
margin-bottom
:
-5px
;
}
}
.scoped-label-wrapper
{
.color-label
{
padding-right
:
$gl-padding-24
;
}
.scoped-label
{
right
:
12px
;
}
}
}
}
.right-sidebar
{
.right-sidebar
{
...
...
app/assets/stylesheets/pages/labels.scss
View file @
97ab8539
...
@@ -402,3 +402,39 @@
...
@@ -402,3 +402,39 @@
.priority-labels-empty-state
.svg-content
img
{
.priority-labels-empty-state
.svg-content
img
{
max-width
:
$priority-label-empty-state-width
;
max-width
:
$priority-label-empty-state-width
;
}
}
.scoped-label-tooltip-title
{
color
:
$indigo-300
;
}
.scoped-label-wrapper
{
&
.label-link
.color-label
a
{
color
:
inherit
;
}
.color-label
{
padding-right
:
$gl-padding-24
;
}
.scoped-label
{
position
:
absolute
;
top
:
4px
;
right
:
8px
;
padding
:
0
;
margin
:
0
;
line-height
:
$gl-line-height
;
}
}
// Label inside title of Delete Label Modal
.modal-header
.page-title
{
.scoped-label-wrapper
{
.scoped-label
{
line-height
:
20px
;
}
span
.color-label
{
padding-right
:
$gl-padding-24
;
}
}
}
locale/gitlab.pot
View file @
97ab8539
...
@@ -6974,6 +6974,9 @@ msgstr ""
...
@@ -6974,6 +6974,9 @@ msgstr ""
msgid "Scope not supported with disabled 'users_search' feature!"
msgid "Scope not supported with disabled 'users_search' feature!"
msgstr ""
msgstr ""
msgid "Scoped label"
msgstr ""
msgid "Scroll down to <strong>Google Code Project Hosting</strong> and enable the switch on the right."
msgid "Scroll down to <strong>Google Code Project Hosting</strong> and enable the switch on the right."
msgstr ""
msgstr ""
...
...
spec/frontend/labels_select_spec.js
View file @
97ab8539
...
@@ -13,40 +13,104 @@ const mockLabels = [
...
@@ -13,40 +13,104 @@ const mockLabels = [
},
},
];
];
const
mockScopedLabels
=
[
{
id
:
27
,
title
:
'
Foo::Bar
'
,
description
:
'
Foobar
'
,
color
:
'
#333ABC
'
,
text_color
:
'
#FFFFFF
'
,
},
];
describe
(
'
LabelsSelect
'
,
()
=>
{
describe
(
'
LabelsSelect
'
,
()
=>
{
describe
(
'
getLabelTemplate
'
,
()
=>
{
describe
(
'
getLabelTemplate
'
,
()
=>
{
const
label
=
mockLabels
[
0
];
describe
(
'
when normal label is present
'
,
()
=>
{
let
$labelEl
;
const
label
=
mockLabels
[
0
];
let
$labelEl
;
beforeEach
(()
=>
{
$labelEl
=
$
(
LabelsSelect
.
getLabelTemplate
({
labels
:
mockLabels
,
issueUpdateURL
:
mockUrl
,
}),
);
});
it
(
'
generated label item template has correct label URL
'
,
()
=>
{
beforeEach
(()
=>
{
expect
(
$labelEl
.
attr
(
'
href
'
)).
toBe
(
'
/foo/bar?label_name[]=Foo%20Label
'
);
$labelEl
=
$
(
});
LabelsSelect
.
getLabelTemplate
({
labels
:
mockLabels
,
issueUpdateURL
:
mockUrl
,
enableScopedLabels
:
true
,
scopedLabelsDocumentationLink
:
'
docs-link
'
,
}),
);
});
it
(
'
generated label item template has correct label title
'
,
()
=>
{
it
(
'
generated label item template has correct label URL
'
,
()
=>
{
expect
(
$labelEl
.
find
(
'
span.label
'
).
text
()).
toBe
(
label
.
title
);
expect
(
$labelEl
.
attr
(
'
href
'
)).
toBe
(
'
/foo/bar?label_name[]=Foo%20Label
'
);
});
});
it
(
'
generated label item template has label description as title attribute
'
,
()
=>
{
it
(
'
generated label item template has correct label title
'
,
()
=>
{
expect
(
$labelEl
.
find
(
'
span.label
'
).
attr
(
'
title
'
)).
toBe
(
label
.
description
);
expect
(
$labelEl
.
find
(
'
span.label
'
).
text
()).
toBe
(
label
.
title
);
});
});
it
(
'
generated label item template has label description as title attribute
'
,
()
=>
{
expect
(
$labelEl
.
find
(
'
span.label
'
).
attr
(
'
title
'
)).
toBe
(
label
.
description
);
});
it
(
'
generated label item template has correct label styles
'
,
()
=>
{
it
(
'
generated label item template has correct label styles
'
,
()
=>
{
expect
(
$labelEl
.
find
(
'
span.label
'
).
attr
(
'
style
'
)).
toBe
(
expect
(
$labelEl
.
find
(
'
span.label
'
).
attr
(
'
style
'
)).
toBe
(
`background-color:
${
label
.
color
}
; color:
${
label
.
text_color
}
;`
,
`background-color:
${
label
.
color
}
; color:
${
label
.
text_color
}
;`
,
);
);
});
it
(
'
generated label item has a badge class
'
,
()
=>
{
expect
(
$labelEl
.
find
(
'
span
'
).
hasClass
(
'
badge
'
)).
toEqual
(
true
);
});
it
(
'
generated label item template does not have scoped-label class
'
,
()
=>
{
expect
(
$labelEl
.
find
(
'
.scoped-label
'
)).
toHaveLength
(
0
);
});
});
});
it
(
'
generated label item has a badge class
'
,
()
=>
{
describe
(
'
when scoped label is present
'
,
()
=>
{
expect
(
$labelEl
.
find
(
'
span
'
).
hasClass
(
'
badge
'
)).
toEqual
(
true
);
const
label
=
mockScopedLabels
[
0
];
let
$labelEl
;
beforeEach
(()
=>
{
$labelEl
=
$
(
LabelsSelect
.
getLabelTemplate
({
labels
:
mockScopedLabels
,
issueUpdateURL
:
mockUrl
,
enableScopedLabels
:
true
,
scopedLabelsDocumentationLink
:
'
docs-link
'
,
}),
);
});
it
(
'
generated label item template has correct label URL
'
,
()
=>
{
expect
(
$labelEl
.
find
(
'
a
'
).
attr
(
'
href
'
)).
toBe
(
'
/foo/bar?label_name[]=Foo%3A%3ABar
'
);
});
it
(
'
generated label item template has correct label title
'
,
()
=>
{
expect
(
$labelEl
.
find
(
'
span.label
'
).
text
()).
toBe
(
label
.
title
);
});
it
(
'
generated label item template has html flag as true
'
,
()
=>
{
expect
(
$labelEl
.
find
(
'
span.label
'
).
attr
(
'
data-html
'
)).
toBe
(
'
true
'
);
});
it
(
'
generated label item template has question icon
'
,
()
=>
{
expect
(
$labelEl
.
find
(
'
i.fa-question-circle
'
)).
toHaveLength
(
1
);
});
it
(
'
generated label item template has scoped-label class
'
,
()
=>
{
expect
(
$labelEl
.
find
(
'
.scoped-label
'
)).
toHaveLength
(
1
);
});
it
(
'
generated label item template has correct label styles
'
,
()
=>
{
expect
(
$labelEl
.
find
(
'
span.label
'
).
attr
(
'
style
'
)).
toBe
(
`background-color:
${
label
.
color
}
; color:
${
label
.
text_color
}
;`
,
);
});
it
(
'
generated label item has a badge class
'
,
()
=>
{
expect
(
$labelEl
.
find
(
'
span
'
).
hasClass
(
'
badge
'
)).
toEqual
(
true
);
});
});
});
});
});
});
});
spec/javascripts/boards/issue_spec.js
View file @
97ab8539
...
@@ -178,6 +178,7 @@ describe('Issue model', () => {
...
@@ -178,6 +178,7 @@ describe('Issue model', () => {
spyOn
(
Vue
.
http
,
'
patch
'
).
and
.
callFake
((
url
,
data
)
=>
{
spyOn
(
Vue
.
http
,
'
patch
'
).
and
.
callFake
((
url
,
data
)
=>
{
expect
(
data
.
issue
.
assignee_ids
).
toEqual
([
1
]);
expect
(
data
.
issue
.
assignee_ids
).
toEqual
([
1
]);
done
();
done
();
return
Promise
.
resolve
();
});
});
issue
.
update
(
'
url
'
);
issue
.
update
(
'
url
'
);
...
@@ -187,6 +188,7 @@ describe('Issue model', () => {
...
@@ -187,6 +188,7 @@ describe('Issue model', () => {
spyOn
(
Vue
.
http
,
'
patch
'
).
and
.
callFake
((
url
,
data
)
=>
{
spyOn
(
Vue
.
http
,
'
patch
'
).
and
.
callFake
((
url
,
data
)
=>
{
expect
(
data
.
issue
.
assignee_ids
).
toEqual
([
0
]);
expect
(
data
.
issue
.
assignee_ids
).
toEqual
([
0
]);
done
();
done
();
return
Promise
.
resolve
();
});
});
issue
.
removeAllAssignees
();
issue
.
removeAllAssignees
();
...
...
spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button_spec.js
View file @
97ab8539
...
@@ -45,12 +45,21 @@ describe('DropdownButtonComponent', () => {
...
@@ -45,12 +45,21 @@ describe('DropdownButtonComponent', () => {
});
});
const
vmMoreLabels
=
createComponent
(
mockMoreLabels
);
const
vmMoreLabels
=
createComponent
(
mockMoreLabels
);
expect
(
vmMoreLabels
.
dropdownToggleText
).
toBe
(
'
Foo Label +1 more
'
);
expect
(
vmMoreLabels
.
dropdownToggleText
).
toBe
(
`Foo Label +
${
mockMoreLabels
.
labels
.
length
-
1
}
more`
,
);
vmMoreLabels
.
$destroy
();
vmMoreLabels
.
$destroy
();
});
});
it
(
'
returns first label name when `labels` prop has only one item present
'
,
()
=>
{
it
(
'
returns first label name when `labels` prop has only one item present
'
,
()
=>
{
expect
(
vm
.
dropdownToggleText
).
toBe
(
'
Foo Label
'
);
const
singleLabel
=
Object
.
assign
({},
componentConfig
,
{
labels
:
[
mockLabels
[
0
]],
});
const
vmSingleLabel
=
createComponent
(
singleLabel
);
expect
(
vmSingleLabel
.
dropdownToggleText
).
toBe
(
mockLabels
[
0
].
title
);
vmSingleLabel
.
$destroy
();
});
});
});
});
});
});
...
@@ -73,7 +82,7 @@ describe('DropdownButtonComponent', () => {
...
@@ -73,7 +82,7 @@ describe('DropdownButtonComponent', () => {
const
dropdownToggleTextEl
=
vm
.
$el
.
querySelector
(
'
.dropdown-toggle-text
'
);
const
dropdownToggleTextEl
=
vm
.
$el
.
querySelector
(
'
.dropdown-toggle-text
'
);
expect
(
dropdownToggleTextEl
).
not
.
toBeNull
();
expect
(
dropdownToggleTextEl
).
not
.
toBeNull
();
expect
(
dropdownToggleTextEl
.
innerText
.
trim
()).
toBe
(
'
Foo Label
'
);
expect
(
dropdownToggleTextEl
.
innerText
.
trim
()).
toBe
(
'
Foo Label
+1 more
'
);
});
});
it
(
'
renders dropdown button icon
'
,
()
=>
{
it
(
'
renders dropdown button icon
'
,
()
=>
{
...
...
spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js
View file @
97ab8539
...
@@ -35,9 +35,12 @@ describe('DropdownValueCollapsedComponent', () => {
...
@@ -35,9 +35,12 @@ describe('DropdownValueCollapsedComponent', () => {
});
});
it
(
'
returns labels names separated by coma when `labels` prop has more than one item
'
,
()
=>
{
it
(
'
returns labels names separated by coma when `labels` prop has more than one item
'
,
()
=>
{
const
vmMoreLabels
=
createComponent
(
mockLabels
.
concat
(
mockLabels
));
const
labels
=
mockLabels
.
concat
(
mockLabels
);
const
vmMoreLabels
=
createComponent
(
labels
);
expect
(
vmMoreLabels
.
labelsList
).
toBe
(
'
Foo Label, Foo Label
'
);
const
expectedText
=
labels
.
map
(
label
=>
label
.
title
).
join
(
'
,
'
);
expect
(
vmMoreLabels
.
labelsList
).
toBe
(
expectedText
);
vmMoreLabels
.
$destroy
();
vmMoreLabels
.
$destroy
();
});
});
...
@@ -49,14 +52,19 @@ describe('DropdownValueCollapsedComponent', () => {
...
@@ -49,14 +52,19 @@ describe('DropdownValueCollapsedComponent', () => {
const
vmMoreLabels
=
createComponent
(
mockMoreLabels
);
const
vmMoreLabels
=
createComponent
(
mockMoreLabels
);
expect
(
vmMoreLabels
.
labelsList
).
toBe
(
const
expectedText
=
`
${
mockMoreLabels
'
Foo Label, Foo Label, Foo Label, Foo Label, Foo Label, and 2 more
'
,
.
slice
(
0
,
5
)
);
.
map
(
label
=>
label
.
title
)
.
join
(
'
,
'
)}
, and
${
mockMoreLabels
.
length
-
5
}
more`
;
expect
(
vmMoreLabels
.
labelsList
).
toBe
(
expectedText
);
vmMoreLabels
.
$destroy
();
vmMoreLabels
.
$destroy
();
});
});
it
(
'
returns first label name when `labels` prop has only one item present
'
,
()
=>
{
it
(
'
returns first label name when `labels` prop has only one item present
'
,
()
=>
{
expect
(
vm
.
labelsList
).
toBe
(
'
Foo Label
'
);
const
text
=
mockLabels
.
map
(
label
=>
label
.
title
).
join
(
'
,
'
);
expect
(
vm
.
labelsList
).
toBe
(
text
);
});
});
});
});
});
});
...
...
spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js
View file @
97ab8539
import
Vue
from
'
vue
'
;
import
Vue
from
'
vue
'
;
import
$
from
'
jquery
'
;
import
dropdownValueComponent
from
'
~/vue_shared/components/sidebar/labels_select/dropdown_value.vue
'
;
import
dropdownValueComponent
from
'
~/vue_shared/components/sidebar/labels_select/dropdown_value.vue
'
;
...
@@ -15,6 +16,7 @@ const createComponent = (
...
@@ -15,6 +16,7 @@ const createComponent = (
return
mountComponent
(
Component
,
{
return
mountComponent
(
Component
,
{
labels
,
labels
,
labelFilterBasePath
,
labelFilterBasePath
,
enableScopedLabels
:
true
,
});
});
};
};
...
@@ -67,6 +69,26 @@ describe('DropdownValueComponent', () => {
...
@@ -67,6 +69,26 @@ describe('DropdownValueComponent', () => {
expect
(
styleObj
.
backgroundColor
).
toBe
(
label
.
color
);
expect
(
styleObj
.
backgroundColor
).
toBe
(
label
.
color
);
});
});
});
});
describe
(
'
scopedLabelsDescription
'
,
()
=>
{
it
(
'
returns html for tooltip
'
,
()
=>
{
const
html
=
vm
.
scopedLabelsDescription
(
mockLabels
[
1
]);
const
$el
=
$
.
parseHTML
(
html
);
expect
(
$el
[
0
]).
toHaveClass
(
'
scoped-label-tooltip-title
'
);
expect
(
$el
[
2
].
textContent
).
toEqual
(
mockLabels
[
1
].
description
);
});
});
describe
(
'
showScopedLabels
'
,
()
=>
{
it
(
'
returns true if the label is scoped label
'
,
()
=>
{
expect
(
vm
.
showScopedLabels
(
mockLabels
[
1
])).
toBe
(
true
);
});
it
(
'
returns false when label is a regular label
'
,
()
=>
{
expect
(
vm
.
showScopedLabels
(
mockLabels
[
0
])).
toBe
(
false
);
});
});
});
});
describe
(
'
template
'
,
()
=>
{
describe
(
'
template
'
,
()
=>
{
...
@@ -91,15 +113,25 @@ describe('DropdownValueComponent', () => {
...
@@ -91,15 +113,25 @@ describe('DropdownValueComponent', () => {
);
);
});
});
it
(
'
renders label element
with tooltip
and styles based on label details
'
,
()
=>
{
it
(
'
renders label element and styles based on label details
'
,
()
=>
{
const
labelEl
=
vm
.
$el
.
querySelector
(
'
a span.badge.color-label
'
);
const
labelEl
=
vm
.
$el
.
querySelector
(
'
a span.badge.color-label
'
);
expect
(
labelEl
).
not
.
toBeNull
();
expect
(
labelEl
).
not
.
toBeNull
();
expect
(
labelEl
.
dataset
.
placement
).
toBe
(
'
bottom
'
);
expect
(
labelEl
.
dataset
.
container
).
toBe
(
'
body
'
);
expect
(
labelEl
.
dataset
.
originalTitle
).
toBe
(
mockLabels
[
0
].
description
);
expect
(
labelEl
.
getAttribute
(
'
style
'
)).
toBe
(
'
background-color: rgb(186, 218, 85);
'
);
expect
(
labelEl
.
getAttribute
(
'
style
'
)).
toBe
(
'
background-color: rgb(186, 218, 85);
'
);
expect
(
labelEl
.
innerText
.
trim
()).
toBe
(
mockLabels
[
0
].
title
);
expect
(
labelEl
.
innerText
.
trim
()).
toBe
(
mockLabels
[
0
].
title
);
});
});
describe
(
'
label is of scoped-label type
'
,
()
=>
{
it
(
'
renders a scoped-label-wrapper span to incorporate 2 anchors
'
,
()
=>
{
expect
(
vm
.
$el
.
querySelector
(
'
span.scoped-label-wrapper
'
)).
not
.
toBeNull
();
});
it
(
'
renders anchor tag containing question icon
'
,
()
=>
{
const
anchor
=
vm
.
$el
.
querySelector
(
'
.scoped-label-wrapper a.scoped-label
'
);
expect
(
anchor
).
not
.
toBeNull
();
expect
(
anchor
.
querySelector
(
'
i.fa-question-circle
'
)).
not
.
toBeNull
();
});
});
});
});
});
});
spec/javascripts/vue_shared/components/sidebar/labels_select/mock_data.js
View file @
97ab8539
...
@@ -6,6 +6,13 @@ export const mockLabels = [
...
@@ -6,6 +6,13 @@ export const mockLabels = [
color
:
'
#BADA55
'
,
color
:
'
#BADA55
'
,
text_color
:
'
#FFFFFF
'
,
text_color
:
'
#FFFFFF
'
,
},
},
{
id
:
27
,
title
:
'
Foo::Bar
'
,
description
:
'
Foobar
'
,
color
:
'
#0033CC
'
,
text_color
:
'
#FFFFFF
'
,
},
];
];
export
const
mockSuggestedColors
=
[
export
const
mockSuggestedColors
=
[
...
...
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