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
d3fc3be0
Commit
d3fc3be0
authored
Mar 04, 2020
by
GitLab Bot
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Add latest changes from gitlab-org/gitlab@master
parent
c6c74378
Changes
48
Hide whitespace changes
Inline
Side-by-side
Showing
48 changed files
with
2692 additions
and
102 deletions
+2692
-102
app/assets/javascripts/blob/components/blob_content.vue
app/assets/javascripts/blob/components/blob_content.vue
+7
-1
app/assets/javascripts/blob/viewer/index.js
app/assets/javascripts/blob/viewer/index.js
+34
-37
app/assets/javascripts/vue_shared/components/blob_viewers/mixins.js
.../javascripts/vue_shared/components/blob_viewers/mixins.js
+4
-0
app/assets/javascripts/vue_shared/components/blob_viewers/rich_viewer.vue
...cripts/vue_shared/components/blob_viewers/rich_viewer.vue
+5
-1
app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_regular_label.vue
...ts/sidebar/labels_select/dropdown_value_regular_label.vue
+6
-1
app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_scoped_label.vue
...nts/sidebar/labels_select/dropdown_value_scoped_label.vue
+6
-1
app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_button.vue
.../components/sidebar/labels_select_vue/dropdown_button.vue
+21
-0
app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents.vue
...omponents/sidebar/labels_select_vue/dropdown_contents.vue
+30
-0
app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_create_view.vue
...debar/labels_select_vue/dropdown_contents_create_view.vue
+124
-0
app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view.vue
...debar/labels_select_vue/dropdown_contents_labels_view.vue
+178
-0
app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_title.vue
...d/components/sidebar/labels_select_vue/dropdown_title.vue
+39
-0
app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_value.vue
...d/components/sidebar/labels_select_vue/dropdown_value.vue
+53
-0
app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue
...mponents/sidebar/labels_select_vue/labels_select_root.vue
+173
-0
app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/actions.js
...red/components/sidebar/labels_select_vue/store/actions.js
+61
-0
app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/getters.js
...red/components/sidebar/labels_select_vue/store/getters.js
+30
-0
app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/index.js
...hared/components/sidebar/labels_select_vue/store/index.js
+12
-0
app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/mutation_types.js
...ponents/sidebar/labels_select_vue/store/mutation_types.js
+20
-0
app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/mutations.js
...d/components/sidebar/labels_select_vue/store/mutations.js
+76
-0
app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/state.js
...hared/components/sidebar/labels_select_vue/store/state.js
+27
-0
app/assets/stylesheets/framework/dropdowns.scss
app/assets/stylesheets/framework/dropdowns.scss
+51
-0
app/models/snippet.rb
app/models/snippet.rb
+2
-3
app/services/snippets/update_service.rb
app/services/snippets/update_service.rb
+53
-5
changelogs/unreleased/dmishunov-rich-viewers.yml
changelogs/unreleased/dmishunov-rich-viewers.yml
+5
-0
changelogs/unreleased/fj-39265-update-snippet-repository-content.yml
...unreleased/fj-39265-update-snippet-repository-content.yml
+5
-0
changelogs/unreleased/make_design_management_versions_created_at_not_null.yml
...d/make_design_management_versions_created_at_not_null.yml
+5
-0
db/migrate/20191114201118_make_created_at_not_null_in_design_management_versions.rb
...make_created_at_not_null_in_design_management_versions.rb
+15
-0
db/schema.rb
db/schema.rb
+1
-1
doc/development/dangerbot.md
doc/development/dangerbot.md
+0
-6
locale/gitlab.pot
locale/gitlab.pot
+6
-0
spec/frontend/vue_shared/components/blob_viewers/rich_viewer_spec.js
...nd/vue_shared/components/blob_viewers/rich_viewer_spec.js
+10
-1
spec/frontend/vue_shared/components/blob_viewers/simple_viewer_spec.js
.../vue_shared/components/blob_viewers/simple_viewer_spec.js
+1
-0
spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_button_spec.js
...ponents/sidebar/labels_select_vue/dropdown_button_spec.js
+55
-0
spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_create_view_spec.js
...r/labels_select_vue/dropdown_contents_create_view_spec.js
+223
-0
spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view_spec.js
...r/labels_select_vue/dropdown_contents_labels_view_spec.js
+265
-0
spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_spec.js
...nents/sidebar/labels_select_vue/dropdown_contents_spec.js
+54
-0
spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_title_spec.js
...mponents/sidebar/labels_select_vue/dropdown_title_spec.js
+61
-0
spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_value_spec.js
...mponents/sidebar/labels_select_vue/dropdown_value_spec.js
+84
-0
spec/frontend/vue_shared/components/sidebar/labels_select_vue/labels_select_root_spec.js
...ents/sidebar/labels_select_vue/labels_select_root_spec.js
+127
-0
spec/frontend/vue_shared/components/sidebar/labels_select_vue/mock_data.js
..._shared/components/sidebar/labels_select_vue/mock_data.js
+66
-0
spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/actions_spec.js
...omponents/sidebar/labels_select_vue/store/actions_spec.js
+276
-0
spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/getters_spec.js
...omponents/sidebar/labels_select_vue/store/getters_spec.js
+31
-0
spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/mutations_spec.js
...ponents/sidebar/labels_select_vue/store/mutations_spec.js
+172
-0
spec/models/snippet_spec.rb
spec/models/snippet_spec.rb
+17
-4
spec/requests/api/graphql/mutations/snippets/update_spec.rb
spec/requests/api/graphql/mutations/snippets/update_spec.rb
+2
-2
spec/requests/api/project_snippets_spec.rb
spec/requests/api/project_snippets_spec.rb
+20
-16
spec/requests/api/snippets_spec.rb
spec/requests/api/snippets_spec.rb
+18
-14
spec/services/snippets/update_service_spec.rb
spec/services/snippets/update_service_spec.rb
+118
-9
spec/support/shared_examples/requests/snippet_shared_examples.rb
...pport/shared_examples/requests/snippet_shared_examples.rb
+43
-0
No files found.
app/assets/javascripts/blob/components/blob_content.vue
View file @
d3fc3be0
...
@@ -45,7 +45,13 @@ export default {
...
@@ -45,7 +45,13 @@ export default {
<template
v-else
>
<template
v-else
>
<blob-content-error
v-if=
"viewerError"
:viewer-error=
"viewerError"
/>
<blob-content-error
v-if=
"viewerError"
:viewer-error=
"viewerError"
/>
<component
:is=
"viewer"
v-else
ref=
"contentViewer"
:content=
"content"
/>
<component
:is=
"viewer"
v-else
ref=
"contentViewer"
:content=
"content"
:type=
"activeViewer.fileType"
/>
</
template
>
</
template
>
</div>
</div>
</template>
</template>
app/assets/javascripts/blob/viewer/index.js
View file @
d3fc3be0
...
@@ -5,10 +5,43 @@ import { handleLocationHash } from '../../lib/utils/common_utils';
...
@@ -5,10 +5,43 @@ import { handleLocationHash } from '../../lib/utils/common_utils';
import
axios
from
'
../../lib/utils/axios_utils
'
;
import
axios
from
'
../../lib/utils/axios_utils
'
;
import
{
__
}
from
'
~/locale
'
;
import
{
__
}
from
'
~/locale
'
;
const
loadRichBlobViewer
=
type
=>
{
switch
(
type
)
{
case
'
balsamiq
'
:
return
import
(
/* webpackChunkName: 'balsamiq_viewer' */
'
../balsamiq_viewer
'
);
case
'
notebook
'
:
return
import
(
/* webpackChunkName: 'notebook_viewer' */
'
../notebook_viewer
'
);
case
'
openapi
'
:
return
import
(
/* webpackChunkName: 'openapi_viewer' */
'
../openapi_viewer
'
);
case
'
pdf
'
:
return
import
(
/* webpackChunkName: 'pdf_viewer' */
'
../pdf_viewer
'
);
case
'
sketch
'
:
return
import
(
/* webpackChunkName: 'sketch_viewer' */
'
../sketch_viewer
'
);
case
'
stl
'
:
return
import
(
/* webpackChunkName: 'stl_viewer' */
'
../stl_viewer
'
);
default
:
return
Promise
.
resolve
();
}
};
export
const
handleBlobRichViewer
=
(
viewer
,
type
)
=>
{
if
(
!
viewer
||
!
type
)
return
;
loadRichBlobViewer
(
type
)
.
then
(
module
=>
module
?.
default
(
viewer
))
.
catch
(
error
=>
{
Flash
(
__
(
'
Error loading file viewer.
'
));
throw
error
;
});
};
export
default
class
BlobViewer
{
export
default
class
BlobViewer
{
constructor
()
{
constructor
()
{
const
viewer
=
document
.
querySelector
(
'
.blob-viewer[data-type="rich"]
'
);
const
type
=
viewer
?.
dataset
?.
richType
;
BlobViewer
.
initAuxiliaryViewer
();
BlobViewer
.
initAuxiliaryViewer
();
BlobViewer
.
initRichViewer
();
handleBlobRichViewer
(
viewer
,
type
);
this
.
initMainViewers
();
this
.
initMainViewers
();
}
}
...
@@ -20,42 +53,6 @@ export default class BlobViewer {
...
@@ -20,42 +53,6 @@ export default class BlobViewer {
BlobViewer
.
loadViewer
(
auxiliaryViewer
);
BlobViewer
.
loadViewer
(
auxiliaryViewer
);
}
}
static
initRichViewer
()
{
const
viewer
=
document
.
querySelector
(
'
.blob-viewer[data-type="rich"]
'
);
if
(
!
viewer
||
!
viewer
.
dataset
.
richType
)
return
;
const
initViewer
=
promise
=>
promise
.
then
(
module
=>
module
.
default
(
viewer
))
.
catch
(
error
=>
{
Flash
(
__
(
'
Error loading file viewer.
'
));
throw
error
;
});
switch
(
viewer
.
dataset
.
richType
)
{
case
'
balsamiq
'
:
initViewer
(
import
(
/* webpackChunkName: 'balsamiq_viewer' */
'
../balsamiq_viewer
'
));
break
;
case
'
notebook
'
:
initViewer
(
import
(
/* webpackChunkName: 'notebook_viewer' */
'
../notebook_viewer
'
));
break
;
case
'
openapi
'
:
initViewer
(
import
(
/* webpackChunkName: 'openapi_viewer' */
'
../openapi_viewer
'
));
break
;
case
'
pdf
'
:
initViewer
(
import
(
/* webpackChunkName: 'pdf_viewer' */
'
../pdf_viewer
'
));
break
;
case
'
sketch
'
:
initViewer
(
import
(
/* webpackChunkName: 'sketch_viewer' */
'
../sketch_viewer
'
));
break
;
case
'
stl
'
:
initViewer
(
import
(
/* webpackChunkName: 'stl_viewer' */
'
../stl_viewer
'
));
break
;
default
:
break
;
}
}
initMainViewers
()
{
initMainViewers
()
{
this
.
$fileHolder
=
$
(
'
.file-holder
'
);
this
.
$fileHolder
=
$
(
'
.file-holder
'
);
if
(
!
this
.
$fileHolder
.
length
)
return
;
if
(
!
this
.
$fileHolder
.
length
)
return
;
...
...
app/assets/javascripts/vue_shared/components/blob_viewers/mixins.js
View file @
d3fc3be0
...
@@ -4,5 +4,9 @@ export default {
...
@@ -4,5 +4,9 @@ export default {
type
:
String
,
type
:
String
,
required
:
true
,
required
:
true
,
},
},
type
:
{
type
:
String
,
required
:
true
,
},
},
},
};
};
app/assets/javascripts/vue_shared/components/blob_viewers/rich_viewer.vue
View file @
d3fc3be0
<
script
>
<
script
>
import
ViewerMixin
from
'
./mixins
'
;
import
ViewerMixin
from
'
./mixins
'
;
import
{
handleBlobRichViewer
}
from
'
~/blob/viewer
'
;
export
default
{
export
default
{
mixins
:
[
ViewerMixin
],
mixins
:
[
ViewerMixin
],
mounted
()
{
handleBlobRichViewer
(
this
.
$refs
.
content
,
this
.
type
);
},
};
};
</
script
>
</
script
>
<
template
>
<
template
>
<div
v-html=
"content"
></div>
<div
ref=
"content"
v-html=
"content"
></div>
</
template
>
</
template
>
app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_regular_label.vue
View file @
d3fc3be0
...
@@ -27,7 +27,12 @@ export default {
...
@@ -27,7 +27,12 @@ export default {
<span
:style=
"labelStyle"
class=
"badge color-label"
>
<span
:style=
"labelStyle"
class=
"badge color-label"
>
{{
label
.
title
}}
{{
label
.
title
}}
</span>
</span>
<gl-tooltip
:target=
"() => $refs.regularLabelRef"
placement=
"top"
boundary=
"viewport"
>
<gl-tooltip
v-if=
"label.description"
:target=
"() => $refs.regularLabelRef"
placement=
"top"
boundary=
"viewport"
>
{{
label
.
description
}}
{{
label
.
description
}}
</gl-tooltip>
</gl-tooltip>
</a>
</a>
...
...
app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_scoped_label.vue
View file @
d3fc3be0
...
@@ -33,7 +33,12 @@ export default {
...
@@ -33,7 +33,12 @@ export default {
<span
:ref=
"`labelTitleRef`"
:style=
"labelStyle"
class=
"badge color-label label"
>
<span
:ref=
"`labelTitleRef`"
:style=
"labelStyle"
class=
"badge color-label label"
>
{{
label
.
title
}}
{{
label
.
title
}}
</span>
</span>
<gl-tooltip
:target=
"() => $refs.labelTitleRef"
placement=
"top"
boundary=
"viewport"
>
<gl-tooltip
v-if=
"label.description"
:target=
"() => $refs.labelTitleRef"
placement=
"top"
boundary=
"viewport"
>
<span
class=
"font-weight-bold scoped-label-tooltip-title"
>
{{
__
(
'
Scoped label
'
)
}}
</span
<span
class=
"font-weight-bold scoped-label-tooltip-title"
>
{{
__
(
'
Scoped label
'
)
}}
</span
><br
/>
><br
/>
{{
label
.
description
}}
{{
label
.
description
}}
...
...
app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_button.vue
0 → 100644
View file @
d3fc3be0
<
script
>
import
{
mapGetters
}
from
'
vuex
'
;
import
{
GlButton
,
GlIcon
}
from
'
@gitlab/ui
'
;
export
default
{
components
:
{
GlButton
,
GlIcon
,
},
computed
:
{
...
mapGetters
([
'
dropdownButtonText
'
]),
},
};
</
script
>
<
template
>
<gl-button
class=
"labels-select-dropdown-button w-100 text-left"
>
<span
class=
"dropdown-toggle-text"
>
{{
dropdownButtonText
}}
</span>
<gl-icon
name=
"chevron-down"
class=
"pull-right"
/>
</gl-button>
</
template
>
app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents.vue
0 → 100644
View file @
d3fc3be0
<
script
>
import
{
mapState
}
from
'
vuex
'
;
import
DropdownContentsLabelsView
from
'
./dropdown_contents_labels_view.vue
'
;
import
DropdownContentsCreateView
from
'
./dropdown_contents_create_view.vue
'
;
export
default
{
components
:
{
DropdownContentsLabelsView
,
DropdownContentsCreateView
,
},
computed
:
{
...
mapState
([
'
showDropdownContentsCreateView
'
]),
dropdownContentsView
()
{
if
(
this
.
showDropdownContentsCreateView
)
{
return
'
dropdown-contents-create-view
'
;
}
return
'
dropdown-contents-labels-view
'
;
},
},
};
</
script
>
<
template
>
<div
class=
"labels-select-dropdown-contents w-100 mt-1 mb-3 py-2 rounded-top rounded-bottom position-absolute"
>
<component
:is=
"dropdownContentsView"
/>
</div>
</
template
>
app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_create_view.vue
0 → 100644
View file @
d3fc3be0
<
script
>
import
{
mapState
,
mapActions
}
from
'
vuex
'
;
import
{
GlTooltipDirective
,
GlButton
,
GlIcon
,
GlFormInput
,
GlLink
,
GlLoadingIcon
,
}
from
'
@gitlab/ui
'
;
export
default
{
components
:
{
GlButton
,
GlIcon
,
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"
>
<div
class=
"dropdown-title d-flex align-items-center pt-0 pb-2"
>
<gl-button
:aria-label=
"__('Go back')"
variant=
"link"
size=
"sm"
class=
"dropdown-header-button p-0"
@
click=
"toggleDropdownContentsCreateView"
>
<gl-icon
name=
"arrow-left"
/>
</gl-button>
<span
class=
"flex-grow-1"
>
{{
labelsCreateTitle
}}
</span>
<gl-button
:aria-label=
"__('Close')"
variant=
"link"
size=
"sm"
class=
"dropdown-header-button p-0"
@
click=
"toggleDropdownContents"
>
<gl-icon
name=
"close"
/>
</gl-button>
</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 d-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"
:placeholder=
"__('Use custom color #FF0000')"
/>
</div>
</div>
<div
class=
"dropdown-actions clearfix pt-2 px-2"
>
<gl-button
:disabled=
"disableCreate"
variant=
"primary"
class=
"pull-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=
"pull-right"
@
click=
"toggleDropdownContentsCreateView"
>
{{
__
(
'
Cancel
'
)
}}
</gl-button>
</div>
</div>
</
template
>
app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view.vue
0 → 100644
View file @
d3fc3be0
<
script
>
import
{
mapState
,
mapGetters
,
mapActions
}
from
'
vuex
'
;
import
{
GlLoadingIcon
,
GlButton
,
GlIcon
,
GlSearchBoxByType
,
GlLink
}
from
'
@gitlab/ui
'
;
import
{
UP_KEY_CODE
,
DOWN_KEY_CODE
,
ENTER_KEY_CODE
,
ESC_KEY_CODE
}
from
'
~/lib/utils/keycodes
'
;
export
default
{
components
:
{
GlLoadingIcon
,
GlButton
,
GlIcon
,
GlSearchBoxByType
,
GlLink
,
},
data
()
{
return
{
searchKey
:
''
,
currentHighlightItem
:
-
1
,
};
},
computed
:
{
...
mapState
([
'
labelsManagePath
'
,
'
labels
'
,
'
labelsFetchInProgress
'
,
'
labelsListTitle
'
,
'
footerCreateLabelTitle
'
,
'
footerManageLabelTitle
'
,
]),
...
mapGetters
([
'
selectedLabelsList
'
]),
visibleLabels
()
{
if
(
this
.
searchKey
)
{
return
this
.
labels
.
filter
(
label
=>
label
.
title
.
toLowerCase
().
includes
(
this
.
searchKey
.
toLowerCase
()),
);
}
return
this
.
labels
;
},
},
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
;
}
},
},
mounted
()
{
this
.
fetchLabels
();
},
methods
:
{
...
mapActions
([
'
toggleDropdownContents
'
,
'
toggleDropdownContentsCreateView
'
,
'
fetchLabels
'
,
'
updateSelectedLabels
'
,
]),
getDropdownLabelBoxStyle
(
label
)
{
return
{
backgroundColor
:
label
.
color
,
};
},
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
rect
=
highlightedLabel
.
getBoundingClientRect
();
if
(
rect
.
bottom
>
this
.
$refs
.
labelsListContainer
.
clientHeight
)
{
highlightedLabel
.
scrollIntoView
(
false
);
}
if
(
rect
.
top
<
0
)
{
highlightedLabel
.
scrollIntoView
();
}
}
},
/**
* 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
]]);
}
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
]);
},
},
};
</
script
>
<
template
>
<div
class=
"labels-select-contents-list"
@
keydown=
"handleKeyDown"
>
<gl-loading-icon
v-if=
"labelsFetchInProgress"
class=
"labels-fetch-loading position-absolute d-flex align-items-center w-100 h-100"
size=
"md"
/>
<div
class=
"dropdown-title d-flex align-items-center pt-0 pb-2"
>
<span
class=
"flex-grow-1"
>
{{
labelsListTitle
}}
</span>
<gl-button
:aria-label=
"__('Close')"
variant=
"link"
size=
"sm"
class=
"dropdown-header-button p-0"
@
click=
"toggleDropdownContents"
>
<gl-icon
name=
"close"
/>
</gl-button>
</div>
<div
class=
"dropdown-input"
>
<gl-search-box-by-type
v-model=
"searchKey"
:autofocus=
"true"
/>
</div>
<div
v-if=
"!labelsFetchInProgress"
ref=
"labelsListContainer"
class=
"dropdown-content"
>
<ul
class=
"list-unstyled mb-0"
>
<li
v-for=
"(label, index) in visibleLabels"
:key=
"label.id"
class=
"d-block text-left"
>
<gl-link
class=
"d-flex align-items-baseline text-break-word label-item"
:class=
"
{ 'is-focused': index === currentHighlightItem }"
@click="handleLabelClick(label)"
>
<gl-icon
v-show=
"label.set"
name=
"mobile-issue-close"
class=
"mr-2 align-self-center"
/>
<span
v-show=
"!label.set"
class=
"mr-3 pr-2"
></span>
<span
class=
"dropdown-label-box"
:style=
"getDropdownLabelBoxStyle(label)"
></span>
<span>
{{
label
.
title
}}
</span>
</gl-link>
</li>
<li
v-if=
"!visibleLabels.length"
class=
"p-2 text-center"
>
{{
__
(
'
No matching results
'
)
}}
</li>
</ul>
</div>
<div
class=
"dropdown-footer"
>
<ul
class=
"list-unstyled"
>
<li>
<gl-button
variant=
"link"
class=
"d-flex w-100 flex-row text-break-word label-item"
@
click=
"toggleDropdownContentsCreateView"
>
{{
footerCreateLabelTitle
}}
</gl-button
>
</li>
<li>
<gl-link
:href=
"labelsManagePath"
class=
"d-flex flex-row text-break-word label-item"
>
{{
footerManageLabelTitle
}}
</gl-link>
</li>
</ul>
</div>
</div>
</
template
>
app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_title.vue
0 → 100644
View file @
d3fc3be0
<
script
>
import
{
mapState
,
mapActions
}
from
'
vuex
'
;
import
{
GlButton
,
GlLoadingIcon
}
from
'
@gitlab/ui
'
;
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 append-bottom-10"
>
{{
__
(
'
Labels
'
)
}}
<template
v-if=
"allowLabelEdit"
>
<gl-loading-icon
v-show=
"labelsSelectInProgress"
inline
/>
<gl-button
variant=
"link"
class=
"pull-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_vue/dropdown_value.vue
0 → 100644
View file @
d3fc3be0
<
script
>
import
{
mapState
}
from
'
vuex
'
;
import
{
GlLabel
}
from
'
@gitlab/ui
'
;
import
{
isScopedLabel
}
from
'
~/lib/utils/common_utils
'
;
export
default
{
components
:
{
GlLabel
,
},
computed
:
{
...
mapState
([
'
selectedLabels
'
,
'
allowScopedLabels
'
,
'
labelsFilterBasePath
'
,
'
scopedLabelsDocumentationPath
'
,
]),
},
methods
:
{
labelFilterUrl
(
label
)
{
return
`
${
this
.
labelsFilterBasePath
}
?label_name[]=
${
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"
:title=
"label.title"
:description=
"label.description"
:background-color=
"label.color"
:target=
"labelFilterUrl(label)"
:scoped=
"scopedLabel(label)"
:scoped-labels-documentation-link=
"scopedLabelsDocumentationPath"
tooltip-placement=
"top"
/>
</
template
>
</div>
</template>
app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue
0 → 100644
View file @
d3fc3be0
<
script
>
import
Vue
from
'
vue
'
;
import
Vuex
,
{
mapState
,
mapActions
}
from
'
vuex
'
;
import
{
__
}
from
'
~/locale
'
;
import
DropdownValueCollapsed
from
'
~/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue
'
;
import
labelsSelectModule
from
'
./store
'
;
import
DropdownTitle
from
'
./dropdown_title.vue
'
;
import
DropdownValue
from
'
./dropdown_value.vue
'
;
import
DropdownButton
from
'
./dropdown_button.vue
'
;
import
DropdownContents
from
'
./dropdown_contents.vue
'
;
Vue
.
use
(
Vuex
);
export
default
{
store
:
new
Vuex
.
Store
(
labelsSelectModule
()),
components
:
{
DropdownTitle
,
DropdownValue
,
DropdownButton
,
DropdownContents
,
DropdownValueCollapsed
,
},
props
:
{
allowLabelEdit
:
{
type
:
Boolean
,
required
:
true
,
},
allowLabelCreate
:
{
type
:
Boolean
,
required
:
true
,
},
allowScopedLabels
:
{
type
:
Boolean
,
required
:
true
,
},
dropdownOnly
:
{
type
:
Boolean
,
required
:
false
,
default
:
false
,
},
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
:
''
,
},
scopedLabelsDocumentationPath
:
{
type
:
String
,
required
:
false
,
default
:
''
,
},
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
'
),
},
},
computed
:
{
...
mapState
([
'
showDropdownButton
'
,
'
showDropdownContents
'
]),
},
watch
:
{
selectedLabels
(
selectedLabels
)
{
this
.
setInitialState
({
selectedLabels
,
});
},
},
mounted
()
{
this
.
setInitialState
({
dropdownOnly
:
this
.
dropdownOnly
,
allowLabelEdit
:
this
.
allowLabelEdit
,
allowLabelCreate
:
this
.
allowLabelCreate
,
allowScopedLabels
:
this
.
allowScopedLabels
,
selectedLabels
:
this
.
selectedLabels
,
labelsFetchPath
:
this
.
labelsFetchPath
,
labelsManagePath
:
this
.
labelsManagePath
,
labelsFilterBasePath
:
this
.
labelsFilterBasePath
,
scopedLabelsDocumentationPath
:
this
.
scopedLabelsDocumentationPath
,
labelsListTitle
:
this
.
labelsListTitle
,
labelsCreateTitle
:
this
.
labelsCreateTitle
,
footerCreateLabelTitle
:
this
.
footerCreateLabelTitle
,
footerManageLabelTitle
:
this
.
footerManageLabelTitle
,
});
this
.
$store
.
subscribeAction
({
after
:
this
.
handleVuexActionDispatch
,
});
},
methods
:
{
...
mapActions
([
'
setInitialState
'
]),
/**
* This method differentiates between
* dispatched actions and calls necessary method.
*/
handleVuexActionDispatch
(
action
,
state
)
{
if
(
action
.
type
===
'
toggleDropdownContents
'
&&
!
state
.
showDropdownButton
&&
!
state
.
showDropdownContents
)
{
this
.
handleDropdownClose
(
state
.
labels
.
filter
(
label
=>
label
.
touched
));
}
},
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
'
);
},
},
};
</
script
>
<
template
>
<div
class=
"labels-select-wrapper position-relative"
>
<div
v-if=
"!dropdownOnly"
>
<dropdown-value-collapsed
v-if=
"allowLabelCreate"
:labels=
"selectedLabels"
@
onValueClick=
"handleCollapsedValueClick"
/>
<dropdown-title
:allow-label-edit=
"allowLabelEdit"
:labels-select-in-progress=
"labelsSelectInProgress"
/>
<dropdown-value
v-show=
"!showDropdownButton"
>
<slot></slot>
</dropdown-value>
<dropdown-button
v-show=
"showDropdownButton"
/>
<dropdown-contents
v-if=
"showDropdownButton && showDropdownContents"
/>
</div>
</div>
</
template
>
app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/actions.js
0 → 100644
View file @
d3fc3be0
import
flash
from
'
~/flash
'
;
import
{
__
}
from
'
~/locale
'
;
import
axios
from
'
~/lib/utils/axios_utils
'
;
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
'
);
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/i18n/no-non-i18n-strings
throw
new
Error
(
'
Error Creating Label
'
);
}
})
.
catch
(()
=>
{
dispatch
(
'
receiveCreateLabelFailure
'
);
});
};
export
const
updateSelectedLabels
=
({
commit
},
labels
)
=>
commit
(
types
.
UPDATE_SELECTED_LABELS
,
{
labels
});
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export
default
()
=>
{};
app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/getters.js
0 → 100644
View file @
d3fc3be0
import
{
__
,
s__
,
sprintf
}
from
'
~/locale
'
;
/**
* Returns string representing current labels
* selection on dropdown button.
*
* @param {object} state
*/
export
const
dropdownButtonText
=
state
=>
{
const
selectedLabels
=
state
.
labels
.
filter
(
label
=>
label
.
set
);
if
(
!
selectedLabels
.
length
)
{
return
__
(
'
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
);
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export
default
()
=>
{};
app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/index.js
0 → 100644
View file @
d3fc3be0
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_vue/store/mutation_types.js
0 → 100644
View file @
d3fc3be0
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_vue/store/mutations.js
0 → 100644
View file @
d3fc3be0
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
.
dropdownOnly
)
{
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
})
{
// Iterate over all the labels and update
// `set` prop value to represent their current state.
const
labelIds
=
labels
.
map
(
label
=>
label
.
id
);
state
.
labels
=
state
.
labels
.
reduce
((
allLabels
,
label
)
=>
{
if
(
labelIds
.
includes
(
label
.
id
))
{
allLabels
.
push
({
...
label
,
touched
:
true
,
set
:
!
label
.
set
,
});
}
else
{
allLabels
.
push
(
label
);
}
return
allLabels
;
},
[]);
},
};
app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/state.js
0 → 100644
View file @
d3fc3be0
export
default
()
=>
({
// Initial Data
labels
:
[],
selectedLabels
:
[],
labelsListTitle
:
''
,
labelsCreateTitle
:
''
,
footerCreateLabelTitle
:
''
,
footerManageLabelTitle
:
''
,
// Paths
namespace
:
''
,
labelsFetchPath
:
''
,
labelsFilterBasePath
:
''
,
scopedLabelsDocumentationPath
:
'
#
'
,
// UI Flags
allowLabelCreate
:
false
,
allowLabelEdit
:
false
,
allowScopedLabels
:
false
,
dropdownOnly
:
false
,
showDropdownButton
:
false
,
showDropdownContents
:
false
,
showDropdownContentsCreateView
:
false
,
labelsFetchInProgress
:
false
,
labelCreateInProgress
:
false
,
selectedLabelsUpdated
:
false
,
});
app/assets/stylesheets/framework/dropdowns.scss
View file @
d3fc3be0
...
@@ -1019,3 +1019,54 @@ header.header-content .dropdown-menu.frequent-items-dropdown-menu {
...
@@ -1019,3 +1019,54 @@ header.header-content .dropdown-menu.frequent-items-dropdown-menu {
opacity
:
0
;
opacity
:
0
;
}
}
}
}
.labels-select-wrapper
{
.labels-select-dropdown-contents
{
min-height
:
$dropdown-min-height
;
max-height
:
330px
;
background-color
:
$white-light
;
border
:
1px
solid
$border-color
;
box-shadow
:
0
2px
4px
$dropdown-shadow-color
;
z-index
:
2
;
.dropdown-content
{
height
:
135px
;
}
}
.labels-fetch-loading
{
top
:
0
;
left
:
0
;
opacity
:
0
.5
;
background-color
:
$white-light
;
z-index
:
1
;
}
.dropdown-header-button
{
.gl-icon
{
color
:
$dropdown-title-btn-color
;
&
:hover
{
color
:
$gl-gray-400
;
}
}
}
.label-item
{
padding
:
8px
20px
;
&
:hover
,
&
.is-focused
{
@include
dropdown-item-hover
;
text-decoration
:
none
;
}
}
.color-input-container
{
.dropdown-label-color-preview
{
border
:
1px
solid
$gray-200
;
border-right
:
0
;
}
}
}
app/models/snippet.rb
View file @
d3fc3be0
...
@@ -281,11 +281,10 @@ class Snippet < ApplicationRecord
...
@@ -281,11 +281,10 @@ class Snippet < ApplicationRecord
end
end
def
create_repository
def
create_repository
return
if
repository_exists?
return
if
repository_exists?
&&
snippet_repository
repository
.
create_if_not_exists
repository
.
create_if_not_exists
track_snippet_repository
track_snippet_repository
if
repository_exists?
end
end
def
track_snippet_repository
def
track_snippet_repository
...
...
app/services/snippets/update_service.rb
View file @
d3fc3be0
...
@@ -4,6 +4,9 @@ module Snippets
...
@@ -4,6 +4,9 @@ module Snippets
class
UpdateService
<
Snippets
::
BaseService
class
UpdateService
<
Snippets
::
BaseService
include
SpamCheckMethods
include
SpamCheckMethods
UpdateError
=
Class
.
new
(
StandardError
)
CreateRepositoryError
=
Class
.
new
(
StandardError
)
def
execute
(
snippet
)
def
execute
(
snippet
)
# check that user is allowed to set specified visibility_level
# check that user is allowed to set specified visibility_level
new_visibility
=
visibility_level
new_visibility
=
visibility_level
...
@@ -20,11 +23,7 @@ module Snippets
...
@@ -20,11 +23,7 @@ module Snippets
snippet
.
assign_attributes
(
params
)
snippet
.
assign_attributes
(
params
)
spam_check
(
snippet
,
current_user
)
spam_check
(
snippet
,
current_user
)
snippet_saved
=
snippet
.
with_transaction_returning_status
do
if
save_and_commit
(
snippet
)
snippet
.
save
end
if
snippet_saved
Gitlab
::
UsageDataCounters
::
SnippetCounter
.
count
(
:update
)
Gitlab
::
UsageDataCounters
::
SnippetCounter
.
count
(
:update
)
ServiceResponse
.
success
(
payload:
{
snippet:
snippet
}
)
ServiceResponse
.
success
(
payload:
{
snippet:
snippet
}
)
...
@@ -32,5 +31,54 @@ module Snippets
...
@@ -32,5 +31,54 @@ module Snippets
snippet_error_response
(
snippet
,
400
)
snippet_error_response
(
snippet
,
400
)
end
end
end
end
private
def
save_and_commit
(
snippet
)
snippet
.
with_transaction_returning_status
do
snippet
.
save
.
tap
do
|
saved
|
break
false
unless
saved
# In order to avoid non migrated snippets scenarios,
# if the snippet does not have a repository we created it
# We don't need to check if the repository exists
# because `create_repository` already handles it
if
Feature
.
enabled?
(
:version_snippets
,
current_user
)
create_repository_for
(
snippet
)
end
# If the snippet repository exists we commit always
# the changes
create_commit
(
snippet
)
if
snippet
.
repository_exists?
end
rescue
snippet
.
errors
.
add
(
:base
,
'Error updating the snippet'
)
false
end
end
def
create_repository_for
(
snippet
)
snippet
.
create_repository
raise
CreateRepositoryError
,
'Repository could not be created'
unless
snippet
.
repository_exists?
end
def
create_commit
(
snippet
)
raise
UpdateError
unless
snippet
.
snippet_repository
commit_attrs
=
{
branch_name:
'master'
,
message:
'Update snippet'
}
snippet
.
snippet_repository
.
multi_files_action
(
current_user
,
snippet_files
(
snippet
),
commit_attrs
)
end
def
snippet_files
(
snippet
)
[{
previous_path:
snippet
.
blobs
.
first
&
.
path
,
file_path:
params
[
:file_name
],
content:
params
[
:content
]
}]
end
end
end
end
end
changelogs/unreleased/dmishunov-rich-viewers.yml
0 → 100644
View file @
d3fc3be0
---
title
:
Special handling for the rich viewer on specific file types
merge_request
:
26260
author
:
type
:
changed
changelogs/unreleased/fj-39265-update-snippet-repository-content.yml
0 → 100644
View file @
d3fc3be0
---
title
:
Update files when snippet is updated
merge_request
:
23993
author
:
type
:
changed
changelogs/unreleased/make_design_management_versions_created_at_not_null.yml
0 → 100644
View file @
d3fc3be0
---
title
:
Make design_management_versions.created_at not
null
merge_request
:
20182
author
:
Lee Tickett
type
:
other
db/migrate/20191114201118_make_created_at_not_null_in_design_management_versions.rb
0 → 100644
View file @
d3fc3be0
# frozen_string_literal: true
class
MakeCreatedAtNotNullInDesignManagementVersions
<
ActiveRecord
::
Migration
[
5.2
]
include
Gitlab
::
Database
::
MigrationHelpers
DOWNTIME
=
false
def
up
change_column_null
:design_management_versions
,
:created_at
,
false
,
Time
.
now
.
to_s
(
:db
)
end
def
down
change_column_null
:design_management_versions
,
:created_at
,
true
end
end
db/schema.rb
View file @
d3fc3be0
...
@@ -1446,7 +1446,7 @@ ActiveRecord::Schema.define(version: 2020_03_03_074328) do
...
@@ -1446,7 +1446,7 @@ ActiveRecord::Schema.define(version: 2020_03_03_074328) do
create_table
"design_management_versions"
,
force: :cascade
do
|
t
|
create_table
"design_management_versions"
,
force: :cascade
do
|
t
|
t
.
binary
"sha"
,
null:
false
t
.
binary
"sha"
,
null:
false
t
.
bigint
"issue_id"
t
.
bigint
"issue_id"
t
.
datetime_with_timezone
"created_at"
t
.
datetime_with_timezone
"created_at"
,
null:
false
t
.
integer
"author_id"
t
.
integer
"author_id"
t
.
index
[
"author_id"
],
name:
"index_design_management_versions_on_author_id"
,
where:
"(author_id IS NOT NULL)"
t
.
index
[
"author_id"
],
name:
"index_design_management_versions_on_author_id"
,
where:
"(author_id IS NOT NULL)"
t
.
index
[
"issue_id"
],
name:
"index_design_management_versions_on_issue_id"
t
.
index
[
"issue_id"
],
name:
"index_design_management_versions_on_issue_id"
...
...
doc/development/dangerbot.md
View file @
d3fc3be0
...
@@ -71,12 +71,6 @@ the need as part of the product in a future version of GitLab!
...
@@ -71,12 +71,6 @@ the need as part of the product in a future version of GitLab!
Implement each task as an isolated piece of functionality and place it in its
Implement each task as an isolated piece of functionality and place it in its
own directory under
`danger`
as
`danger/<task-name>/Dangerfile`
.
own directory under
`danger`
as
`danger/<task-name>/Dangerfile`
.
Add a line to the top-level
`Dangerfile`
to ensure it is loaded like:
```
ruby
danger
.
import_dangerfile
(
'danger/<task-name>'
)
```
Each task should be isolated from the others, and able to function in isolation.
Each task should be isolated from the others, and able to function in isolation.
If there is code that should be shared between multiple tasks, add a plugin to
If there is code that should be shared between multiple tasks, add a plugin to
`danger/plugins/...`
and require it in each task that needs it. You can also
`danger/plugins/...`
and require it in each task that needs it. You can also
...
...
locale/gitlab.pot
View file @
d3fc3be0
...
@@ -7778,6 +7778,9 @@ msgstr ""
...
@@ -7778,6 +7778,9 @@ msgstr ""
msgid "Error creating epic"
msgid "Error creating epic"
msgstr ""
msgstr ""
msgid "Error creating label."
msgstr ""
msgid "Error deleting %{issuableType}"
msgid "Error deleting %{issuableType}"
msgstr ""
msgstr ""
...
@@ -21315,6 +21318,9 @@ msgstr ""
...
@@ -21315,6 +21318,9 @@ msgstr ""
msgid "Use an one time password authenticator on your mobile device or computer to enable two-factor authentication (2FA)."
msgid "Use an one time password authenticator on your mobile device or computer to enable two-factor authentication (2FA)."
msgstr ""
msgstr ""
msgid "Use custom color #FF0000"
msgstr ""
msgid "Use group milestones to manage issues from multiple projects in the same milestone."
msgid "Use group milestones to manage issues from multiple projects in the same milestone."
msgstr ""
msgstr ""
...
...
spec/frontend/vue_shared/components/blob_viewers/rich_viewer_spec.js
View file @
d3fc3be0
import
{
shallowMount
}
from
'
@vue/test-utils
'
;
import
{
shallowMount
}
from
'
@vue/test-utils
'
;
import
RichViewer
from
'
~/vue_shared/components/blob_viewers/rich_viewer.vue
'
;
import
RichViewer
from
'
~/vue_shared/components/blob_viewers/rich_viewer.vue
'
;
import
{
handleBlobRichViewer
}
from
'
~/blob/viewer
'
;
jest
.
mock
(
'
~/blob/viewer
'
);
describe
(
'
Blob Rich Viewer component
'
,
()
=>
{
describe
(
'
Blob Rich Viewer component
'
,
()
=>
{
let
wrapper
;
let
wrapper
;
const
content
=
'
<h1 id="markdown">Foo Bar</h1>
'
;
const
content
=
'
<h1 id="markdown">Foo Bar</h1>
'
;
const
defaultType
=
'
markdown
'
;
function
createComponent
()
{
function
createComponent
(
type
=
defaultType
)
{
wrapper
=
shallowMount
(
RichViewer
,
{
wrapper
=
shallowMount
(
RichViewer
,
{
propsData
:
{
propsData
:
{
content
,
content
,
type
,
},
},
});
});
}
}
...
@@ -24,4 +29,8 @@ describe('Blob Rich Viewer component', () => {
...
@@ -24,4 +29,8 @@ describe('Blob Rich Viewer component', () => {
it
(
'
renders the passed content without transformations
'
,
()
=>
{
it
(
'
renders the passed content without transformations
'
,
()
=>
{
expect
(
wrapper
.
html
()).
toContain
(
content
);
expect
(
wrapper
.
html
()).
toContain
(
content
);
});
});
it
(
'
queries for advanced viewer
'
,
()
=>
{
expect
(
handleBlobRichViewer
).
toHaveBeenCalledWith
(
expect
.
anything
(),
defaultType
);
});
});
});
spec/frontend/vue_shared/components/blob_viewers/simple_viewer_spec.js
View file @
d3fc3be0
...
@@ -10,6 +10,7 @@ describe('Blob Simple Viewer component', () => {
...
@@ -10,6 +10,7 @@ describe('Blob Simple Viewer component', () => {
wrapper
=
shallowMount
(
SimpleViewer
,
{
wrapper
=
shallowMount
(
SimpleViewer
,
{
propsData
:
{
propsData
:
{
content
,
content
,
type
:
'
text
'
,
},
},
});
});
}
}
...
...
spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_button_spec.js
0 → 100644
View file @
d3fc3be0
import
Vuex
from
'
vuex
'
;
import
{
shallowMount
,
createLocalVue
}
from
'
@vue/test-utils
'
;
import
{
GlIcon
}
from
'
@gitlab/ui
'
;
import
DropdownButton
from
'
~/vue_shared/components/sidebar/labels_select_vue/dropdown_button.vue
'
;
import
labelSelectModule
from
'
~/vue_shared/components/sidebar/labels_select_vue/store
'
;
import
{
mockConfig
}
from
'
./mock_data
'
;
const
localVue
=
createLocalVue
();
localVue
.
use
(
Vuex
);
const
createComponent
=
(
initialState
=
mockConfig
)
=>
{
const
store
=
new
Vuex
.
Store
(
labelSelectModule
());
store
.
dispatch
(
'
setInitialState
'
,
initialState
);
return
shallowMount
(
DropdownButton
,
{
localVue
,
store
,
});
};
describe
(
'
DropdownButton
'
,
()
=>
{
let
wrapper
;
beforeEach
(()
=>
{
wrapper
=
createComponent
();
});
afterEach
(()
=>
{
wrapper
.
destroy
();
});
describe
(
'
template
'
,
()
=>
{
it
(
'
renders component container element
'
,
()
=>
{
expect
(
wrapper
.
is
(
'
gl-button-stub
'
)).
toBe
(
true
);
});
it
(
'
renders button text element
'
,
()
=>
{
const
dropdownTextEl
=
wrapper
.
find
(
'
.dropdown-toggle-text
'
);
expect
(
dropdownTextEl
.
exists
()).
toBe
(
true
);
expect
(
dropdownTextEl
.
text
()).
toBe
(
'
Label
'
);
});
it
(
'
renders chevron icon element
'
,
()
=>
{
const
iconEl
=
wrapper
.
find
(
GlIcon
);
expect
(
iconEl
.
exists
()).
toBe
(
true
);
expect
(
iconEl
.
props
(
'
name
'
)).
toBe
(
'
chevron-down
'
);
});
});
});
spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_create_view_spec.js
0 → 100644
View file @
d3fc3be0
import
Vuex
from
'
vuex
'
;
import
{
shallowMount
,
createLocalVue
}
from
'
@vue/test-utils
'
;
import
{
GlButton
,
GlIcon
,
GlFormInput
,
GlLink
,
GlLoadingIcon
}
from
'
@gitlab/ui
'
;
import
DropdownContentsCreateView
from
'
~/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_create_view.vue
'
;
import
labelSelectModule
from
'
~/vue_shared/components/sidebar/labels_select_vue/store
'
;
import
{
mockConfig
,
mockSuggestedColors
}
from
'
./mock_data
'
;
const
localVue
=
createLocalVue
();
localVue
.
use
(
Vuex
);
const
createComponent
=
(
initialState
=
mockConfig
)
=>
{
const
store
=
new
Vuex
.
Store
(
labelSelectModule
());
store
.
dispatch
(
'
setInitialState
'
,
initialState
);
return
shallowMount
(
DropdownContentsCreateView
,
{
localVue
,
store
,
});
};
describe
(
'
DropdownContentsCreateView
'
,
()
=>
{
let
wrapper
;
const
colors
=
Object
.
keys
(
mockSuggestedColors
).
map
(
color
=>
({
[
color
]:
mockSuggestedColors
[
color
],
}));
beforeEach
(()
=>
{
gon
.
suggested_label_colors
=
mockSuggestedColors
;
wrapper
=
createComponent
();
});
afterEach
(()
=>
{
wrapper
.
destroy
();
});
describe
(
'
computed
'
,
()
=>
{
describe
(
'
disableCreate
'
,
()
=>
{
it
(
'
returns `true` when label title and color is not defined
'
,
()
=>
{
expect
(
wrapper
.
vm
.
disableCreate
).
toBe
(
true
);
});
it
(
'
returns `true` when `labelCreateInProgress` is true
'
,
()
=>
{
wrapper
.
setData
({
labelTitle
:
'
Foo
'
,
selectedColor
:
'
#ff0000
'
,
});
wrapper
.
vm
.
$store
.
dispatch
(
'
requestCreateLabel
'
);
return
wrapper
.
vm
.
$nextTick
(()
=>
{
expect
(
wrapper
.
vm
.
disableCreate
).
toBe
(
true
);
});
});
it
(
'
returns `false` when label title and color is defined and create request is not already in progress
'
,
()
=>
{
wrapper
.
setData
({
labelTitle
:
'
Foo
'
,
selectedColor
:
'
#ff0000
'
,
});
return
wrapper
.
vm
.
$nextTick
(()
=>
{
expect
(
wrapper
.
vm
.
disableCreate
).
toBe
(
false
);
});
});
});
describe
(
'
suggestedColors
'
,
()
=>
{
it
(
'
returns array of color objects containing color code and name
'
,
()
=>
{
colors
.
forEach
((
color
,
index
)
=>
{
expect
(
wrapper
.
vm
.
suggestedColors
[
index
]).
toEqual
(
expect
.
objectContaining
(
color
));
});
});
});
});
describe
(
'
methods
'
,
()
=>
{
describe
(
'
getColorCode
'
,
()
=>
{
it
(
'
returns color code from color object
'
,
()
=>
{
expect
(
wrapper
.
vm
.
getColorCode
(
colors
[
0
])).
toBe
(
Object
.
keys
(
colors
[
0
]).
pop
());
});
});
describe
(
'
getColorName
'
,
()
=>
{
it
(
'
returns color name from color object
'
,
()
=>
{
expect
(
wrapper
.
vm
.
getColorName
(
colors
[
0
])).
toBe
(
Object
.
values
(
colors
[
0
]).
pop
());
});
});
describe
(
'
handleColorClick
'
,
()
=>
{
it
(
'
sets provided `color` param to `selectedColor` prop
'
,
()
=>
{
wrapper
.
vm
.
handleColorClick
(
colors
[
0
]);
expect
(
wrapper
.
vm
.
selectedColor
).
toBe
(
Object
.
keys
(
colors
[
0
]).
pop
());
});
});
describe
(
'
handleCreateClick
'
,
()
=>
{
it
(
'
calls action `createLabel` with object containing `labelTitle` & `selectedColor`
'
,
()
=>
{
jest
.
spyOn
(
wrapper
.
vm
,
'
createLabel
'
).
mockImplementation
();
wrapper
.
setData
({
labelTitle
:
'
Foo
'
,
selectedColor
:
'
#ff0000
'
,
});
wrapper
.
vm
.
handleCreateClick
();
return
wrapper
.
vm
.
$nextTick
(()
=>
{
expect
(
wrapper
.
vm
.
createLabel
).
toHaveBeenCalledWith
(
expect
.
objectContaining
({
title
:
'
Foo
'
,
color
:
'
#ff0000
'
,
}),
);
});
});
});
});
describe
(
'
template
'
,
()
=>
{
it
(
'
renders component container element with class "labels-select-contents-create"
'
,
()
=>
{
expect
(
wrapper
.
attributes
(
'
class
'
)).
toContain
(
'
labels-select-contents-create
'
);
});
it
(
'
renders dropdown back button element
'
,
()
=>
{
const
backBtnEl
=
wrapper
.
find
(
'
.dropdown-title
'
)
.
findAll
(
GlButton
)
.
at
(
0
);
expect
(
backBtnEl
.
exists
()).
toBe
(
true
);
expect
(
backBtnEl
.
attributes
(
'
aria-label
'
)).
toBe
(
'
Go back
'
);
expect
(
backBtnEl
.
find
(
GlIcon
).
props
(
'
name
'
)).
toBe
(
'
arrow-left
'
);
});
it
(
'
renders dropdown title element
'
,
()
=>
{
const
headerEl
=
wrapper
.
find
(
'
.dropdown-title > span
'
);
expect
(
headerEl
.
exists
()).
toBe
(
true
);
expect
(
headerEl
.
text
()).
toBe
(
'
Create label
'
);
});
it
(
'
renders dropdown close button element
'
,
()
=>
{
const
closeBtnEl
=
wrapper
.
find
(
'
.dropdown-title
'
)
.
findAll
(
GlButton
)
.
at
(
1
);
expect
(
closeBtnEl
.
exists
()).
toBe
(
true
);
expect
(
closeBtnEl
.
attributes
(
'
aria-label
'
)).
toBe
(
'
Close
'
);
expect
(
closeBtnEl
.
find
(
GlIcon
).
props
(
'
name
'
)).
toBe
(
'
close
'
);
});
it
(
'
renders label title input element
'
,
()
=>
{
const
titleInputEl
=
wrapper
.
find
(
'
.dropdown-input
'
).
find
(
GlFormInput
);
expect
(
titleInputEl
.
exists
()).
toBe
(
true
);
expect
(
titleInputEl
.
attributes
(
'
placeholder
'
)).
toBe
(
'
Name new label
'
);
expect
(
titleInputEl
.
attributes
(
'
autofocus
'
)).
toBe
(
'
true
'
);
});
it
(
'
renders color block element for all suggested colors
'
,
()
=>
{
const
colorBlocksEl
=
wrapper
.
find
(
'
.dropdown-content
'
).
findAll
(
GlLink
);
colorBlocksEl
.
wrappers
.
forEach
((
colorBlock
,
index
)
=>
{
expect
(
colorBlock
.
attributes
(
'
style
'
)).
toContain
(
'
background-color
'
);
expect
(
colorBlock
.
attributes
(
'
title
'
)).
toBe
(
Object
.
values
(
colors
[
index
]).
pop
());
});
});
it
(
'
renders color input element
'
,
()
=>
{
wrapper
.
setData
({
selectedColor
:
'
#ff0000
'
,
});
return
wrapper
.
vm
.
$nextTick
(()
=>
{
const
colorPreviewEl
=
wrapper
.
find
(
'
.color-input-container > .dropdown-label-color-preview
'
,
);
const
colorInputEl
=
wrapper
.
find
(
'
.color-input-container
'
).
find
(
GlFormInput
);
expect
(
colorPreviewEl
.
exists
()).
toBe
(
true
);
expect
(
colorPreviewEl
.
attributes
(
'
style
'
)).
toContain
(
'
background-color
'
);
expect
(
colorInputEl
.
exists
()).
toBe
(
true
);
expect
(
colorInputEl
.
attributes
(
'
placeholder
'
)).
toBe
(
'
Use custom color #FF0000
'
);
expect
(
colorInputEl
.
attributes
(
'
value
'
)).
toBe
(
'
#ff0000
'
);
});
});
it
(
'
renders create button element
'
,
()
=>
{
const
createBtnEl
=
wrapper
.
find
(
'
.dropdown-actions
'
)
.
findAll
(
GlButton
)
.
at
(
0
);
expect
(
createBtnEl
.
exists
()).
toBe
(
true
);
expect
(
createBtnEl
.
text
()).
toContain
(
'
Create
'
);
});
it
(
'
shows gl-loading-icon within create button element when `labelCreateInProgress` is `true`
'
,
()
=>
{
wrapper
.
vm
.
$store
.
dispatch
(
'
requestCreateLabel
'
);
return
wrapper
.
vm
.
$nextTick
(()
=>
{
const
loadingIconEl
=
wrapper
.
find
(
'
.dropdown-actions
'
).
find
(
GlLoadingIcon
);
expect
(
loadingIconEl
.
exists
()).
toBe
(
true
);
expect
(
loadingIconEl
.
isVisible
()).
toBe
(
true
);
});
});
it
(
'
renders cancel button element
'
,
()
=>
{
const
cancelBtnEl
=
wrapper
.
find
(
'
.dropdown-actions
'
)
.
findAll
(
GlButton
)
.
at
(
1
);
expect
(
cancelBtnEl
.
exists
()).
toBe
(
true
);
expect
(
cancelBtnEl
.
text
()).
toContain
(
'
Cancel
'
);
});
});
});
spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view_spec.js
0 → 100644
View file @
d3fc3be0
import
Vuex
from
'
vuex
'
;
import
{
shallowMount
,
createLocalVue
}
from
'
@vue/test-utils
'
;
import
{
GlButton
,
GlLoadingIcon
,
GlIcon
,
GlSearchBoxByType
,
GlLink
}
from
'
@gitlab/ui
'
;
import
{
UP_KEY_CODE
,
DOWN_KEY_CODE
,
ENTER_KEY_CODE
,
ESC_KEY_CODE
}
from
'
~/lib/utils/keycodes
'
;
import
DropdownContentsLabelsView
from
'
~/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view.vue
'
;
import
defaultState
from
'
~/vue_shared/components/sidebar/labels_select_vue/store/state
'
;
import
mutations
from
'
~/vue_shared/components/sidebar/labels_select_vue/store/mutations
'
;
import
*
as
actions
from
'
~/vue_shared/components/sidebar/labels_select_vue/store/actions
'
;
import
*
as
getters
from
'
~/vue_shared/components/sidebar/labels_select_vue/store/getters
'
;
import
{
mockConfig
,
mockLabels
,
mockRegularLabel
}
from
'
./mock_data
'
;
const
localVue
=
createLocalVue
();
localVue
.
use
(
Vuex
);
const
createComponent
=
(
initialState
=
mockConfig
)
=>
{
const
store
=
new
Vuex
.
Store
({
getters
,
mutations
,
state
:
{
...
defaultState
(),
footerCreateLabelTitle
:
'
Create label
'
,
footerManageLabelTitle
:
'
Manage labels
'
,
},
actions
:
{
...
actions
,
fetchLabels
:
jest
.
fn
(),
},
});
store
.
dispatch
(
'
setInitialState
'
,
initialState
);
store
.
dispatch
(
'
receiveLabelsSuccess
'
,
mockLabels
);
return
shallowMount
(
DropdownContentsLabelsView
,
{
localVue
,
store
,
});
};
describe
(
'
DropdownContentsLabelsView
'
,
()
=>
{
let
wrapper
;
beforeEach
(()
=>
{
wrapper
=
createComponent
();
});
afterEach
(()
=>
{
wrapper
.
destroy
();
});
describe
(
'
computed
'
,
()
=>
{
describe
(
'
visibleLabels
'
,
()
=>
{
it
(
'
returns matching labels filtered with `searchKey`
'
,
()
=>
{
wrapper
.
setData
({
searchKey
:
'
bug
'
,
});
expect
(
wrapper
.
vm
.
visibleLabels
.
length
).
toBe
(
1
);
expect
(
wrapper
.
vm
.
visibleLabels
[
0
].
title
).
toBe
(
'
Bug
'
);
});
it
(
'
returns all labels when `searchKey` is empty
'
,
()
=>
{
wrapper
.
setData
({
searchKey
:
''
,
});
expect
(
wrapper
.
vm
.
visibleLabels
.
length
).
toBe
(
mockLabels
.
length
);
});
});
});
describe
(
'
methods
'
,
()
=>
{
describe
(
'
getDropdownLabelBoxStyle
'
,
()
=>
{
it
(
'
returns an object containing `backgroundColor` based on provided `label` param
'
,
()
=>
{
expect
(
wrapper
.
vm
.
getDropdownLabelBoxStyle
(
mockRegularLabel
)).
toEqual
(
expect
.
objectContaining
({
backgroundColor
:
mockRegularLabel
.
color
,
}),
);
});
});
describe
(
'
isLabelSelected
'
,
()
=>
{
it
(
'
returns true when provided `label` param is one of the selected labels
'
,
()
=>
{
expect
(
wrapper
.
vm
.
isLabelSelected
(
mockRegularLabel
)).
toBe
(
true
);
});
it
(
'
returns false when provided `label` param is not one of the selected labels
'
,
()
=>
{
expect
(
wrapper
.
vm
.
isLabelSelected
(
mockLabels
[
2
])).
toBe
(
false
);
});
});
describe
(
'
handleKeyDown
'
,
()
=>
{
it
(
'
decreases `currentHighlightItem` value by 1 when Up arrow key is pressed
'
,
()
=>
{
wrapper
.
setData
({
currentHighlightItem
:
1
,
});
wrapper
.
vm
.
handleKeyDown
({
keyCode
:
UP_KEY_CODE
,
});
expect
(
wrapper
.
vm
.
currentHighlightItem
).
toBe
(
0
);
});
it
(
'
increases `currentHighlightItem` value by 1 when Down arrow key is pressed
'
,
()
=>
{
wrapper
.
setData
({
currentHighlightItem
:
1
,
});
wrapper
.
vm
.
handleKeyDown
({
keyCode
:
DOWN_KEY_CODE
,
});
expect
(
wrapper
.
vm
.
currentHighlightItem
).
toBe
(
2
);
});
it
(
'
calls action `updateSelectedLabels` with currently highlighted label when Enter key is pressed
'
,
()
=>
{
jest
.
spyOn
(
wrapper
.
vm
,
'
updateSelectedLabels
'
).
mockImplementation
();
wrapper
.
setData
({
currentHighlightItem
:
1
,
});
wrapper
.
vm
.
handleKeyDown
({
keyCode
:
ENTER_KEY_CODE
,
});
expect
(
wrapper
.
vm
.
updateSelectedLabels
).
toHaveBeenCalledWith
([
{
...
mockLabels
[
1
],
set
:
true
,
},
]);
});
it
(
'
calls action `toggleDropdownContents` when Esc key is pressed
'
,
()
=>
{
jest
.
spyOn
(
wrapper
.
vm
,
'
toggleDropdownContents
'
).
mockImplementation
();
wrapper
.
setData
({
currentHighlightItem
:
1
,
});
wrapper
.
vm
.
handleKeyDown
({
keyCode
:
ESC_KEY_CODE
,
});
expect
(
wrapper
.
vm
.
toggleDropdownContents
).
toHaveBeenCalled
();
});
it
(
'
calls action `scrollIntoViewIfNeeded` in next tick when any key is pressed
'
,
()
=>
{
jest
.
spyOn
(
wrapper
.
vm
,
'
scrollIntoViewIfNeeded
'
).
mockImplementation
();
wrapper
.
setData
({
currentHighlightItem
:
1
,
});
wrapper
.
vm
.
handleKeyDown
({
keyCode
:
DOWN_KEY_CODE
,
});
return
wrapper
.
vm
.
$nextTick
(()
=>
{
expect
(
wrapper
.
vm
.
scrollIntoViewIfNeeded
).
toHaveBeenCalled
();
});
});
});
describe
(
'
handleLabelClick
'
,
()
=>
{
it
(
'
calls action `updateSelectedLabels` with provided `label` param
'
,
()
=>
{
jest
.
spyOn
(
wrapper
.
vm
,
'
updateSelectedLabels
'
).
mockImplementation
();
wrapper
.
vm
.
handleLabelClick
(
mockRegularLabel
);
expect
(
wrapper
.
vm
.
updateSelectedLabels
).
toHaveBeenCalledWith
([
mockRegularLabel
]);
});
});
});
describe
(
'
template
'
,
()
=>
{
it
(
'
renders component container element with class `labels-select-contents-list`
'
,
()
=>
{
expect
(
wrapper
.
attributes
(
'
class
'
)).
toContain
(
'
labels-select-contents-list
'
);
});
it
(
'
renders gl-loading-icon component when `labelsFetchInProgress` prop is true
'
,
()
=>
{
wrapper
.
vm
.
$store
.
dispatch
(
'
requestLabels
'
);
return
wrapper
.
vm
.
$nextTick
(()
=>
{
const
loadingIconEl
=
wrapper
.
find
(
GlLoadingIcon
);
expect
(
loadingIconEl
.
exists
()).
toBe
(
true
);
expect
(
loadingIconEl
.
attributes
(
'
class
'
)).
toContain
(
'
labels-fetch-loading
'
);
});
});
it
(
'
renders dropdown title element
'
,
()
=>
{
const
titleEl
=
wrapper
.
find
(
'
.dropdown-title > span
'
);
expect
(
titleEl
.
exists
()).
toBe
(
true
);
expect
(
titleEl
.
text
()).
toBe
(
'
Assign labels
'
);
});
it
(
'
renders dropdown close button element
'
,
()
=>
{
const
closeButtonEl
=
wrapper
.
find
(
'
.dropdown-title
'
).
find
(
GlButton
);
expect
(
closeButtonEl
.
exists
()).
toBe
(
true
);
expect
(
closeButtonEl
.
find
(
GlIcon
).
exists
()).
toBe
(
true
);
expect
(
closeButtonEl
.
find
(
GlIcon
).
props
(
'
name
'
)).
toBe
(
'
close
'
);
});
it
(
'
renders label search input element
'
,
()
=>
{
const
searchInputEl
=
wrapper
.
find
(
GlSearchBoxByType
);
expect
(
searchInputEl
.
exists
()).
toBe
(
true
);
expect
(
searchInputEl
.
attributes
(
'
autofocus
'
)).
toBe
(
'
true
'
);
});
it
(
'
renders label elements for all labels
'
,
()
=>
{
const
labelsEl
=
wrapper
.
findAll
(
'
.dropdown-content li
'
);
const
labelItemEl
=
labelsEl
.
at
(
0
).
find
(
GlLink
);
expect
(
labelsEl
.
length
).
toBe
(
mockLabels
.
length
);
expect
(
labelItemEl
.
exists
()).
toBe
(
true
);
expect
(
labelItemEl
.
find
(
GlIcon
).
props
(
'
name
'
)).
toBe
(
'
mobile-issue-close
'
);
expect
(
labelItemEl
.
find
(
'
.dropdown-label-box
'
).
attributes
(
'
style
'
)).
toBe
(
'
background-color: rgb(186, 218, 85);
'
,
);
expect
(
labelItemEl
.
find
(
GlLink
).
text
()).
toContain
(
mockLabels
[
0
].
title
);
});
it
(
'
renders label element with "is-focused" when value of `currentHighlightItem` is more than -1
'
,
()
=>
{
wrapper
.
setData
({
currentHighlightItem
:
0
,
});
return
wrapper
.
vm
.
$nextTick
(()
=>
{
const
labelsEl
=
wrapper
.
findAll
(
'
.dropdown-content li
'
);
const
labelItemEl
=
labelsEl
.
at
(
0
).
find
(
GlLink
);
expect
(
labelItemEl
.
attributes
(
'
class
'
)).
toContain
(
'
is-focused
'
);
});
});
it
(
'
renders element containing "No matching results" when `searchKey` does not match with any label
'
,
()
=>
{
wrapper
.
setData
({
searchKey
:
'
abc
'
,
});
return
wrapper
.
vm
.
$nextTick
(()
=>
{
const
noMatchEl
=
wrapper
.
find
(
'
.dropdown-content li
'
);
expect
(
noMatchEl
.
exists
()).
toBe
(
true
);
expect
(
noMatchEl
.
text
()).
toContain
(
'
No matching results
'
);
});
});
it
(
'
renders footer list items
'
,
()
=>
{
const
createLabelBtn
=
wrapper
.
find
(
'
.dropdown-footer
'
).
find
(
GlButton
);
const
manageLabelsLink
=
wrapper
.
find
(
'
.dropdown-footer
'
).
find
(
GlLink
);
expect
(
createLabelBtn
.
exists
()).
toBe
(
true
);
expect
(
createLabelBtn
.
text
()).
toBe
(
'
Create label
'
);
expect
(
manageLabelsLink
.
exists
()).
toBe
(
true
);
expect
(
manageLabelsLink
.
text
()).
toBe
(
'
Manage labels
'
);
});
});
});
spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_spec.js
0 → 100644
View file @
d3fc3be0
import
Vuex
from
'
vuex
'
;
import
{
shallowMount
,
createLocalVue
}
from
'
@vue/test-utils
'
;
import
DropdownContents
from
'
~/vue_shared/components/sidebar/labels_select_vue/dropdown_contents.vue
'
;
import
labelsSelectModule
from
'
~/vue_shared/components/sidebar/labels_select_vue/store
'
;
import
{
mockConfig
}
from
'
./mock_data
'
;
const
localVue
=
createLocalVue
();
localVue
.
use
(
Vuex
);
const
createComponent
=
(
initialState
=
mockConfig
)
=>
{
const
store
=
new
Vuex
.
Store
(
labelsSelectModule
());
store
.
dispatch
(
'
setInitialState
'
,
initialState
);
return
shallowMount
(
DropdownContents
,
{
localVue
,
store
,
});
};
describe
(
'
DropdownContent
'
,
()
=>
{
let
wrapper
;
beforeEach
(()
=>
{
wrapper
=
createComponent
();
});
afterEach
(()
=>
{
wrapper
.
destroy
();
});
describe
(
'
computed
'
,
()
=>
{
describe
(
'
dropdownContentsView
'
,
()
=>
{
it
(
'
returns string "dropdown-contents-create-view" when `showDropdownContentsCreateView` prop is `true`
'
,
()
=>
{
wrapper
.
vm
.
$store
.
dispatch
(
'
toggleDropdownContentsCreateView
'
);
expect
(
wrapper
.
vm
.
dropdownContentsView
).
toBe
(
'
dropdown-contents-create-view
'
);
});
it
(
'
returns string "dropdown-contents-labels-view" when `showDropdownContentsCreateView` prop is `false`
'
,
()
=>
{
expect
(
wrapper
.
vm
.
dropdownContentsView
).
toBe
(
'
dropdown-contents-labels-view
'
);
});
});
});
describe
(
'
template
'
,
()
=>
{
it
(
'
renders component container element with class `labels-select-dropdown-contents`
'
,
()
=>
{
expect
(
wrapper
.
attributes
(
'
class
'
)).
toContain
(
'
labels-select-dropdown-contents
'
);
});
});
});
spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_title_spec.js
0 → 100644
View file @
d3fc3be0
import
Vuex
from
'
vuex
'
;
import
{
shallowMount
,
createLocalVue
}
from
'
@vue/test-utils
'
;
import
{
GlButton
,
GlLoadingIcon
}
from
'
@gitlab/ui
'
;
import
DropdownTitle
from
'
~/vue_shared/components/sidebar/labels_select_vue/dropdown_title.vue
'
;
import
labelsSelectModule
from
'
~/vue_shared/components/sidebar/labels_select_vue/store
'
;
import
{
mockConfig
}
from
'
./mock_data
'
;
const
localVue
=
createLocalVue
();
localVue
.
use
(
Vuex
);
const
createComponent
=
(
initialState
=
mockConfig
)
=>
{
const
store
=
new
Vuex
.
Store
(
labelsSelectModule
());
store
.
dispatch
(
'
setInitialState
'
,
initialState
);
return
shallowMount
(
DropdownTitle
,
{
localVue
,
store
,
propsData
:
{
labelsSelectInProgress
:
false
,
},
});
};
describe
(
'
DropdownTitle
'
,
()
=>
{
let
wrapper
;
beforeEach
(()
=>
{
wrapper
=
createComponent
();
});
afterEach
(()
=>
{
wrapper
.
destroy
();
});
describe
(
'
template
'
,
()
=>
{
it
(
'
renders component container element with string "Labels"
'
,
()
=>
{
expect
(
wrapper
.
text
()).
toContain
(
'
Labels
'
);
});
it
(
'
renders edit link
'
,
()
=>
{
const
editBtnEl
=
wrapper
.
find
(
GlButton
);
expect
(
editBtnEl
.
exists
()).
toBe
(
true
);
expect
(
editBtnEl
.
text
()).
toBe
(
'
Edit
'
);
});
it
(
'
renders loading icon element when `labelsSelectInProgress` prop is true
'
,
()
=>
{
wrapper
.
setProps
({
labelsSelectInProgress
:
true
,
});
return
wrapper
.
vm
.
$nextTick
(()
=>
{
expect
(
wrapper
.
find
(
GlLoadingIcon
).
isVisible
()).
toBe
(
true
);
});
});
});
});
spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_value_spec.js
0 → 100644
View file @
d3fc3be0
import
Vuex
from
'
vuex
'
;
import
{
shallowMount
,
createLocalVue
}
from
'
@vue/test-utils
'
;
import
{
GlLabel
}
from
'
@gitlab/ui
'
;
import
DropdownValue
from
'
~/vue_shared/components/sidebar/labels_select_vue/dropdown_value.vue
'
;
import
labelsSelectModule
from
'
~/vue_shared/components/sidebar/labels_select_vue/store
'
;
import
{
mockConfig
,
mockRegularLabel
,
mockScopedLabel
}
from
'
./mock_data
'
;
const
localVue
=
createLocalVue
();
localVue
.
use
(
Vuex
);
const
createComponent
=
(
initialState
=
mockConfig
,
slots
=
{})
=>
{
const
store
=
new
Vuex
.
Store
(
labelsSelectModule
());
store
.
dispatch
(
'
setInitialState
'
,
initialState
);
return
shallowMount
(
DropdownValue
,
{
localVue
,
store
,
slots
,
});
};
describe
(
'
DropdownValue
'
,
()
=>
{
let
wrapper
;
beforeEach
(()
=>
{
wrapper
=
createComponent
();
});
afterEach
(()
=>
{
wrapper
.
destroy
();
});
describe
(
'
methods
'
,
()
=>
{
describe
(
'
labelFilterUrl
'
,
()
=>
{
it
(
'
returns a label filter URL based on provided label param
'
,
()
=>
{
expect
(
wrapper
.
vm
.
labelFilterUrl
(
mockRegularLabel
)).
toBe
(
'
/gitlab-org/my-project/issues?label_name[]=Foo%20Label
'
,
);
});
});
describe
(
'
scopedLabel
'
,
()
=>
{
it
(
'
returns `true` when provided label param is a scoped label
'
,
()
=>
{
expect
(
wrapper
.
vm
.
scopedLabel
(
mockScopedLabel
)).
toBe
(
true
);
});
it
(
'
returns `false` when provided label param is a regular label
'
,
()
=>
{
expect
(
wrapper
.
vm
.
scopedLabel
(
mockRegularLabel
)).
toBe
(
false
);
});
});
});
describe
(
'
template
'
,
()
=>
{
it
(
'
renders class `has-labels` on component container element when `selectedLabels` is not empty
'
,
()
=>
{
expect
(
wrapper
.
attributes
(
'
class
'
)).
toContain
(
'
has-labels
'
);
});
it
(
'
renders element containing `None` when `selectedLabels` is empty
'
,
()
=>
{
const
wrapperNoLabels
=
createComponent
(
{
...
mockConfig
,
selectedLabels
:
[],
},
{
default
:
'
None
'
,
},
);
const
noneEl
=
wrapperNoLabels
.
find
(
'
span.text-secondary
'
);
expect
(
noneEl
.
exists
()).
toBe
(
true
);
expect
(
noneEl
.
text
()).
toBe
(
'
None
'
);
wrapperNoLabels
.
destroy
();
});
it
(
'
renders labels when `selectedLabels` is not empty
'
,
()
=>
{
expect
(
wrapper
.
findAll
(
GlLabel
).
length
).
toBe
(
2
);
});
});
});
spec/frontend/vue_shared/components/sidebar/labels_select_vue/labels_select_root_spec.js
0 → 100644
View file @
d3fc3be0
import
Vuex
from
'
vuex
'
;
import
{
shallowMount
,
createLocalVue
}
from
'
@vue/test-utils
'
;
import
LabelsSelectRoot
from
'
~/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue
'
;
import
DropdownTitle
from
'
~/vue_shared/components/sidebar/labels_select_vue/dropdown_title.vue
'
;
import
DropdownValue
from
'
~/vue_shared/components/sidebar/labels_select_vue/dropdown_value.vue
'
;
import
DropdownValueCollapsed
from
'
~/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue
'
;
import
DropdownButton
from
'
~/vue_shared/components/sidebar/labels_select_vue/dropdown_button.vue
'
;
import
DropdownContents
from
'
~/vue_shared/components/sidebar/labels_select_vue/dropdown_contents.vue
'
;
import
labelsSelectModule
from
'
~/vue_shared/components/sidebar/labels_select_vue/store
'
;
import
{
mockConfig
}
from
'
./mock_data
'
;
const
localVue
=
createLocalVue
();
localVue
.
use
(
Vuex
);
const
createComponent
=
(
config
=
mockConfig
,
slots
=
{})
=>
shallowMount
(
LabelsSelectRoot
,
{
localVue
,
slots
,
store
:
new
Vuex
.
Store
(
labelsSelectModule
()),
propsData
:
config
,
});
describe
(
'
LabelsSelectRoot
'
,
()
=>
{
let
wrapper
;
beforeEach
(()
=>
{
wrapper
=
createComponent
();
});
afterEach
(()
=>
{
wrapper
.
destroy
();
});
describe
(
'
methods
'
,
()
=>
{
describe
(
'
handleVuexActionDispatch
'
,
()
=>
{
it
(
'
calls `handleDropdownClose` when params `action.type` is `toggleDropdownContents` and state has `showDropdownButton` & `showDropdownContents` props `false`
'
,
()
=>
{
jest
.
spyOn
(
wrapper
.
vm
,
'
handleDropdownClose
'
).
mockImplementation
();
wrapper
.
vm
.
handleVuexActionDispatch
(
{
type
:
'
toggleDropdownContents
'
},
{
showDropdownButton
:
false
,
showDropdownContents
:
false
,
labels
:
[{
id
:
1
},
{
id
:
2
,
touched
:
true
}],
},
);
expect
(
wrapper
.
vm
.
handleDropdownClose
).
toHaveBeenCalledWith
(
expect
.
arrayContaining
([
{
id
:
2
,
touched
:
true
,
},
]),
);
});
});
describe
(
'
handleDropdownClose
'
,
()
=>
{
it
(
'
emits `updateSelectedLabels` & `onDropdownClose` events on component when provided `labels` param is not empty
'
,
()
=>
{
wrapper
.
vm
.
handleDropdownClose
([{
id
:
1
},
{
id
:
2
}]);
expect
(
wrapper
.
emitted
().
updateSelectedLabels
).
toBeTruthy
();
expect
(
wrapper
.
emitted
().
onDropdownClose
).
toBeTruthy
();
});
it
(
'
emits only `onDropdownClose` event on component when provided `labels` param is empty
'
,
()
=>
{
wrapper
.
vm
.
handleDropdownClose
([]);
expect
(
wrapper
.
emitted
().
updateSelectedLabels
).
toBeFalsy
();
expect
(
wrapper
.
emitted
().
onDropdownClose
).
toBeTruthy
();
});
});
describe
(
'
handleCollapsedValueClick
'
,
()
=>
{
it
(
'
emits `toggleCollapse` event on component
'
,
()
=>
{
wrapper
.
vm
.
handleCollapsedValueClick
();
expect
(
wrapper
.
emitted
().
toggleCollapse
).
toBeTruthy
();
});
});
});
describe
(
'
template
'
,
()
=>
{
it
(
'
renders component with classes `labels-select-wrapper position-relative`
'
,
()
=>
{
expect
(
wrapper
.
attributes
(
'
class
'
)).
toContain
(
'
labels-select-wrapper position-relative
'
);
});
it
(
'
renders `dropdown-value-collapsed` component when `allowLabelCreate` prop is `true`
'
,
()
=>
{
expect
(
wrapper
.
find
(
DropdownValueCollapsed
).
exists
()).
toBe
(
true
);
});
it
(
'
renders `dropdown-title` component
'
,
()
=>
{
expect
(
wrapper
.
find
(
DropdownTitle
).
exists
()).
toBe
(
true
);
});
it
(
'
renders `dropdown-value` component with slot when `showDropdownButton` prop is `false`
'
,
()
=>
{
const
wrapperDropdownValue
=
createComponent
(
mockConfig
,
{
default
:
'
None
'
,
});
const
valueComp
=
wrapperDropdownValue
.
find
(
DropdownValue
);
expect
(
valueComp
.
exists
()).
toBe
(
true
);
expect
(
valueComp
.
text
()).
toBe
(
'
None
'
);
wrapperDropdownValue
.
destroy
();
});
it
(
'
renders `dropdown-button` component when `showDropdownButton` prop is `true`
'
,
()
=>
{
wrapper
.
vm
.
$store
.
dispatch
(
'
toggleDropdownButton
'
);
expect
(
wrapper
.
find
(
DropdownButton
).
exists
()).
toBe
(
true
);
});
it
(
'
renders `dropdown-contents` component when `showDropdownButton` & `showDropdownContents` prop is `true`
'
,
()
=>
{
wrapper
.
vm
.
$store
.
dispatch
(
'
toggleDropdownContents
'
);
return
wrapper
.
vm
.
$nextTick
(()
=>
{
expect
(
wrapper
.
find
(
DropdownContents
).
exists
()).
toBe
(
true
);
});
});
});
});
spec/frontend/vue_shared/components/sidebar/labels_select_vue/mock_data.js
0 → 100644
View file @
d3fc3be0
export
const
mockRegularLabel
=
{
id
:
26
,
title
:
'
Foo Label
'
,
description
:
'
Foobar
'
,
color
:
'
#BADA55
'
,
textColor
:
'
#FFFFFF
'
,
};
export
const
mockScopedLabel
=
{
id
:
27
,
title
:
'
Foo::Bar
'
,
description
:
'
Foobar
'
,
color
:
'
#0033CC
'
,
textColor
:
'
#FFFFFF
'
,
};
export
const
mockLabels
=
[
mockRegularLabel
,
mockScopedLabel
,
{
id
:
28
,
title
:
'
Bug
'
,
description
:
'
Label for bugs
'
,
color
:
'
#FF0000
'
,
textColor
:
'
#FFFFFF
'
,
},
];
export
const
mockConfig
=
{
allowLabelEdit
:
true
,
allowLabelCreate
:
true
,
allowScopedLabels
:
true
,
labelsListTitle
:
'
Assign labels
'
,
labelsCreateTitle
:
'
Create label
'
,
dropdownOnly
:
false
,
selectedLabels
:
[
mockRegularLabel
,
mockScopedLabel
],
labelsSelectInProgress
:
false
,
labelsFetchPath
:
'
/gitlab-org/my-project/-/labels.json
'
,
labelsManagePath
:
'
/gitlab-org/my-project/-/labels
'
,
labelsFilterBasePath
:
'
/gitlab-org/my-project/issues
'
,
scopedLabelsDocumentationPath
:
'
/help/user/project/labels.md#scoped-labels-premium
'
,
};
export
const
mockSuggestedColors
=
{
'
#0033CC
'
:
'
UA blue
'
,
'
#428BCA
'
:
'
Moderate blue
'
,
'
#44AD8E
'
:
'
Lime green
'
,
'
#A8D695
'
:
'
Feijoa
'
,
'
#5CB85C
'
:
'
Slightly desaturated green
'
,
'
#69D100
'
:
'
Bright green
'
,
'
#004E00
'
:
'
Very dark lime green
'
,
'
#34495E
'
:
'
Very dark desaturated blue
'
,
'
#7F8C8D
'
:
'
Dark grayish cyan
'
,
'
#A295D6
'
:
'
Slightly desaturated blue
'
,
'
#5843AD
'
:
'
Dark moderate blue
'
,
'
#8E44AD
'
:
'
Dark moderate violet
'
,
'
#FFECDB
'
:
'
Very pale orange
'
,
'
#AD4363
'
:
'
Dark moderate pink
'
,
'
#D10069
'
:
'
Strong pink
'
,
'
#CC0033
'
:
'
Strong red
'
,
'
#FF0000
'
:
'
Pure red
'
,
'
#D9534F
'
:
'
Soft red
'
,
'
#D1D100
'
:
'
Strong yellow
'
,
'
#F0AD4E
'
:
'
Soft orange
'
,
'
#AD8D43
'
:
'
Dark moderate orange
'
,
};
spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/actions_spec.js
0 → 100644
View file @
d3fc3be0
import
MockAdapter
from
'
axios-mock-adapter
'
;
import
defaultState
from
'
~/vue_shared/components/sidebar/labels_select_vue/store/state
'
;
import
*
as
types
from
'
~/vue_shared/components/sidebar/labels_select_vue/store/mutation_types
'
;
import
*
as
actions
from
'
~/vue_shared/components/sidebar/labels_select_vue/store/actions
'
;
import
testAction
from
'
helpers/vuex_action_helper
'
;
import
axios
from
'
~/lib/utils/axios_utils
'
;
describe
(
'
LabelsSelect Actions
'
,
()
=>
{
let
state
;
const
mockInitialState
=
{
labels
:
[],
selectedLabels
:
[],
};
beforeEach
(()
=>
{
state
=
Object
.
assign
({},
defaultState
());
});
describe
(
'
setInitialState
'
,
()
=>
{
it
(
'
sets initial store state
'
,
done
=>
{
testAction
(
actions
.
setInitialState
,
mockInitialState
,
state
,
[{
type
:
types
.
SET_INITIAL_STATE
,
payload
:
mockInitialState
}],
[],
done
,
);
});
});
describe
(
'
toggleDropdownButton
'
,
()
=>
{
it
(
'
toggles dropdown button
'
,
done
=>
{
testAction
(
actions
.
toggleDropdownButton
,
{},
state
,
[{
type
:
types
.
TOGGLE_DROPDOWN_BUTTON
}],
[],
done
,
);
});
});
describe
(
'
toggleDropdownContents
'
,
()
=>
{
it
(
'
toggles dropdown contents
'
,
done
=>
{
testAction
(
actions
.
toggleDropdownContents
,
{},
state
,
[{
type
:
types
.
TOGGLE_DROPDOWN_CONTENTS
}],
[],
done
,
);
});
});
describe
(
'
toggleDropdownContentsCreateView
'
,
()
=>
{
it
(
'
toggles dropdown create view
'
,
done
=>
{
testAction
(
actions
.
toggleDropdownContentsCreateView
,
{},
state
,
[{
type
:
types
.
TOGGLE_DROPDOWN_CONTENTS_CREATE_VIEW
}],
[],
done
,
);
});
});
describe
(
'
requestLabels
'
,
()
=>
{
it
(
'
sets value of `state.labelsFetchInProgress` to `true`
'
,
done
=>
{
testAction
(
actions
.
requestLabels
,
{},
state
,
[{
type
:
types
.
REQUEST_LABELS
}],
[],
done
);
});
});
describe
(
'
receiveLabelsSuccess
'
,
()
=>
{
it
(
'
sets provided labels to `state.labels`
'
,
done
=>
{
const
labels
=
[{
id
:
1
},
{
id
:
2
},
{
id
:
3
},
{
id
:
4
}];
testAction
(
actions
.
receiveLabelsSuccess
,
labels
,
state
,
[{
type
:
types
.
RECEIVE_SET_LABELS_SUCCESS
,
payload
:
labels
}],
[],
done
,
);
});
});
describe
(
'
receiveLabelsFailure
'
,
()
=>
{
beforeEach
(()
=>
{
setFixtures
(
'
<div class="flash-container"></div>
'
);
});
it
(
'
sets value `state.labelsFetchInProgress` to `false`
'
,
done
=>
{
testAction
(
actions
.
receiveLabelsFailure
,
{},
state
,
[{
type
:
types
.
RECEIVE_SET_LABELS_FAILURE
}],
[],
done
,
);
});
it
(
'
shows flash error
'
,
()
=>
{
actions
.
receiveLabelsFailure
({
commit
:
()
=>
{}
});
expect
(
document
.
querySelector
(
'
.flash-container .flash-text
'
).
innerText
.
trim
()).
toBe
(
'
Error fetching labels.
'
,
);
});
});
describe
(
'
fetchLabels
'
,
()
=>
{
let
mock
;
beforeEach
(()
=>
{
mock
=
new
MockAdapter
(
axios
);
state
.
labelsFetchPath
=
'
labels.json
'
;
});
afterEach
(()
=>
{
mock
.
restore
();
});
describe
(
'
on success
'
,
()
=>
{
it
(
'
dispatches `requestLabels` & `receiveLabelsSuccess` actions
'
,
done
=>
{
const
labels
=
[{
id
:
1
},
{
id
:
2
},
{
id
:
3
},
{
id
:
4
}];
mock
.
onGet
(
/labels.json/
).
replyOnce
(
200
,
labels
);
testAction
(
actions
.
fetchLabels
,
{},
state
,
[],
[{
type
:
'
requestLabels
'
},
{
type
:
'
receiveLabelsSuccess
'
,
payload
:
labels
}],
done
,
);
});
});
describe
(
'
on failure
'
,
()
=>
{
it
(
'
dispatches `requestLabels` & `receiveLabelsFailure` actions
'
,
done
=>
{
mock
.
onGet
(
/labels.json/
).
replyOnce
(
500
,
{});
testAction
(
actions
.
fetchLabels
,
{},
state
,
[],
[{
type
:
'
requestLabels
'
},
{
type
:
'
receiveLabelsFailure
'
}],
done
,
);
});
});
});
describe
(
'
requestCreateLabel
'
,
()
=>
{
it
(
'
sets value `state.labelCreateInProgress` to `true`
'
,
done
=>
{
testAction
(
actions
.
requestCreateLabel
,
{},
state
,
[{
type
:
types
.
REQUEST_CREATE_LABEL
}],
[],
done
,
);
});
});
describe
(
'
receiveCreateLabelSuccess
'
,
()
=>
{
it
(
'
sets value `state.labelCreateInProgress` to `false`
'
,
done
=>
{
testAction
(
actions
.
receiveCreateLabelSuccess
,
{},
state
,
[{
type
:
types
.
RECEIVE_CREATE_LABEL_SUCCESS
}],
[],
done
,
);
});
});
describe
(
'
receiveCreateLabelFailure
'
,
()
=>
{
beforeEach
(()
=>
{
setFixtures
(
'
<div class="flash-container"></div>
'
);
});
it
(
'
sets value `state.labelCreateInProgress` to `false`
'
,
done
=>
{
testAction
(
actions
.
receiveCreateLabelFailure
,
{},
state
,
[{
type
:
types
.
RECEIVE_CREATE_LABEL_FAILURE
}],
[],
done
,
);
});
it
(
'
shows flash error
'
,
()
=>
{
actions
.
receiveCreateLabelFailure
({
commit
:
()
=>
{}
});
expect
(
document
.
querySelector
(
'
.flash-container .flash-text
'
).
innerText
.
trim
()).
toBe
(
'
Error creating label.
'
,
);
});
});
describe
(
'
createLabel
'
,
()
=>
{
let
mock
;
beforeEach
(()
=>
{
mock
=
new
MockAdapter
(
axios
);
state
.
labelsManagePath
=
'
labels.json
'
;
});
afterEach
(()
=>
{
mock
.
restore
();
});
describe
(
'
on success
'
,
()
=>
{
it
(
'
dispatches `requestCreateLabel`, `receiveCreateLabelSuccess` & `toggleDropdownContentsCreateView` actions
'
,
done
=>
{
const
label
=
{
id
:
1
};
mock
.
onPost
(
/labels.json/
).
replyOnce
(
200
,
label
);
testAction
(
actions
.
createLabel
,
{},
state
,
[],
[
{
type
:
'
requestCreateLabel
'
},
{
type
:
'
receiveCreateLabelSuccess
'
},
{
type
:
'
toggleDropdownContentsCreateView
'
},
],
done
,
);
});
});
describe
(
'
on failure
'
,
()
=>
{
it
(
'
dispatches `requestCreateLabel` & `receiveCreateLabelFailure` actions
'
,
done
=>
{
mock
.
onPost
(
/labels.json/
).
replyOnce
(
500
,
{});
testAction
(
actions
.
createLabel
,
{},
state
,
[],
[{
type
:
'
requestCreateLabel
'
},
{
type
:
'
receiveCreateLabelFailure
'
}],
done
,
);
});
});
});
describe
(
'
updateSelectedLabels
'
,
()
=>
{
it
(
'
updates `state.labels` based on provided `labels` param
'
,
done
=>
{
const
labels
=
[{
id
:
1
},
{
id
:
2
},
{
id
:
3
},
{
id
:
4
}];
testAction
(
actions
.
updateSelectedLabels
,
labels
,
state
,
[{
type
:
types
.
UPDATE_SELECTED_LABELS
,
payload
:
{
labels
}
}],
[],
done
,
);
});
});
});
spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/getters_spec.js
0 → 100644
View file @
d3fc3be0
import
*
as
getters
from
'
~/vue_shared/components/sidebar/labels_select_vue/store/getters
'
;
describe
(
'
LabelsSelect Getters
'
,
()
=>
{
describe
(
'
dropdownButtonText
'
,
()
=>
{
it
(
'
returns string "Label" when state.labels has no selected labels
'
,
()
=>
{
const
labels
=
[{
id
:
1
},
{
id
:
2
},
{
id
:
3
},
{
id
:
4
}];
expect
(
getters
.
dropdownButtonText
({
labels
})).
toBe
(
'
Label
'
);
});
it
(
'
returns label title when state.labels has only 1 label
'
,
()
=>
{
const
labels
=
[{
id
:
1
,
title
:
'
Foobar
'
,
set
:
true
}];
expect
(
getters
.
dropdownButtonText
({
labels
})).
toBe
(
'
Foobar
'
);
});
it
(
'
returns first label title and remaining labels count when state.labels has more than 1 label
'
,
()
=>
{
const
labels
=
[{
id
:
1
,
title
:
'
Foo
'
,
set
:
true
},
{
id
:
2
,
title
:
'
Bar
'
,
set
:
true
}];
expect
(
getters
.
dropdownButtonText
({
labels
})).
toBe
(
'
Foo +1 more
'
);
});
});
describe
(
'
selectedLabelsList
'
,
()
=>
{
it
(
'
returns array of IDs of all labels within `state.selectedLabels`
'
,
()
=>
{
const
selectedLabels
=
[{
id
:
1
},
{
id
:
2
},
{
id
:
3
},
{
id
:
4
}];
expect
(
getters
.
selectedLabelsList
({
selectedLabels
})).
toEqual
([
1
,
2
,
3
,
4
]);
});
});
});
spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/mutations_spec.js
0 → 100644
View file @
d3fc3be0
import
mutations
from
'
~/vue_shared/components/sidebar/labels_select_vue/store/mutations
'
;
import
*
as
types
from
'
~/vue_shared/components/sidebar/labels_select_vue/store/mutation_types
'
;
describe
(
'
LabelsSelect Mutations
'
,
()
=>
{
describe
(
`
${
types
.
SET_INITIAL_STATE
}
`
,
()
=>
{
it
(
'
initializes provided props to store state
'
,
()
=>
{
const
state
=
{};
mutations
[
types
.
SET_INITIAL_STATE
](
state
,
{
labels
:
'
foo
'
,
});
expect
(
state
.
labels
).
toEqual
(
'
foo
'
);
});
});
describe
(
`
${
types
.
TOGGLE_DROPDOWN_BUTTON
}
`
,
()
=>
{
it
(
'
toggles value of `state.showDropdownButton`
'
,
()
=>
{
const
state
=
{
showDropdownButton
:
false
,
};
mutations
[
types
.
TOGGLE_DROPDOWN_BUTTON
](
state
);
expect
(
state
.
showDropdownButton
).
toBe
(
true
);
});
});
describe
(
`
${
types
.
TOGGLE_DROPDOWN_CONTENTS
}
`
,
()
=>
{
it
(
'
toggles value of `state.showDropdownButton` when `state.dropdownOnly` is false
'
,
()
=>
{
const
state
=
{
dropdownOnly
:
false
,
showDropdownButton
:
false
,
};
mutations
[
types
.
TOGGLE_DROPDOWN_CONTENTS
](
state
);
expect
(
state
.
showDropdownButton
).
toBe
(
true
);
});
it
(
'
toggles value of `state.showDropdownContents`
'
,
()
=>
{
const
state
=
{
showDropdownContents
:
false
,
};
mutations
[
types
.
TOGGLE_DROPDOWN_CONTENTS
](
state
);
expect
(
state
.
showDropdownContents
).
toBe
(
true
);
});
it
(
'
sets value of `state.showDropdownContentsCreateView` to `false` when `showDropdownContents` is true
'
,
()
=>
{
const
state
=
{
showDropdownContents
:
false
,
showDropdownContentsCreateView
:
true
,
};
mutations
[
types
.
TOGGLE_DROPDOWN_CONTENTS
](
state
);
expect
(
state
.
showDropdownContentsCreateView
).
toBe
(
false
);
});
});
describe
(
`
${
types
.
TOGGLE_DROPDOWN_CONTENTS_CREATE_VIEW
}
`
,
()
=>
{
it
(
'
toggles value of `state.showDropdownContentsCreateView`
'
,
()
=>
{
const
state
=
{
showDropdownContentsCreateView
:
false
,
};
mutations
[
types
.
TOGGLE_DROPDOWN_CONTENTS_CREATE_VIEW
](
state
);
expect
(
state
.
showDropdownContentsCreateView
).
toBe
(
true
);
});
});
describe
(
`
${
types
.
REQUEST_LABELS
}
`
,
()
=>
{
it
(
'
sets value of `state.labelsFetchInProgress` to true
'
,
()
=>
{
const
state
=
{
labelsFetchInProgress
:
false
,
};
mutations
[
types
.
REQUEST_LABELS
](
state
);
expect
(
state
.
labelsFetchInProgress
).
toBe
(
true
);
});
});
describe
(
`
${
types
.
RECEIVE_SET_LABELS_SUCCESS
}
`
,
()
=>
{
const
selectedLabels
=
[{
id
:
2
},
{
id
:
4
}];
const
labels
=
[{
id
:
1
},
{
id
:
2
},
{
id
:
3
},
{
id
:
4
}];
it
(
'
sets value of `state.labelsFetchInProgress` to false
'
,
()
=>
{
const
state
=
{
selectedLabels
,
labelsFetchInProgress
:
true
,
};
mutations
[
types
.
RECEIVE_SET_LABELS_SUCCESS
](
state
,
labels
);
expect
(
state
.
labelsFetchInProgress
).
toBe
(
false
);
});
it
(
'
sets provided `labels` to `state.labels` along with `set` prop based on `state.selectedLabels`
'
,
()
=>
{
const
selectedLabelIds
=
selectedLabels
.
map
(
label
=>
label
.
id
);
const
state
=
{
selectedLabels
,
labelsFetchInProgress
:
true
,
};
mutations
[
types
.
RECEIVE_SET_LABELS_SUCCESS
](
state
,
labels
);
state
.
labels
.
forEach
(
label
=>
{
if
(
selectedLabelIds
.
includes
(
label
.
id
))
{
expect
(
label
.
set
).
toBe
(
true
);
}
});
});
});
describe
(
`
${
types
.
RECEIVE_SET_LABELS_FAILURE
}
`
,
()
=>
{
it
(
'
sets value of `state.labelsFetchInProgress` to false
'
,
()
=>
{
const
state
=
{
labelsFetchInProgress
:
true
,
};
mutations
[
types
.
RECEIVE_SET_LABELS_FAILURE
](
state
);
expect
(
state
.
labelsFetchInProgress
).
toBe
(
false
);
});
});
describe
(
`
${
types
.
REQUEST_CREATE_LABEL
}
`
,
()
=>
{
it
(
'
sets value of `state.labelCreateInProgress` to true
'
,
()
=>
{
const
state
=
{
labelCreateInProgress
:
false
,
};
mutations
[
types
.
REQUEST_CREATE_LABEL
](
state
);
expect
(
state
.
labelCreateInProgress
).
toBe
(
true
);
});
});
describe
(
`
${
types
.
RECEIVE_CREATE_LABEL_SUCCESS
}
`
,
()
=>
{
it
(
'
sets value of `state.labelCreateInProgress` to false
'
,
()
=>
{
const
state
=
{
labelCreateInProgress
:
false
,
};
mutations
[
types
.
RECEIVE_CREATE_LABEL_SUCCESS
](
state
);
expect
(
state
.
labelCreateInProgress
).
toBe
(
false
);
});
});
describe
(
`
${
types
.
RECEIVE_CREATE_LABEL_FAILURE
}
`
,
()
=>
{
it
(
'
sets value of `state.labelCreateInProgress` to false
'
,
()
=>
{
const
state
=
{
labelCreateInProgress
:
false
,
};
mutations
[
types
.
RECEIVE_CREATE_LABEL_FAILURE
](
state
);
expect
(
state
.
labelCreateInProgress
).
toBe
(
false
);
});
});
describe
(
`
${
types
.
UPDATE_SELECTED_LABELS
}
`
,
()
=>
{
const
labels
=
[{
id
:
1
},
{
id
:
2
},
{
id
:
3
},
{
id
:
4
}];
it
(
'
updates `state.labels` to include `touched` and `set` props based on provided `labels` param
'
,
()
=>
{
const
updatedLabelIds
=
[
2
,
4
];
const
state
=
{
labels
,
};
mutations
[
types
.
UPDATE_SELECTED_LABELS
](
state
,
{
labels
});
state
.
labels
.
forEach
(
label
=>
{
if
(
updatedLabelIds
.
includes
(
label
.
id
))
{
expect
(
label
.
touched
).
toBe
(
true
);
expect
(
label
.
set
).
toBe
(
true
);
}
});
});
});
});
spec/models/snippet_spec.rb
View file @
d3fc3be0
...
@@ -601,10 +601,23 @@ describe Snippet do
...
@@ -601,10 +601,23 @@ describe Snippet do
expect
(
snippet
.
create_repository
).
to
be_nil
expect
(
snippet
.
create_repository
).
to
be_nil
end
end
it
'does not track snippet repository'
do
context
'when snippet_repository exists'
do
expect
do
it
'does not create a new snippet repository'
do
snippet
.
create_repository
expect
do
end
.
not_to
change
(
SnippetRepository
,
:count
)
snippet
.
create_repository
end
.
not_to
change
(
SnippetRepository
,
:count
)
end
end
context
'when snippet_repository does not exist'
do
it
'creates a snippet_repository'
do
snippet
.
snippet_repository
.
destroy
snippet
.
reload
expect
do
snippet
.
create_repository
end
.
to
change
(
SnippetRepository
,
:count
).
by
(
1
)
end
end
end
end
end
end
end
...
...
spec/requests/api/graphql/mutations/snippets/update_spec.rb
View file @
d3fc3be0
...
@@ -91,7 +91,7 @@ describe 'Updating a Snippet' do
...
@@ -91,7 +91,7 @@ describe 'Updating a Snippet' do
describe
'PersonalSnippet'
do
describe
'PersonalSnippet'
do
it_behaves_like
'graphql update actions'
do
it_behaves_like
'graphql update actions'
do
let
_it_be
(
:snippet
)
do
let
(
:snippet
)
do
create
(
:personal_snippet
,
create
(
:personal_snippet
,
:private
,
:private
,
file_name:
original_file_name
,
file_name:
original_file_name
,
...
@@ -104,7 +104,7 @@ describe 'Updating a Snippet' do
...
@@ -104,7 +104,7 @@ describe 'Updating a Snippet' do
describe
'ProjectSnippet'
do
describe
'ProjectSnippet'
do
let_it_be
(
:project
)
{
create
(
:project
,
:private
)
}
let_it_be
(
:project
)
{
create
(
:project
,
:private
)
}
let
_it_be
(
:snippet
)
do
let
(
:snippet
)
do
create
(
:project_snippet
,
create
(
:project_snippet
,
:private
,
:private
,
project:
project
,
project:
project
,
...
...
spec/requests/api/project_snippets_spec.rb
View file @
d3fc3be0
...
@@ -278,13 +278,13 @@ describe API::ProjectSnippets do
...
@@ -278,13 +278,13 @@ describe API::ProjectSnippets do
describe
'PUT /projects/:project_id/snippets/:id/'
do
describe
'PUT /projects/:project_id/snippets/:id/'
do
let
(
:visibility_level
)
{
Snippet
::
PUBLIC
}
let
(
:visibility_level
)
{
Snippet
::
PUBLIC
}
let
(
:snippet
)
{
create
(
:project_snippet
,
author:
admin
,
visibility_level:
visibility_level
,
project:
project
)
}
let
(
:snippet
)
{
create
(
:project_snippet
,
:repository
,
author:
admin
,
visibility_level:
visibility_level
,
project:
project
)
}
it
'updates snippet'
do
it
'updates snippet'
do
new_content
=
'New content'
new_content
=
'New content'
new_description
=
'New description'
new_description
=
'New description'
put
api
(
"/projects/
#{
snippet
.
project
.
id
}
/snippets/
#{
snippet
.
id
}
/"
,
admin
),
params:
{
code:
new_content
,
description:
new_description
,
visibility:
'private'
}
update_snippet
(
params:
{
code:
new_content
,
description:
new_description
,
visibility:
'private'
})
expect
(
response
).
to
have_gitlab_http_status
(
:ok
)
expect
(
response
).
to
have_gitlab_http_status
(
:ok
)
snippet
.
reload
snippet
.
reload
...
@@ -297,7 +297,7 @@ describe API::ProjectSnippets do
...
@@ -297,7 +297,7 @@ describe API::ProjectSnippets do
new_content
=
'New content'
new_content
=
'New content'
new_description
=
'New description'
new_description
=
'New description'
put
api
(
"/projects/
#{
snippet
.
project
.
id
}
/snippets/
#{
snippet
.
id
}
/"
,
admin
),
params:
{
content:
new_content
,
description:
new_description
}
update_snippet
(
params:
{
content:
new_content
,
description:
new_description
})
expect
(
response
).
to
have_gitlab_http_status
(
:ok
)
expect
(
response
).
to
have_gitlab_http_status
(
:ok
)
snippet
.
reload
snippet
.
reload
...
@@ -306,21 +306,21 @@ describe API::ProjectSnippets do
...
@@ -306,21 +306,21 @@ describe API::ProjectSnippets do
end
end
it
'returns 400 when both code and content parameters specified'
do
it
'returns 400 when both code and content parameters specified'
do
put
api
(
"/projects/
#{
snippet
.
project
.
id
}
/snippets/1234"
,
admin
),
params:
{
code:
'some content'
,
content:
'other content'
}
update_snippet
(
params:
{
code:
'some content'
,
content:
'other content'
})
expect
(
response
).
to
have_gitlab_http_status
(
:bad_request
)
expect
(
response
).
to
have_gitlab_http_status
(
:bad_request
)
expect
(
json_response
[
'error'
]).
to
eq
(
'code, content are mutually exclusive'
)
expect
(
json_response
[
'error'
]).
to
eq
(
'code, content are mutually exclusive'
)
end
end
it
'returns 404 for invalid snippet id'
do
it
'returns 404 for invalid snippet id'
do
put
api
(
"/projects/
#{
snippet
.
project
.
id
}
/snippets/1234"
,
admin
),
params:
{
title:
'foo'
}
update_snippet
(
snippet_id:
'1234'
,
params:
{
title:
'foo'
})
expect
(
response
).
to
have_gitlab_http_status
(
:not_found
)
expect
(
response
).
to
have_gitlab_http_status
(
:not_found
)
expect
(
json_response
[
'message'
]).
to
eq
(
'404 Snippet Not Found'
)
expect
(
json_response
[
'message'
]).
to
eq
(
'404 Snippet Not Found'
)
end
end
it
'returns 400 for missing parameters'
do
it
'returns 400 for missing parameters'
do
put
api
(
"/projects/
#{
project
.
id
}
/snippets/1234"
,
admin
)
update_snippet
expect
(
response
).
to
have_gitlab_http_status
(
:bad_request
)
expect
(
response
).
to
have_gitlab_http_status
(
:bad_request
)
end
end
...
@@ -328,16 +328,16 @@ describe API::ProjectSnippets do
...
@@ -328,16 +328,16 @@ describe API::ProjectSnippets do
it
'returns 400 for empty code field'
do
it
'returns 400 for empty code field'
do
new_content
=
''
new_content
=
''
put
api
(
"/projects/
#{
snippet
.
project
.
id
}
/snippets/
#{
snippet
.
id
}
/"
,
admin
),
params:
{
code:
new_content
}
update_snippet
(
params:
{
code:
new_content
})
expect
(
response
).
to
have_gitlab_http_status
(
:bad_request
)
expect
(
response
).
to
have_gitlab_http_status
(
:bad_request
)
end
end
context
'when the snippet is spam'
do
it_behaves_like
'update with repository actions'
do
def
update_snippet
(
snippet_params
=
{})
let
(
:snippet_without_repo
)
{
create
(
:project_snippet
,
author:
admin
,
project:
project
,
visibility_level:
visibility_level
)
}
put
api
(
"/projects/
#{
snippet
.
project
.
id
}
/snippets/
#{
snippet
.
id
}
"
,
admin
),
params:
snippet_params
end
end
context
'when the snippet is spam'
do
before
do
before
do
allow_next_instance_of
(
Spam
::
AkismetService
)
do
|
instance
|
allow_next_instance_of
(
Spam
::
AkismetService
)
do
|
instance
|
allow
(
instance
).
to
receive
(
:spam?
).
and_return
(
true
)
allow
(
instance
).
to
receive
(
:spam?
).
and_return
(
true
)
...
@@ -348,7 +348,7 @@ describe API::ProjectSnippets do
...
@@ -348,7 +348,7 @@ describe API::ProjectSnippets do
let
(
:visibility_level
)
{
Snippet
::
PRIVATE
}
let
(
:visibility_level
)
{
Snippet
::
PRIVATE
}
it
'creates the snippet'
do
it
'creates the snippet'
do
expect
{
update_snippet
(
title:
'Foo'
)
}
expect
{
update_snippet
(
params:
{
title:
'Foo'
}
)
}
.
to
change
{
snippet
.
reload
.
title
}.
to
(
'Foo'
)
.
to
change
{
snippet
.
reload
.
title
}.
to
(
'Foo'
)
end
end
end
end
...
@@ -357,12 +357,12 @@ describe API::ProjectSnippets do
...
@@ -357,12 +357,12 @@ describe API::ProjectSnippets do
let
(
:visibility_level
)
{
Snippet
::
PUBLIC
}
let
(
:visibility_level
)
{
Snippet
::
PUBLIC
}
it
'rejects the snippet'
do
it
'rejects the snippet'
do
expect
{
update_snippet
(
title:
'Foo'
)
}
expect
{
update_snippet
(
params:
{
title:
'Foo'
}
)
}
.
not_to
change
{
snippet
.
reload
.
title
}
.
not_to
change
{
snippet
.
reload
.
title
}
end
end
it
'creates a spam log'
do
it
'creates a spam log'
do
expect
{
update_snippet
(
title:
'Foo'
)
}
expect
{
update_snippet
(
params:
{
title:
'Foo'
}
)
}
.
to
log_spam
(
title:
'Foo'
,
user_id:
admin
.
id
,
noteable_type:
'ProjectSnippet'
)
.
to
log_spam
(
title:
'Foo'
,
user_id:
admin
.
id
,
noteable_type:
'ProjectSnippet'
)
end
end
end
end
...
@@ -371,7 +371,7 @@ describe API::ProjectSnippets do
...
@@ -371,7 +371,7 @@ describe API::ProjectSnippets do
let
(
:visibility_level
)
{
Snippet
::
PRIVATE
}
let
(
:visibility_level
)
{
Snippet
::
PRIVATE
}
it
'rejects the snippet'
do
it
'rejects the snippet'
do
expect
{
update_snippet
(
title:
'Foo'
,
visibility:
'public'
)
}
expect
{
update_snippet
(
params:
{
title:
'Foo'
,
visibility:
'public'
}
)
}
.
not_to
change
{
snippet
.
reload
.
title
}
.
not_to
change
{
snippet
.
reload
.
title
}
expect
(
response
).
to
have_gitlab_http_status
(
:bad_request
)
expect
(
response
).
to
have_gitlab_http_status
(
:bad_request
)
...
@@ -379,7 +379,7 @@ describe API::ProjectSnippets do
...
@@ -379,7 +379,7 @@ describe API::ProjectSnippets do
end
end
it
'creates a spam log'
do
it
'creates a spam log'
do
expect
{
update_snippet
(
title:
'Foo'
,
visibility:
'public'
)
}
expect
{
update_snippet
(
params:
{
title:
'Foo'
,
visibility:
'public'
}
)
}
.
to
log_spam
(
title:
'Foo'
,
user_id:
admin
.
id
,
noteable_type:
'ProjectSnippet'
)
.
to
log_spam
(
title:
'Foo'
,
user_id:
admin
.
id
,
noteable_type:
'ProjectSnippet'
)
end
end
end
end
...
@@ -390,6 +390,10 @@ describe API::ProjectSnippets do
...
@@ -390,6 +390,10 @@ describe API::ProjectSnippets do
let
(
:request
)
{
put
api
(
"/projects/
#{
project_no_snippets
.
id
}
/snippets/123"
,
admin
),
params:
{
description:
'foo'
}
}
let
(
:request
)
{
put
api
(
"/projects/
#{
project_no_snippets
.
id
}
/snippets/123"
,
admin
),
params:
{
description:
'foo'
}
}
end
end
end
end
def
update_snippet
(
snippet_id:
snippet
.
id
,
params:
{})
put
api
(
"/projects/
#{
snippet
.
project
.
id
}
/snippets/
#{
snippet_id
}
"
,
admin
),
params:
params
end
end
end
describe
'DELETE /projects/:project_id/snippets/:id/'
do
describe
'DELETE /projects/:project_id/snippets/:id/'
do
...
...
spec/requests/api/snippets_spec.rb
View file @
d3fc3be0
...
@@ -301,7 +301,7 @@ describe API::Snippets do
...
@@ -301,7 +301,7 @@ describe API::Snippets do
let
(
:visibility_level
)
{
Snippet
::
PUBLIC
}
let
(
:visibility_level
)
{
Snippet
::
PUBLIC
}
let
(
:other_user
)
{
create
(
:user
)
}
let
(
:other_user
)
{
create
(
:user
)
}
let
(
:snippet
)
do
let
(
:snippet
)
do
create
(
:personal_snippet
,
author:
user
,
visibility_level:
visibility_level
)
create
(
:personal_snippet
,
:repository
,
author:
user
,
visibility_level:
visibility_level
)
end
end
shared_examples
'snippet updates'
do
shared_examples
'snippet updates'
do
...
@@ -309,7 +309,7 @@ describe API::Snippets do
...
@@ -309,7 +309,7 @@ describe API::Snippets do
new_content
=
'New content'
new_content
=
'New content'
new_description
=
'New description'
new_description
=
'New description'
put
api
(
"/snippets/
#{
snippet
.
id
}
"
,
user
),
params:
{
content:
new_content
,
description:
new_description
,
visibility:
'internal'
}
update_snippet
(
params:
{
content:
new_content
,
description:
new_description
,
visibility:
'internal'
})
expect
(
response
).
to
have_gitlab_http_status
(
:ok
)
expect
(
response
).
to
have_gitlab_http_status
(
:ok
)
snippet
.
reload
snippet
.
reload
...
@@ -332,30 +332,30 @@ describe API::Snippets do
...
@@ -332,30 +332,30 @@ describe API::Snippets do
it_behaves_like
'snippet updates'
it_behaves_like
'snippet updates'
it
'returns 404 for invalid snippet id'
do
it
'returns 404 for invalid snippet id'
do
put
api
(
"/snippets/1234"
,
user
),
params:
{
title:
'foo'
}
update_snippet
(
snippet_id:
'1234'
,
params:
{
title:
'Foo'
})
expect
(
response
).
to
have_gitlab_http_status
(
:not_found
)
expect
(
response
).
to
have_gitlab_http_status
(
:not_found
)
expect
(
json_response
[
'message'
]).
to
eq
(
'404 Snippet Not Found'
)
expect
(
json_response
[
'message'
]).
to
eq
(
'404 Snippet Not Found'
)
end
end
it
"returns 404 for another user's snippet"
do
it
"returns 404 for another user's snippet"
do
put
api
(
"/snippets/
#{
snippet
.
id
}
"
,
other_user
),
params:
{
title:
'fubar'
}
update_snippet
(
requester:
other_user
,
params:
{
title:
'foobar'
})
expect
(
response
).
to
have_gitlab_http_status
(
:not_found
)
expect
(
response
).
to
have_gitlab_http_status
(
:not_found
)
expect
(
json_response
[
'message'
]).
to
eq
(
'404 Snippet Not Found'
)
expect
(
json_response
[
'message'
]).
to
eq
(
'404 Snippet Not Found'
)
end
end
it
'returns 400 for missing parameters'
do
it
'returns 400 for missing parameters'
do
put
api
(
"/snippets/1234"
,
user
)
update_snippet
expect
(
response
).
to
have_gitlab_http_status
(
:bad_request
)
expect
(
response
).
to
have_gitlab_http_status
(
:bad_request
)
end
end
context
'when the snippet is spam'
do
it_behaves_like
'update with repository actions'
do
def
update_snippet
(
snippet_params
=
{})
let
(
:snippet_without_repo
)
{
create
(
:personal_snippet
,
author:
user
,
visibility_level:
visibility_level
)
}
put
api
(
"/snippets/
#{
snippet
.
id
}
"
,
user
),
params:
snippet_params
end
end
context
'when the snippet is spam'
do
before
do
before
do
allow_next_instance_of
(
Spam
::
AkismetService
)
do
|
instance
|
allow_next_instance_of
(
Spam
::
AkismetService
)
do
|
instance
|
allow
(
instance
).
to
receive
(
:spam?
).
and_return
(
true
)
allow
(
instance
).
to
receive
(
:spam?
).
and_return
(
true
)
...
@@ -366,7 +366,7 @@ describe API::Snippets do
...
@@ -366,7 +366,7 @@ describe API::Snippets do
let
(
:visibility_level
)
{
Snippet
::
PRIVATE
}
let
(
:visibility_level
)
{
Snippet
::
PRIVATE
}
it
'updates the snippet'
do
it
'updates the snippet'
do
expect
{
update_snippet
(
title:
'Foo'
)
}
expect
{
update_snippet
(
params:
{
title:
'Foo'
}
)
}
.
to
change
{
snippet
.
reload
.
title
}.
to
(
'Foo'
)
.
to
change
{
snippet
.
reload
.
title
}.
to
(
'Foo'
)
end
end
end
end
...
@@ -375,7 +375,7 @@ describe API::Snippets do
...
@@ -375,7 +375,7 @@ describe API::Snippets do
let
(
:visibility_level
)
{
Snippet
::
PUBLIC
}
let
(
:visibility_level
)
{
Snippet
::
PUBLIC
}
it
'rejects the shippet'
do
it
'rejects the shippet'
do
expect
{
update_snippet
(
title:
'Foo'
)
}
expect
{
update_snippet
(
params:
{
title:
'Foo'
}
)
}
.
not_to
change
{
snippet
.
reload
.
title
}
.
not_to
change
{
snippet
.
reload
.
title
}
expect
(
response
).
to
have_gitlab_http_status
(
:bad_request
)
expect
(
response
).
to
have_gitlab_http_status
(
:bad_request
)
...
@@ -383,7 +383,7 @@ describe API::Snippets do
...
@@ -383,7 +383,7 @@ describe API::Snippets do
end
end
it
'creates a spam log'
do
it
'creates a spam log'
do
expect
{
update_snippet
(
title:
'Foo'
)
}.
to
log_spam
(
title:
'Foo'
,
user_id:
user
.
id
,
noteable_type:
'PersonalSnippet'
)
expect
{
update_snippet
(
params:
{
title:
'Foo'
}
)
}.
to
log_spam
(
title:
'Foo'
,
user_id:
user
.
id
,
noteable_type:
'PersonalSnippet'
)
end
end
end
end
...
@@ -391,16 +391,20 @@ describe API::Snippets do
...
@@ -391,16 +391,20 @@ describe API::Snippets do
let
(
:visibility_level
)
{
Snippet
::
PRIVATE
}
let
(
:visibility_level
)
{
Snippet
::
PRIVATE
}
it
'rejects the snippet'
do
it
'rejects the snippet'
do
expect
{
update_snippet
(
title:
'Foo'
,
visibility:
'public'
)
}
expect
{
update_snippet
(
params:
{
title:
'Foo'
,
visibility:
'public'
}
)
}
.
not_to
change
{
snippet
.
reload
.
title
}
.
not_to
change
{
snippet
.
reload
.
title
}
end
end
it
'creates a spam log'
do
it
'creates a spam log'
do
expect
{
update_snippet
(
title:
'Foo'
,
visibility:
'public'
)
}
expect
{
update_snippet
(
params:
{
title:
'Foo'
,
visibility:
'public'
}
)
}
.
to
log_spam
(
title:
'Foo'
,
user_id:
user
.
id
,
noteable_type:
'PersonalSnippet'
)
.
to
log_spam
(
title:
'Foo'
,
user_id:
user
.
id
,
noteable_type:
'PersonalSnippet'
)
end
end
end
end
end
end
def
update_snippet
(
snippet_id:
snippet
.
id
,
params:
{},
requester:
user
)
put
api
(
"/snippets/
#{
snippet_id
}
"
,
requester
),
params:
params
end
end
end
describe
'DELETE /snippets/:id'
do
describe
'DELETE /snippets/:id'
do
...
...
spec/services/snippets/update_service_spec.rb
View file @
d3fc3be0
...
@@ -16,14 +16,9 @@ describe Snippets::UpdateService do
...
@@ -16,14 +16,9 @@ describe Snippets::UpdateService do
}
}
end
end
let
(
:updater
)
{
user
}
let
(
:updater
)
{
user
}
let
(
:service
)
{
Snippets
::
UpdateService
.
new
(
project
,
updater
,
options
)
}
subject
do
subject
{
service
.
execute
(
snippet
)
}
described_class
.
new
(
project
,
updater
,
options
).
execute
(
snippet
)
end
shared_examples
'a service that updates a snippet'
do
shared_examples
'a service that updates a snippet'
do
it
'updates a snippet with the provided attributes'
do
it
'updates a snippet with the provided attributes'
do
...
@@ -98,9 +93,109 @@ describe Snippets::UpdateService do
...
@@ -98,9 +93,109 @@ describe Snippets::UpdateService do
end
end
end
end
shared_examples
'creates repository and creates file'
do
it
'creates repository'
do
expect
(
snippet
.
repository
).
not_to
exist
subject
expect
(
snippet
.
repository
).
to
exist
end
it
'commits the files to the repository'
do
subject
expect
(
snippet
.
blobs
.
count
).
to
eq
1
blob
=
snippet
.
repository
.
blob_at
(
'master'
,
options
[
:file_name
])
expect
(
blob
.
data
).
to
eq
options
[
:content
]
end
context
'when the repository does not exist'
do
it
'does not try to commit file'
do
allow
(
snippet
).
to
receive
(
:repository_exists?
).
and_return
(
false
)
expect
(
service
).
not_to
receive
(
:create_commit
)
subject
end
end
context
'when feature flag is disabled'
do
before
do
stub_feature_flags
(
version_snippets:
false
)
end
it
'does not create repository'
do
subject
expect
(
snippet
.
repository
).
not_to
exist
end
it
'does not try to commit file'
do
expect
(
service
).
not_to
receive
(
:create_commit
)
subject
end
end
it
'returns error when the commit action fails'
do
allow_next_instance_of
(
SnippetRepository
)
do
|
instance
|
allow
(
instance
).
to
receive
(
:multi_files_action
).
and_raise
(
SnippetRepository
::
CommitError
)
end
response
=
subject
expect
(
response
).
to
be_error
expect
(
response
.
payload
[
:snippet
].
errors
.
full_messages
).
to
eq
[
'Error updating the snippet'
]
end
end
shared_examples
'updates repository content'
do
it
'commit the files to the repository'
do
blob
=
snippet
.
blobs
.
first
options
[
:file_name
]
=
blob
.
path
+
'_new'
expect
(
blob
.
data
).
not_to
eq
(
options
[
:content
])
subject
blob
=
snippet
.
blobs
.
first
expect
(
blob
.
path
).
to
eq
(
options
[
:file_name
])
expect
(
blob
.
data
).
to
eq
(
options
[
:content
])
end
it
'returns error when the commit action fails'
do
allow
(
snippet
.
snippet_repository
).
to
receive
(
:multi_files_action
).
and_raise
(
SnippetRepository
::
CommitError
)
response
=
subject
expect
(
response
).
to
be_error
expect
(
response
.
payload
[
:snippet
].
errors
.
full_messages
).
to
eq
[
'Error updating the snippet'
]
end
it
'returns error if snippet does not have a snippet_repository'
do
allow
(
snippet
).
to
receive
(
:snippet_repository
).
and_return
(
nil
)
expect
(
subject
).
to
be_error
end
context
'when the repository does not exist'
do
it
'does not try to commit file'
do
allow
(
snippet
).
to
receive
(
:repository_exists?
).
and_return
(
false
)
expect
(
service
).
not_to
receive
(
:create_commit
)
subject
end
end
end
context
'when Project Snippet'
do
context
'when Project Snippet'
do
let_it_be
(
:project
)
{
create
(
:project
)
}
let_it_be
(
:project
)
{
create
(
:project
)
}
let!
(
:snippet
)
{
create
(
:project_snippet
,
author:
user
,
project:
project
)
}
let!
(
:snippet
)
{
create
(
:project_snippet
,
:repository
,
author:
user
,
project:
project
)
}
before
do
before
do
project
.
add_developer
(
user
)
project
.
add_developer
(
user
)
...
@@ -109,15 +204,29 @@ describe Snippets::UpdateService do
...
@@ -109,15 +204,29 @@ describe Snippets::UpdateService do
it_behaves_like
'a service that updates a snippet'
it_behaves_like
'a service that updates a snippet'
it_behaves_like
'public visibility level restrictions apply'
it_behaves_like
'public visibility level restrictions apply'
it_behaves_like
'snippet update data is tracked'
it_behaves_like
'snippet update data is tracked'
it_behaves_like
'updates repository content'
context
'when snippet does not have a repository'
do
let!
(
:snippet
)
{
create
(
:project_snippet
,
author:
user
,
project:
project
)
}
it_behaves_like
'creates repository and creates file'
end
end
end
context
'when PersonalSnippet'
do
context
'when PersonalSnippet'
do
let
(
:project
)
{
nil
}
let
(
:project
)
{
nil
}
let!
(
:snippet
)
{
create
(
:personal_snippet
,
author:
user
)
}
let!
(
:snippet
)
{
create
(
:personal_snippet
,
:repository
,
author:
user
)
}
it_behaves_like
'a service that updates a snippet'
it_behaves_like
'a service that updates a snippet'
it_behaves_like
'public visibility level restrictions apply'
it_behaves_like
'public visibility level restrictions apply'
it_behaves_like
'snippet update data is tracked'
it_behaves_like
'snippet update data is tracked'
it_behaves_like
'updates repository content'
context
'when snippet does not have a repository'
do
let!
(
:snippet
)
{
create
(
:personal_snippet
,
author:
user
,
project:
project
)
}
it_behaves_like
'creates repository and creates file'
end
end
end
end
end
end
end
spec/support/shared_examples/requests/snippet_shared_examples.rb
0 → 100644
View file @
d3fc3be0
# frozen_string_literal: true
RSpec
.
shared_examples
'update with repository actions'
do
context
'when the repository exists'
do
it
'commits the changes to the repository'
do
existing_blob
=
snippet
.
blobs
.
first
new_file_name
=
existing_blob
.
path
+
'_new'
new_content
=
'New content'
update_snippet
(
params:
{
content:
new_content
,
file_name:
new_file_name
})
aggregate_failures
do
expect
(
response
).
to
have_gitlab_http_status
(
:ok
)
expect
(
snippet
.
repository
.
blob_at
(
'master'
,
existing_blob
.
path
)).
to
be_nil
blob
=
snippet
.
repository
.
blob_at
(
'master'
,
new_file_name
)
expect
(
blob
).
not_to
be_nil
expect
(
blob
.
data
).
to
eq
(
new_content
)
end
end
end
context
'when the repository does not exist'
do
let
(
:snippet
)
{
snippet_without_repo
}
it
'creates the repository'
do
update_snippet
(
snippet_id:
snippet
.
id
,
params:
{
title:
'foo'
})
expect
(
snippet
.
repository
).
to
exist
end
it
'commits the file to the repository'
do
content
=
'New Content'
file_name
=
'file_name.rb'
update_snippet
(
snippet_id:
snippet
.
id
,
params:
{
content:
content
,
file_name:
file_name
})
blob
=
snippet
.
repository
.
blob_at
(
'master'
,
file_name
)
expect
(
blob
).
not_to
be_nil
expect
(
blob
.
data
).
to
eq
content
end
end
end
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