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 {
<template
v-else
>
<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
>
</div>
</template>
app/assets/javascripts/blob/viewer/index.js
View file @
d3fc3be0
...
...
@@ -5,10 +5,43 @@ import { handleLocationHash } from '../../lib/utils/common_utils';
import
axios
from
'
../../lib/utils/axios_utils
'
;
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
{
constructor
()
{
const
viewer
=
document
.
querySelector
(
'
.blob-viewer[data-type="rich"]
'
);
const
type
=
viewer
?.
dataset
?.
richType
;
BlobViewer
.
initAuxiliaryViewer
();
BlobViewer
.
initRichViewer
();
handleBlobRichViewer
(
viewer
,
type
);
this
.
initMainViewers
();
}
...
...
@@ -20,42 +53,6 @@ export default class BlobViewer {
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
()
{
this
.
$fileHolder
=
$
(
'
.file-holder
'
);
if
(
!
this
.
$fileHolder
.
length
)
return
;
...
...
app/assets/javascripts/vue_shared/components/blob_viewers/mixins.js
View file @
d3fc3be0
...
...
@@ -4,5 +4,9 @@ export default {
type
:
String
,
required
:
true
,
},
type
:
{
type
:
String
,
required
:
true
,
},
},
};
app/assets/javascripts/vue_shared/components/blob_viewers/rich_viewer.vue
View file @
d3fc3be0
<
script
>
import
ViewerMixin
from
'
./mixins
'
;
import
{
handleBlobRichViewer
}
from
'
~/blob/viewer
'
;
export
default
{
mixins
:
[
ViewerMixin
],
mounted
()
{
handleBlobRichViewer
(
this
.
$refs
.
content
,
this
.
type
);
},
};
</
script
>
<
template
>
<div
v-html=
"content"
></div>
<div
ref=
"content"
v-html=
"content"
></div>
</
template
>
app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_regular_label.vue
View file @
d3fc3be0
...
...
@@ -27,7 +27,12 @@ export default {
<span
:style=
"labelStyle"
class=
"badge color-label"
>
{{
label
.
title
}}
</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
}}
</gl-tooltip>
</a>
...
...
app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_scoped_label.vue
View file @
d3fc3be0
...
...
@@ -33,7 +33,12 @@ export default {
<span
:ref=
"`labelTitleRef`"
:style=
"labelStyle"
class=
"badge color-label label"
>
{{
label
.
title
}}
</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
><br
/>
{{
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 {
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
end
def
create_repository
return
if
repository_exists?
return
if
repository_exists?
&&
snippet_repository
repository
.
create_if_not_exists
track_snippet_repository
if
repository_exists?
track_snippet_repository
end
def
track_snippet_repository
...
...
app/services/snippets/update_service.rb
View file @
d3fc3be0
...
...
@@ -4,6 +4,9 @@ module Snippets
class
UpdateService
<
Snippets
::
BaseService
include
SpamCheckMethods
UpdateError
=
Class
.
new
(
StandardError
)
CreateRepositoryError
=
Class
.
new
(
StandardError
)
def
execute
(
snippet
)
# check that user is allowed to set specified visibility_level
new_visibility
=
visibility_level
...
...
@@ -20,11 +23,7 @@ module Snippets
snippet
.
assign_attributes
(
params
)
spam_check
(
snippet
,
current_user
)
snippet_saved
=
snippet
.
with_transaction_returning_status
do
snippet
.
save
end
if
snippet_saved
if
save_and_commit
(
snippet
)
Gitlab
::
UsageDataCounters
::
SnippetCounter
.
count
(
:update
)
ServiceResponse
.
success
(
payload:
{
snippet:
snippet
}
)
...
...
@@ -32,5 +31,54 @@ module Snippets
snippet_error_response
(
snippet
,
400
)
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
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
create_table
"design_management_versions"
,
force: :cascade
do
|
t
|
t
.
binary
"sha"
,
null:
false
t
.
bigint
"issue_id"
t
.
datetime_with_timezone
"created_at"
t
.
datetime_with_timezone
"created_at"
,
null:
false
t
.
integer
"author_id"
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"
...
...
doc/development/dangerbot.md
View file @
d3fc3be0
...
...
@@ -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
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.
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
...
...
locale/gitlab.pot
View file @
d3fc3be0
...
...
@@ -7778,6 +7778,9 @@ msgstr ""
msgid "Error creating epic"
msgstr ""
msgid "Error creating label."
msgstr ""
msgid "Error deleting %{issuableType}"
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)."
msgstr ""
msgid "Use custom color #FF0000"
msgstr ""
msgid "Use group milestones to manage issues from multiple projects in the same milestone."
msgstr ""
...
...
spec/frontend/vue_shared/components/blob_viewers/rich_viewer_spec.js
View file @
d3fc3be0
import
{
shallowMount
}
from
'
@vue/test-utils
'
;
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
'
,
()
=>
{
let
wrapper
;
const
content
=
'
<h1 id="markdown">Foo Bar</h1>
'
;
const
defaultType
=
'
markdown
'
;
function
createComponent
()
{
function
createComponent
(
type
=
defaultType
)
{
wrapper
=
shallowMount
(
RichViewer
,
{
propsData
:
{
content
,
type
,
},
});
}
...
...
@@ -24,4 +29,8 @@ describe('Blob Rich Viewer component', () => {
it
(
'
renders the passed content without transformations
'
,
()
=>
{
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', () => {
wrapper
=
shallowMount
(
SimpleViewer
,
{
propsData
:
{
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
expect
(
snippet
.
create_repository
).
to
be_nil
end
it
'does not track snippet repository'
do
expect
do
snippet
.
create_repository
end
.
not_to
change
(
SnippetRepository
,
:count
)
context
'when snippet_repository exists'
do
it
'does not create a new snippet repository'
do
expect
do
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
...
...
spec/requests/api/graphql/mutations/snippets/update_spec.rb
View file @
d3fc3be0
...
...
@@ -91,7 +91,7 @@ describe 'Updating a Snippet' do
describe
'PersonalSnippet'
do
it_behaves_like
'graphql update actions'
do
let
_it_be
(
:snippet
)
do
let
(
:snippet
)
do
create
(
:personal_snippet
,
:private
,
file_name:
original_file_name
,
...
...
@@ -104,7 +104,7 @@ describe 'Updating a Snippet' do
describe
'ProjectSnippet'
do
let_it_be
(
:project
)
{
create
(
:project
,
:private
)
}
let
_it_be
(
:snippet
)
do
let
(
:snippet
)
do
create
(
:project_snippet
,
:private
,
project:
project
,
...
...
spec/requests/api/project_snippets_spec.rb
View file @
d3fc3be0
...
...
@@ -278,13 +278,13 @@ describe API::ProjectSnippets do
describe
'PUT /projects/:project_id/snippets/:id/'
do
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
new_content
=
'New content'
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
)
snippet
.
reload
...
...
@@ -297,7 +297,7 @@ describe API::ProjectSnippets do
new_content
=
'New content'
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
)
snippet
.
reload
...
...
@@ -306,21 +306,21 @@ describe API::ProjectSnippets do
end
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
(
json_response
[
'error'
]).
to
eq
(
'code, content are mutually exclusive'
)
end
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
(
json_response
[
'message'
]).
to
eq
(
'404 Snippet Not Found'
)
end
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
)
end
...
...
@@ -328,16 +328,16 @@ describe API::ProjectSnippets do
it
'returns 400 for empty code field'
do
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
)
end
context
'when the snippet is spam'
do
def
update_snippet
(
snippet_params
=
{})
put
api
(
"/projects/
#{
snippet
.
project
.
id
}
/snippets/
#{
snippet
.
id
}
"
,
admin
),
params:
snippet_params
end
it_behaves_like
'update with repository actions'
do
let
(
:snippet_without_repo
)
{
create
(
:project_snippet
,
author:
admin
,
project:
project
,
visibility_level:
visibility_level
)
}
end
context
'when the snippet is spam'
do
before
do
allow_next_instance_of
(
Spam
::
AkismetService
)
do
|
instance
|
allow
(
instance
).
to
receive
(
:spam?
).
and_return
(
true
)
...
...
@@ -348,7 +348,7 @@ describe API::ProjectSnippets do
let
(
:visibility_level
)
{
Snippet
::
PRIVATE
}
it
'creates the snippet'
do
expect
{
update_snippet
(
title:
'Foo'
)
}
expect
{
update_snippet
(
params:
{
title:
'Foo'
}
)
}
.
to
change
{
snippet
.
reload
.
title
}.
to
(
'Foo'
)
end
end
...
...
@@ -357,12 +357,12 @@ describe API::ProjectSnippets do
let
(
:visibility_level
)
{
Snippet
::
PUBLIC
}
it
'rejects the snippet'
do
expect
{
update_snippet
(
title:
'Foo'
)
}
expect
{
update_snippet
(
params:
{
title:
'Foo'
}
)
}
.
not_to
change
{
snippet
.
reload
.
title
}
end
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'
)
end
end
...
...
@@ -371,7 +371,7 @@ describe API::ProjectSnippets do
let
(
:visibility_level
)
{
Snippet
::
PRIVATE
}
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
}
expect
(
response
).
to
have_gitlab_http_status
(
:bad_request
)
...
...
@@ -379,7 +379,7 @@ describe API::ProjectSnippets do
end
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'
)
end
end
...
...
@@ -390,6 +390,10 @@ describe API::ProjectSnippets do
let
(
:request
)
{
put
api
(
"/projects/
#{
project_no_snippets
.
id
}
/snippets/123"
,
admin
),
params:
{
description:
'foo'
}
}
end
end
def
update_snippet
(
snippet_id:
snippet
.
id
,
params:
{})
put
api
(
"/projects/
#{
snippet
.
project
.
id
}
/snippets/
#{
snippet_id
}
"
,
admin
),
params:
params
end
end
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
let
(
:visibility_level
)
{
Snippet
::
PUBLIC
}
let
(
:other_user
)
{
create
(
:user
)
}
let
(
:snippet
)
do
create
(
:personal_snippet
,
author:
user
,
visibility_level:
visibility_level
)
create
(
:personal_snippet
,
:repository
,
author:
user
,
visibility_level:
visibility_level
)
end
shared_examples
'snippet updates'
do
...
...
@@ -309,7 +309,7 @@ describe API::Snippets do
new_content
=
'New content'
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
)
snippet
.
reload
...
...
@@ -332,30 +332,30 @@ describe API::Snippets do
it_behaves_like
'snippet updates'
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
(
json_response
[
'message'
]).
to
eq
(
'404 Snippet Not Found'
)
end
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
(
json_response
[
'message'
]).
to
eq
(
'404 Snippet Not Found'
)
end
it
'returns 400 for missing parameters'
do
put
api
(
"/snippets/1234"
,
user
)
update_snippet
expect
(
response
).
to
have_gitlab_http_status
(
:bad_request
)
end
context
'when the snippet is spam'
do
def
update_snippet
(
snippet_params
=
{})
put
api
(
"/snippets/
#{
snippet
.
id
}
"
,
user
),
params:
snippet_params
end
it_behaves_like
'update with repository actions'
do
let
(
:snippet_without_repo
)
{
create
(
:personal_snippet
,
author:
user
,
visibility_level:
visibility_level
)
}
end
context
'when the snippet is spam'
do
before
do
allow_next_instance_of
(
Spam
::
AkismetService
)
do
|
instance
|
allow
(
instance
).
to
receive
(
:spam?
).
and_return
(
true
)
...
...
@@ -366,7 +366,7 @@ describe API::Snippets do
let
(
:visibility_level
)
{
Snippet
::
PRIVATE
}
it
'updates the snippet'
do
expect
{
update_snippet
(
title:
'Foo'
)
}
expect
{
update_snippet
(
params:
{
title:
'Foo'
}
)
}
.
to
change
{
snippet
.
reload
.
title
}.
to
(
'Foo'
)
end
end
...
...
@@ -375,7 +375,7 @@ describe API::Snippets do
let
(
:visibility_level
)
{
Snippet
::
PUBLIC
}
it
'rejects the shippet'
do
expect
{
update_snippet
(
title:
'Foo'
)
}
expect
{
update_snippet
(
params:
{
title:
'Foo'
}
)
}
.
not_to
change
{
snippet
.
reload
.
title
}
expect
(
response
).
to
have_gitlab_http_status
(
:bad_request
)
...
...
@@ -383,7 +383,7 @@ describe API::Snippets do
end
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
...
...
@@ -391,16 +391,20 @@ describe API::Snippets do
let
(
:visibility_level
)
{
Snippet
::
PRIVATE
}
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
}
end
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'
)
end
end
end
def
update_snippet
(
snippet_id:
snippet
.
id
,
params:
{},
requester:
user
)
put
api
(
"/snippets/
#{
snippet_id
}
"
,
requester
),
params:
params
end
end
describe
'DELETE /snippets/:id'
do
...
...
spec/services/snippets/update_service_spec.rb
View file @
d3fc3be0
...
...
@@ -16,14 +16,9 @@ describe Snippets::UpdateService do
}
end
let
(
:updater
)
{
user
}
let
(
:service
)
{
Snippets
::
UpdateService
.
new
(
project
,
updater
,
options
)
}
subject
do
described_class
.
new
(
project
,
updater
,
options
).
execute
(
snippet
)
end
subject
{
service
.
execute
(
snippet
)
}
shared_examples
'a service that updates a snippet'
do
it
'updates a snippet with the provided attributes'
do
...
...
@@ -98,9 +93,109 @@ describe Snippets::UpdateService do
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
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
project
.
add_developer
(
user
)
...
...
@@ -109,15 +204,29 @@ describe Snippets::UpdateService do
it_behaves_like
'a service that updates a snippet'
it_behaves_like
'public visibility level restrictions apply'
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
context
'when PersonalSnippet'
do
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
'public visibility level restrictions apply'
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
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