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
0
Merge Requests
0
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
Boxiang Sun
gitlab-ce
Commits
31dd86b6
Commit
31dd86b6
authored
Apr 08, 2018
by
Francisco Javier López
Committed by
Douwe Maan
Apr 08, 2018
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Projects and groups badges settings UI
parent
dd552d06
Changes
46
Expand all
Show whitespace changes
Inline
Side-by-side
Showing
46 changed files
with
3021 additions
and
13 deletions
+3021
-13
app/assets/javascripts/badges/components/badge.vue
app/assets/javascripts/badges/components/badge.vue
+121
-0
app/assets/javascripts/badges/components/badge_form.vue
app/assets/javascripts/badges/components/badge_form.vue
+219
-0
app/assets/javascripts/badges/components/badge_list.vue
app/assets/javascripts/badges/components/badge_list.vue
+57
-0
app/assets/javascripts/badges/components/badge_list_row.vue
app/assets/javascripts/badges/components/badge_list_row.vue
+89
-0
app/assets/javascripts/badges/components/badge_settings.vue
app/assets/javascripts/badges/components/badge_settings.vue
+70
-0
app/assets/javascripts/badges/constants.js
app/assets/javascripts/badges/constants.js
+2
-0
app/assets/javascripts/badges/empty_badge.js
app/assets/javascripts/badges/empty_badge.js
+7
-0
app/assets/javascripts/badges/store/actions.js
app/assets/javascripts/badges/store/actions.js
+167
-0
app/assets/javascripts/badges/store/index.js
app/assets/javascripts/badges/store/index.js
+13
-0
app/assets/javascripts/badges/store/mutation_types.js
app/assets/javascripts/badges/store/mutation_types.js
+21
-0
app/assets/javascripts/badges/store/mutations.js
app/assets/javascripts/badges/store/mutations.js
+158
-0
app/assets/javascripts/badges/store/state.js
app/assets/javascripts/badges/store/state.js
+13
-0
app/assets/javascripts/pages/groups/settings/badges/index.js
app/assets/javascripts/pages/groups/settings/badges/index.js
+10
-0
app/assets/javascripts/pages/projects/settings/badges/index/index.js
...javascripts/pages/projects/settings/badges/index/index.js
+10
-0
app/assets/javascripts/pages/shared/mount_badge_settings.js
app/assets/javascripts/pages/shared/mount_badge_settings.js
+24
-0
app/assets/stylesheets/framework/responsive_tables.scss
app/assets/stylesheets/framework/responsive_tables.scss
+1
-1
app/assets/stylesheets/pages/projects.scss
app/assets/stylesheets/pages/projects.scss
+8
-0
app/controllers/groups/settings/badges_controller.rb
app/controllers/groups/settings/badges_controller.rb
+13
-0
app/controllers/projects/settings/badges_controller.rb
app/controllers/projects/settings/badges_controller.rb
+13
-0
app/helpers/groups_helper.rb
app/helpers/groups_helper.rb
+1
-1
app/views/groups/settings/badges/index.html.haml
app/views/groups/settings/badges/index.html.haml
+4
-0
app/views/layouts/nav/sidebar/_group.html.haml
app/views/layouts/nav/sidebar/_group.html.haml
+7
-1
app/views/layouts/nav/sidebar/_project.html.haml
app/views/layouts/nav/sidebar/_project.html.haml
+7
-2
app/views/projects/_home_panel.html.haml
app/views/projects/_home_panel.html.haml
+7
-4
app/views/projects/settings/badges/index.html.haml
app/views/projects/settings/badges/index.html.haml
+4
-0
app/views/shared/badges/_badge_settings.html.haml
app/views/shared/badges/_badge_settings.html.haml
+4
-0
changelogs/unreleased/winh-41174-projects-groups-badges-ui.yml
...elogs/unreleased/winh-41174-projects-groups-badges-ui.yml
+5
-0
config/karma.config.js
config/karma.config.js
+1
-1
config/routes/group.rb
config/routes/group.rb
+1
-0
config/routes/project.rb
config/routes/project.rb
+1
-0
lib/api/badges.rb
lib/api/badges.rb
+1
-0
spec/features/groups/settings/group_badges_spec.rb
spec/features/groups/settings/group_badges_spec.rb
+124
-0
spec/features/projects/settings/project_badges_spec.rb
spec/features/projects/settings/project_badges_spec.rb
+125
-0
spec/javascripts/badges/components/badge_form_spec.js
spec/javascripts/badges/components/badge_form_spec.js
+171
-0
spec/javascripts/badges/components/badge_list_row_spec.js
spec/javascripts/badges/components/badge_list_row_spec.js
+97
-0
spec/javascripts/badges/components/badge_list_spec.js
spec/javascripts/badges/components/badge_list_spec.js
+88
-0
spec/javascripts/badges/components/badge_settings_spec.js
spec/javascripts/badges/components/badge_settings_spec.js
+109
-0
spec/javascripts/badges/components/badge_spec.js
spec/javascripts/badges/components/badge_spec.js
+147
-0
spec/javascripts/badges/dummy_badge.js
spec/javascripts/badges/dummy_badge.js
+23
-0
spec/javascripts/badges/store/actions_spec.js
spec/javascripts/badges/store/actions_spec.js
+607
-0
spec/javascripts/badges/store/mutations_spec.js
spec/javascripts/badges/store/mutations_spec.js
+418
-0
spec/javascripts/fixtures/one_white_pixel.png
spec/javascripts/fixtures/one_white_pixel.png
+0
-0
spec/javascripts/helpers/vue_mount_component_helper.js
spec/javascripts/helpers/vue_mount_component_helper.js
+6
-0
spec/javascripts/matchers.js
spec/javascripts/matchers.js
+35
-0
spec/javascripts/test_bundle.js
spec/javascripts/test_bundle.js
+8
-3
spec/javascripts/test_constants.js
spec/javascripts/test_constants.js
+4
-0
No files found.
app/assets/javascripts/badges/components/badge.vue
0 → 100644
View file @
31dd86b6
<
script
>
import
Icon
from
'
~/vue_shared/components/icon.vue
'
;
import
LoadingIcon
from
'
~/vue_shared/components/loading_icon.vue
'
;
import
Tooltip
from
'
~/vue_shared/directives/tooltip
'
;
export
default
{
name
:
'
Badge
'
,
components
:
{
Icon
,
LoadingIcon
,
Tooltip
,
},
directives
:
{
Tooltip
,
},
props
:
{
imageUrl
:
{
type
:
String
,
required
:
true
,
},
linkUrl
:
{
type
:
String
,
required
:
true
,
},
},
data
()
{
return
{
hasError
:
false
,
isLoading
:
true
,
numRetries
:
0
,
};
},
computed
:
{
imageUrlWithRetries
()
{
if
(
this
.
numRetries
===
0
)
{
return
this
.
imageUrl
;
}
return
`
${
this
.
imageUrl
}
#retries=
${
this
.
numRetries
}
`
;
},
},
watch
:
{
imageUrl
()
{
this
.
hasError
=
false
;
this
.
isLoading
=
true
;
this
.
numRetries
=
0
;
},
},
methods
:
{
onError
()
{
this
.
isLoading
=
false
;
this
.
hasError
=
true
;
},
onLoad
()
{
this
.
isLoading
=
false
;
},
reloadImage
()
{
this
.
hasError
=
false
;
this
.
isLoading
=
true
;
this
.
numRetries
+=
1
;
},
},
};
</
script
>
<
template
>
<div>
<a
v-show=
"!isLoading && !hasError"
:href=
"linkUrl"
target=
"_blank"
rel=
"noopener noreferrer"
>
<img
class=
"project-badge"
:src=
"imageUrlWithRetries"
@
load=
"onLoad"
@
error=
"onError"
aria-hidden=
"true"
/>
</a>
<loading-icon
v-show=
"isLoading"
:inline=
"true"
/>
<div
v-show=
"hasError"
class=
"btn-group"
>
<div
class=
"btn btn-default btn-xs disabled"
>
<icon
class=
"prepend-left-8 append-right-8"
name=
"doc_image"
:size=
"16"
aria-hidden=
"true"
/>
</div>
<div
class=
"btn btn-default btn-xs disabled"
>
<span
class=
"prepend-left-8 append-right-8"
>
{{
s__
(
'
Badges|No badge image
'
)
}}
</span>
</div>
</div>
<button
v-show=
"hasError"
class=
"btn btn-transparent btn-xs text-primary"
type=
"button"
v-tooltip
:title=
"s__('Badges|Reload badge image')"
@
click=
"reloadImage"
>
<icon
name=
"retry"
:size=
"16"
/>
</button>
</div>
</
template
>
app/assets/javascripts/badges/components/badge_form.vue
0 → 100644
View file @
31dd86b6
<
script
>
import
_
from
'
underscore
'
;
import
{
mapActions
,
mapState
}
from
'
vuex
'
;
import
createFlash
from
'
~/flash
'
;
import
{
s__
,
sprintf
}
from
'
~/locale
'
;
import
LoadingButton
from
'
~/vue_shared/components/loading_button.vue
'
;
import
LoadingIcon
from
'
~/vue_shared/components/loading_icon.vue
'
;
import
createEmptyBadge
from
'
../empty_badge
'
;
import
Badge
from
'
./badge.vue
'
;
const
badgePreviewDelayInMilliseconds
=
1500
;
export
default
{
name
:
'
BadgeForm
'
,
components
:
{
Badge
,
LoadingButton
,
LoadingIcon
,
},
props
:
{
isEditing
:
{
type
:
Boolean
,
required
:
true
,
},
},
computed
:
{
...
mapState
([
'
badgeInAddForm
'
,
'
badgeInEditForm
'
,
'
docsUrl
'
,
'
isRendering
'
,
'
isSaving
'
,
'
renderedBadge
'
,
]),
badge
()
{
if
(
this
.
isEditing
)
{
return
this
.
badgeInEditForm
;
}
return
this
.
badgeInAddForm
;
},
canSubmit
()
{
return
(
this
.
badge
!==
null
&&
this
.
badge
.
imageUrl
&&
this
.
badge
.
imageUrl
.
trim
()
!==
''
&&
this
.
badge
.
linkUrl
&&
this
.
badge
.
linkUrl
.
trim
()
!==
''
&&
!
this
.
isSaving
);
},
helpText
()
{
const
placeholders
=
[
'
project_path
'
,
'
project_id
'
,
'
default_branch
'
,
'
commit_sha
'
]
.
map
(
placeholder
=>
`<code>%{
${
placeholder
}
}</code>`
)
.
join
(
'
,
'
);
return
sprintf
(
s__
(
'
Badges|The %{docsLinkStart}variables%{docsLinkEnd} GitLab supports: %{placeholders}
'
),
{
docsLinkEnd
:
'
</a>
'
,
docsLinkStart
:
`<a href="
${
_
.
escape
(
this
.
docsUrl
)}
">`
,
placeholders
,
},
false
,
);
},
renderedImageUrl
()
{
return
this
.
renderedBadge
?
this
.
renderedBadge
.
renderedImageUrl
:
''
;
},
renderedLinkUrl
()
{
return
this
.
renderedBadge
?
this
.
renderedBadge
.
renderedLinkUrl
:
''
;
},
imageUrl
:
{
get
()
{
return
this
.
badge
?
this
.
badge
.
imageUrl
:
''
;
},
set
(
imageUrl
)
{
const
badge
=
this
.
badge
||
createEmptyBadge
();
this
.
updateBadgeInForm
({
...
badge
,
imageUrl
,
});
},
},
linkUrl
:
{
get
()
{
return
this
.
badge
?
this
.
badge
.
linkUrl
:
''
;
},
set
(
linkUrl
)
{
const
badge
=
this
.
badge
||
createEmptyBadge
();
this
.
updateBadgeInForm
({
...
badge
,
linkUrl
,
});
},
},
submitButtonLabel
()
{
if
(
this
.
isEditing
)
{
return
s__
(
'
Badges|Save changes
'
);
}
return
s__
(
'
Badges|Add badge
'
);
},
},
methods
:
{
...
mapActions
([
'
addBadge
'
,
'
renderBadge
'
,
'
saveBadge
'
,
'
stopEditing
'
,
'
updateBadgeInForm
'
]),
debouncedPreview
:
_
.
debounce
(
function
preview
()
{
this
.
renderBadge
();
},
badgePreviewDelayInMilliseconds
),
onCancel
()
{
this
.
stopEditing
();
},
onSubmit
()
{
if
(
!
this
.
canSubmit
)
{
return
Promise
.
resolve
();
}
if
(
this
.
isEditing
)
{
return
this
.
saveBadge
()
.
then
(()
=>
{
createFlash
(
s__
(
'
Badges|The badge was saved.
'
),
'
notice
'
);
})
.
catch
(
error
=>
{
createFlash
(
s__
(
'
Badges|Saving the badge failed, please check the entered URLs and try again.
'
),
);
throw
error
;
});
}
return
this
.
addBadge
()
.
then
(()
=>
{
createFlash
(
s__
(
'
Badges|A new badge was added.
'
),
'
notice
'
);
})
.
catch
(
error
=>
{
createFlash
(
s__
(
'
Badges|Adding the badge failed, please check the entered URLs and try again.
'
),
);
throw
error
;
});
},
},
badgeImageUrlPlaceholder
:
'
https://example.gitlab.com/%{project_path}/badges/%{default_branch}/<badge>.svg
'
,
badgeLinkUrlPlaceholder
:
'
https://example.gitlab.com/%{project_path}
'
,
};
</
script
>
<
template
>
<form
class=
"prepend-top-default append-bottom-default"
@
submit.prevent.stop=
"onSubmit"
>
<div
class=
"form-group"
>
<label
for=
"badge-link-url"
>
{{
s__
(
'
Badges|Link
'
)
}}
</label>
<input
id=
"badge-link-url"
type=
"text"
class=
"form-control"
v-model=
"linkUrl"
:placeholder=
"$options.badgeLinkUrlPlaceholder"
@
input=
"debouncedPreview"
/>
<span
class=
"help-block"
v-html=
"helpText"
></span>
</div>
<div
class=
"form-group"
>
<label
for=
"badge-image-url"
>
{{
s__
(
'
Badges|Badge image URL
'
)
}}
</label>
<input
id=
"badge-image-url"
type=
"text"
class=
"form-control"
v-model=
"imageUrl"
:placeholder=
"$options.badgeImageUrlPlaceholder"
@
input=
"debouncedPreview"
/>
<span
class=
"help-block"
v-html=
"helpText"
></span>
</div>
<div
class=
"form-group"
>
<label
for=
"badge-preview"
>
{{
s__
(
'
Badges|Badge image preview
'
)
}}
</label>
<badge
id=
"badge-preview"
v-show=
"renderedBadge && !isRendering"
:image-url=
"renderedImageUrl"
:link-url=
"renderedLinkUrl"
/>
<p
v-show=
"isRendering"
>
<loading-icon
:inline=
"true"
/>
</p>
<p
v-show=
"!renderedBadge && !isRendering"
class=
"disabled-content"
>
{{
s__
(
'
Badges|No image to preview
'
)
}}
</p>
</div>
<div
class=
"row-content-block"
>
<loading-button
type=
"submit"
container-class=
"btn btn-success"
:disabled=
"!canSubmit"
:loading=
"isSaving"
:label=
"submitButtonLabel"
/>
<button
class=
"btn btn-cancel"
type=
"button"
v-if=
"isEditing"
@
click=
"onCancel"
>
{{
__
(
'
Cancel
'
)
}}
</button>
</div>
</form>
</
template
>
app/assets/javascripts/badges/components/badge_list.vue
0 → 100644
View file @
31dd86b6
<
script
>
import
{
mapState
}
from
'
vuex
'
;
import
LoadingIcon
from
'
~/vue_shared/components/loading_icon.vue
'
;
import
BadgeListRow
from
'
./badge_list_row.vue
'
;
import
{
GROUP_BADGE
}
from
'
../constants
'
;
export
default
{
name
:
'
BadgeList
'
,
components
:
{
BadgeListRow
,
LoadingIcon
,
},
computed
:
{
...
mapState
([
'
badges
'
,
'
isLoading
'
,
'
kind
'
]),
hasNoBadges
()
{
return
!
this
.
isLoading
&&
(
!
this
.
badges
||
!
this
.
badges
.
length
);
},
isGroupBadge
()
{
return
this
.
kind
===
GROUP_BADGE
;
},
},
};
</
script
>
<
template
>
<div
class=
"panel panel-default"
>
<div
class=
"panel-heading"
>
{{
s__
(
'
Badges|Your badges
'
)
}}
<span
v-show=
"!isLoading"
class=
"badge"
>
{{
badges
.
length
}}
</span>
</div>
<loading-icon
v-show=
"isLoading"
class=
"panel-body"
size=
"2"
/>
<div
v-if=
"hasNoBadges"
class=
"panel-body"
>
<span
v-if=
"isGroupBadge"
>
{{
s__
(
'
Badges|This group has no badges
'
)
}}
</span>
<span
v-else
>
{{
s__
(
'
Badges|This project has no badges
'
)
}}
</span>
</div>
<div
v-else
class=
"panel-body"
>
<badge-list-row
v-for=
"badge in badges"
:key=
"badge.id"
:badge=
"badge"
/>
</div>
</div>
</
template
>
app/assets/javascripts/badges/components/badge_list_row.vue
0 → 100644
View file @
31dd86b6
<
script
>
import
{
mapActions
,
mapState
}
from
'
vuex
'
;
import
{
s__
}
from
'
~/locale
'
;
import
Icon
from
'
~/vue_shared/components/icon.vue
'
;
import
LoadingIcon
from
'
~/vue_shared/components/loading_icon.vue
'
;
import
{
PROJECT_BADGE
}
from
'
../constants
'
;
import
Badge
from
'
./badge.vue
'
;
export
default
{
name
:
'
BadgeListRow
'
,
components
:
{
Badge
,
Icon
,
LoadingIcon
,
},
props
:
{
badge
:
{
type
:
Object
,
required
:
true
,
},
},
computed
:
{
...
mapState
([
'
kind
'
]),
badgeKindText
()
{
if
(
this
.
badge
.
kind
===
PROJECT_BADGE
)
{
return
s__
(
'
Badges|Project Badge
'
);
}
return
s__
(
'
Badges|Group Badge
'
);
},
canEditBadge
()
{
return
this
.
badge
.
kind
===
this
.
kind
;
},
},
methods
:
{
...
mapActions
([
'
editBadge
'
,
'
updateBadgeInModal
'
]),
},
};
</
script
>
<
template
>
<div
class=
"gl-responsive-table-row-layout gl-responsive-table-row"
>
<badge
class=
"table-section section-30"
:image-url=
"badge.renderedImageUrl"
:link-url=
"badge.renderedLinkUrl"
/>
<span
class=
"table-section section-50 str-truncated"
>
{{
badge
.
linkUrl
}}
</span>
<div
class=
"table-section section-10"
>
<span
class=
"badge"
>
{{
badgeKindText
}}
</span>
</div>
<div
class=
"table-section section-10 table-button-footer"
>
<div
v-if=
"canEditBadge"
class=
"table-action-buttons"
>
<button
class=
"btn btn-default append-right-8"
type=
"button"
:disabled=
"badge.isDeleting"
@
click=
"editBadge(badge)"
>
<icon
name=
"pencil"
:size=
"16"
:aria-label=
"__('Edit')"
/>
</button>
<button
class=
"btn btn-danger"
type=
"button"
data-toggle=
"modal"
data-target=
"#delete-badge-modal"
:disabled=
"badge.isDeleting"
@
click=
"updateBadgeInModal(badge)"
>
<icon
name=
"remove"
:size=
"16"
:aria-label=
"__('Delete')"
/>
</button>
<loading-icon
v-show=
"badge.isDeleting"
:inline=
"true"
/>
</div>
</div>
</div>
</
template
>
app/assets/javascripts/badges/components/badge_settings.vue
0 → 100644
View file @
31dd86b6
<
script
>
import
{
mapState
,
mapActions
}
from
'
vuex
'
;
import
createFlash
from
'
~/flash
'
;
import
{
s__
}
from
'
~/locale
'
;
import
GlModal
from
'
~/vue_shared/components/gl_modal.vue
'
;
import
Badge
from
'
./badge.vue
'
;
import
BadgeForm
from
'
./badge_form.vue
'
;
import
BadgeList
from
'
./badge_list.vue
'
;
export
default
{
name
:
'
BadgeSettings
'
,
components
:
{
Badge
,
BadgeForm
,
BadgeList
,
GlModal
,
},
computed
:
{
...
mapState
([
'
badgeInModal
'
,
'
isEditing
'
]),
deleteModalText
()
{
return
s__
(
'
Badges|You are going to delete this badge. Deleted badges <strong>cannot</strong> be restored.
'
,
);
},
},
methods
:
{
...
mapActions
([
'
deleteBadge
'
]),
onSubmitModal
()
{
this
.
deleteBadge
(
this
.
badgeInModal
)
.
then
(()
=>
{
createFlash
(
s__
(
'
Badges|The badge was deleted.
'
),
'
notice
'
);
})
.
catch
(
error
=>
{
createFlash
(
s__
(
'
Badges|Deleting the badge failed, please try again.
'
));
throw
error
;
});
},
},
};
</
script
>
<
template
>
<div
class=
"badge-settings"
>
<gl-modal
id=
"delete-badge-modal"
:header-title-text=
"s__('Badges|Delete badge?')"
footer-primary-button-variant=
"danger"
:footer-primary-button-text=
"s__('Badges|Delete badge')"
@
submit=
"onSubmitModal"
>
<div
class=
"well"
>
<badge
:image-url=
"badgeInModal ? badgeInModal.renderedImageUrl : ''"
:link-url=
"badgeInModal ? badgeInModal.renderedLinkUrl : ''"
/>
</div>
<p
v-html=
"deleteModalText"
></p>
</gl-modal>
<badge-form
v-show=
"isEditing"
:is-editing=
"true"
/>
<badge-form
v-show=
"!isEditing"
:is-editing=
"false"
/>
<badge-list
v-show=
"!isEditing"
/>
</div>
</
template
>
app/assets/javascripts/badges/constants.js
0 → 100644
View file @
31dd86b6
export
const
GROUP_BADGE
=
'
group
'
;
export
const
PROJECT_BADGE
=
'
project
'
;
app/assets/javascripts/badges/empty_badge.js
0 → 100644
View file @
31dd86b6
export
default
()
=>
({
imageUrl
:
''
,
isDeleting
:
false
,
linkUrl
:
''
,
renderedImageUrl
:
''
,
renderedLinkUrl
:
''
,
});
app/assets/javascripts/badges/store/actions.js
0 → 100644
View file @
31dd86b6
import
axios
from
'
~/lib/utils/axios_utils
'
;
import
types
from
'
./mutation_types
'
;
export
const
transformBackendBadge
=
badge
=>
({
id
:
badge
.
id
,
imageUrl
:
badge
.
image_url
,
kind
:
badge
.
kind
,
linkUrl
:
badge
.
link_url
,
renderedImageUrl
:
badge
.
rendered_image_url
,
renderedLinkUrl
:
badge
.
rendered_link_url
,
isDeleting
:
false
,
});
export
default
{
requestNewBadge
({
commit
})
{
commit
(
types
.
REQUEST_NEW_BADGE
);
},
receiveNewBadge
({
commit
},
newBadge
)
{
commit
(
types
.
RECEIVE_NEW_BADGE
,
newBadge
);
},
receiveNewBadgeError
({
commit
})
{
commit
(
types
.
RECEIVE_NEW_BADGE_ERROR
);
},
addBadge
({
dispatch
,
state
})
{
const
newBadge
=
state
.
badgeInAddForm
;
const
endpoint
=
state
.
apiEndpointUrl
;
dispatch
(
'
requestNewBadge
'
);
return
axios
.
post
(
endpoint
,
{
image_url
:
newBadge
.
imageUrl
,
link_url
:
newBadge
.
linkUrl
,
})
.
catch
(
error
=>
{
dispatch
(
'
receiveNewBadgeError
'
);
throw
error
;
})
.
then
(
res
=>
{
dispatch
(
'
receiveNewBadge
'
,
transformBackendBadge
(
res
.
data
));
});
},
requestDeleteBadge
({
commit
},
badgeId
)
{
commit
(
types
.
REQUEST_DELETE_BADGE
,
badgeId
);
},
receiveDeleteBadge
({
commit
},
badgeId
)
{
commit
(
types
.
RECEIVE_DELETE_BADGE
,
badgeId
);
},
receiveDeleteBadgeError
({
commit
},
badgeId
)
{
commit
(
types
.
RECEIVE_DELETE_BADGE_ERROR
,
badgeId
);
},
deleteBadge
({
dispatch
,
state
},
badge
)
{
const
badgeId
=
badge
.
id
;
dispatch
(
'
requestDeleteBadge
'
,
badgeId
);
const
endpoint
=
`
${
state
.
apiEndpointUrl
}
/
${
badgeId
}
`
;
return
axios
.
delete
(
endpoint
)
.
catch
(
error
=>
{
dispatch
(
'
receiveDeleteBadgeError
'
,
badgeId
);
throw
error
;
})
.
then
(()
=>
{
dispatch
(
'
receiveDeleteBadge
'
,
badgeId
);
});
},
editBadge
({
commit
},
badge
)
{
commit
(
types
.
START_EDITING
,
badge
);
},
requestLoadBadges
({
commit
},
data
)
{
commit
(
types
.
REQUEST_LOAD_BADGES
,
data
);
},
receiveLoadBadges
({
commit
},
badges
)
{
commit
(
types
.
RECEIVE_LOAD_BADGES
,
badges
);
},
receiveLoadBadgesError
({
commit
})
{
commit
(
types
.
RECEIVE_LOAD_BADGES_ERROR
);
},
loadBadges
({
dispatch
,
state
},
data
)
{
dispatch
(
'
requestLoadBadges
'
,
data
);
const
endpoint
=
state
.
apiEndpointUrl
;
return
axios
.
get
(
endpoint
)
.
catch
(
error
=>
{
dispatch
(
'
receiveLoadBadgesError
'
);
throw
error
;
})
.
then
(
res
=>
{
dispatch
(
'
receiveLoadBadges
'
,
res
.
data
.
map
(
transformBackendBadge
));
});
},
requestRenderedBadge
({
commit
})
{
commit
(
types
.
REQUEST_RENDERED_BADGE
);
},
receiveRenderedBadge
({
commit
},
renderedBadge
)
{
commit
(
types
.
RECEIVE_RENDERED_BADGE
,
renderedBadge
);
},
receiveRenderedBadgeError
({
commit
})
{
commit
(
types
.
RECEIVE_RENDERED_BADGE_ERROR
);
},
renderBadge
({
dispatch
,
state
})
{
const
badge
=
state
.
isEditing
?
state
.
badgeInEditForm
:
state
.
badgeInAddForm
;
const
{
linkUrl
,
imageUrl
}
=
badge
;
if
(
!
linkUrl
||
linkUrl
.
trim
()
===
''
||
!
imageUrl
||
imageUrl
.
trim
()
===
''
)
{
return
Promise
.
resolve
(
badge
);
}
dispatch
(
'
requestRenderedBadge
'
);
const
parameters
=
[
`link_url=
${
encodeURIComponent
(
linkUrl
)}
`
,
`image_url=
${
encodeURIComponent
(
imageUrl
)}
`
,
].
join
(
'
&
'
);
const
renderEndpoint
=
`
${
state
.
apiEndpointUrl
}
/render?
${
parameters
}
`
;
return
axios
.
get
(
renderEndpoint
)
.
catch
(
error
=>
{
dispatch
(
'
receiveRenderedBadgeError
'
);
throw
error
;
})
.
then
(
res
=>
{
dispatch
(
'
receiveRenderedBadge
'
,
transformBackendBadge
(
res
.
data
));
});
},
requestUpdatedBadge
({
commit
})
{
commit
(
types
.
REQUEST_UPDATED_BADGE
);
},
receiveUpdatedBadge
({
commit
},
updatedBadge
)
{
commit
(
types
.
RECEIVE_UPDATED_BADGE
,
updatedBadge
);
},
receiveUpdatedBadgeError
({
commit
})
{
commit
(
types
.
RECEIVE_UPDATED_BADGE_ERROR
);
},
saveBadge
({
dispatch
,
state
})
{
const
badge
=
state
.
badgeInEditForm
;
const
endpoint
=
`
${
state
.
apiEndpointUrl
}
/
${
badge
.
id
}
`
;
dispatch
(
'
requestUpdatedBadge
'
);
return
axios
.
put
(
endpoint
,
{
image_url
:
badge
.
imageUrl
,
link_url
:
badge
.
linkUrl
,
})
.
catch
(
error
=>
{
dispatch
(
'
receiveUpdatedBadgeError
'
);
throw
error
;
})
.
then
(
res
=>
{
dispatch
(
'
receiveUpdatedBadge
'
,
transformBackendBadge
(
res
.
data
));
});
},
stopEditing
({
commit
})
{
commit
(
types
.
STOP_EDITING
);
},
updateBadgeInForm
({
commit
},
badge
)
{
commit
(
types
.
UPDATE_BADGE_IN_FORM
,
badge
);
},
updateBadgeInModal
({
commit
},
badge
)
{
commit
(
types
.
UPDATE_BADGE_IN_MODAL
,
badge
);
},
};
app/assets/javascripts/badges/store/index.js
0 → 100644
View file @
31dd86b6
import
Vue
from
'
vue
'
;
import
Vuex
from
'
vuex
'
;
import
createState
from
'
./state
'
;
import
actions
from
'
./actions
'
;
import
mutations
from
'
./mutations
'
;
Vue
.
use
(
Vuex
);
export
default
new
Vuex
.
Store
({
state
:
createState
(),
actions
,
mutations
,
});
app/assets/javascripts/badges/store/mutation_types.js
0 → 100644
View file @
31dd86b6
export
default
{
RECEIVE_DELETE_BADGE
:
'
RECEIVE_DELETE_BADGE
'
,
RECEIVE_DELETE_BADGE_ERROR
:
'
RECEIVE_DELETE_BADGE_ERROR
'
,
RECEIVE_LOAD_BADGES
:
'
RECEIVE_LOAD_BADGES
'
,
RECEIVE_LOAD_BADGES_ERROR
:
'
RECEIVE_LOAD_BADGES_ERROR
'
,
RECEIVE_NEW_BADGE
:
'
RECEIVE_NEW_BADGE
'
,
RECEIVE_NEW_BADGE_ERROR
:
'
RECEIVE_NEW_BADGE_ERROR
'
,
RECEIVE_RENDERED_BADGE
:
'
RECEIVE_RENDERED_BADGE
'
,
RECEIVE_RENDERED_BADGE_ERROR
:
'
RECEIVE_RENDERED_BADGE_ERROR
'
,
RECEIVE_UPDATED_BADGE
:
'
RECEIVE_UPDATED_BADGE
'
,
RECEIVE_UPDATED_BADGE_ERROR
:
'
RECEIVE_UPDATED_BADGE_ERROR
'
,
REQUEST_DELETE_BADGE
:
'
REQUEST_DELETE_BADGE
'
,
REQUEST_LOAD_BADGES
:
'
REQUEST_LOAD_BADGES
'
,
REQUEST_NEW_BADGE
:
'
REQUEST_NEW_BADGE
'
,
REQUEST_RENDERED_BADGE
:
'
REQUEST_RENDERED_BADGE
'
,
REQUEST_UPDATED_BADGE
:
'
REQUEST_UPDATED_BADGE
'
,
START_EDITING
:
'
START_EDITING
'
,
STOP_EDITING
:
'
STOP_EDITING
'
,
UPDATE_BADGE_IN_FORM
:
'
UPDATE_BADGE_IN_FORM
'
,
UPDATE_BADGE_IN_MODAL
:
'
UPDATE_BADGE_IN_MODAL
'
,
};
app/assets/javascripts/badges/store/mutations.js
0 → 100644
View file @
31dd86b6
import
types
from
'
./mutation_types
'
;
import
{
PROJECT_BADGE
}
from
'
../constants
'
;
const
reorderBadges
=
badges
=>
badges
.
sort
((
a
,
b
)
=>
{
if
(
a
.
kind
!==
b
.
kind
)
{
return
a
.
kind
===
PROJECT_BADGE
?
1
:
-
1
;
}
return
a
.
id
-
b
.
id
;
});
export
default
{
[
types
.
RECEIVE_NEW_BADGE
](
state
,
newBadge
)
{
Object
.
assign
(
state
,
{
badgeInAddForm
:
null
,
badges
:
reorderBadges
(
state
.
badges
.
concat
(
newBadge
)),
isSaving
:
false
,
renderedBadge
:
null
,
});
},
[
types
.
RECEIVE_NEW_BADGE_ERROR
](
state
)
{
Object
.
assign
(
state
,
{
isSaving
:
false
,
});
},
[
types
.
REQUEST_NEW_BADGE
](
state
)
{
Object
.
assign
(
state
,
{
isSaving
:
true
,
});
},
[
types
.
RECEIVE_UPDATED_BADGE
](
state
,
updatedBadge
)
{
const
badges
=
state
.
badges
.
map
(
badge
=>
{
if
(
badge
.
id
===
updatedBadge
.
id
)
{
return
updatedBadge
;
}
return
badge
;
});
Object
.
assign
(
state
,
{
badgeInEditForm
:
null
,
badges
,
isEditing
:
false
,
isSaving
:
false
,
renderedBadge
:
null
,
});
},
[
types
.
RECEIVE_UPDATED_BADGE_ERROR
](
state
)
{
Object
.
assign
(
state
,
{
isSaving
:
false
,
});
},
[
types
.
REQUEST_UPDATED_BADGE
](
state
)
{
Object
.
assign
(
state
,
{
isSaving
:
true
,
});
},
[
types
.
RECEIVE_LOAD_BADGES
](
state
,
badges
)
{
Object
.
assign
(
state
,
{
badges
:
reorderBadges
(
badges
),
isLoading
:
false
,
});
},
[
types
.
RECEIVE_LOAD_BADGES_ERROR
](
state
)
{
Object
.
assign
(
state
,
{
isLoading
:
false
,
});
},
[
types
.
REQUEST_LOAD_BADGES
](
state
,
data
)
{
Object
.
assign
(
state
,
{
kind
:
data
.
kind
,
// project or group
apiEndpointUrl
:
data
.
apiEndpointUrl
,
docsUrl
:
data
.
docsUrl
,
isLoading
:
true
,
});
},
[
types
.
RECEIVE_DELETE_BADGE
](
state
,
badgeId
)
{
const
badges
=
state
.
badges
.
filter
(
badge
=>
badge
.
id
!==
badgeId
);
Object
.
assign
(
state
,
{
badges
,
});
},
[
types
.
RECEIVE_DELETE_BADGE_ERROR
](
state
,
badgeId
)
{
const
badges
=
state
.
badges
.
map
(
badge
=>
{
if
(
badge
.
id
===
badgeId
)
{
return
{
...
badge
,
isDeleting
:
false
,
};
}
return
badge
;
});
Object
.
assign
(
state
,
{
badges
,
});
},
[
types
.
REQUEST_DELETE_BADGE
](
state
,
badgeId
)
{
const
badges
=
state
.
badges
.
map
(
badge
=>
{
if
(
badge
.
id
===
badgeId
)
{
return
{
...
badge
,
isDeleting
:
true
,
};
}
return
badge
;
});
Object
.
assign
(
state
,
{
badges
,
});
},
[
types
.
RECEIVE_RENDERED_BADGE
](
state
,
renderedBadge
)
{
Object
.
assign
(
state
,
{
isRendering
:
false
,
renderedBadge
});
},
[
types
.
RECEIVE_RENDERED_BADGE_ERROR
](
state
)
{
Object
.
assign
(
state
,
{
isRendering
:
false
});
},
[
types
.
REQUEST_RENDERED_BADGE
](
state
)
{
Object
.
assign
(
state
,
{
isRendering
:
true
});
},
[
types
.
START_EDITING
](
state
,
badge
)
{
Object
.
assign
(
state
,
{
badgeInEditForm
:
{
...
badge
},
isEditing
:
true
,
renderedBadge
:
{
...
badge
},
});
},
[
types
.
STOP_EDITING
](
state
)
{
Object
.
assign
(
state
,
{
badgeInEditForm
:
null
,
isEditing
:
false
,
renderedBadge
:
null
,
});
},
[
types
.
UPDATE_BADGE_IN_FORM
](
state
,
badge
)
{
if
(
state
.
isEditing
)
{
Object
.
assign
(
state
,
{
badgeInEditForm
:
badge
,
});
}
else
{
Object
.
assign
(
state
,
{
badgeInAddForm
:
badge
,
});
}
},
[
types
.
UPDATE_BADGE_IN_MODAL
](
state
,
badge
)
{
Object
.
assign
(
state
,
{
badgeInModal
:
badge
,
});
},
};
app/assets/javascripts/badges/store/state.js
0 → 100644
View file @
31dd86b6
export
default
()
=>
({
apiEndpointUrl
:
null
,
badgeInAddForm
:
null
,
badgeInEditForm
:
null
,
badgeInModal
:
null
,
badges
:
[],
docsUrl
:
null
,
renderedBadge
:
null
,
isEditing
:
false
,
isLoading
:
false
,
isRendering
:
false
,
isSaving
:
false
,
});
app/assets/javascripts/pages/groups/settings/badges/index.js
0 → 100644
View file @
31dd86b6
import
Vue
from
'
vue
'
;
import
Translate
from
'
~/vue_shared/translate
'
;
import
{
GROUP_BADGE
}
from
'
~/badges/constants
'
;
import
mountBadgeSettings
from
'
~/pages/shared/mount_badge_settings
'
;
Vue
.
use
(
Translate
);
document
.
addEventListener
(
'
DOMContentLoaded
'
,
()
=>
{
mountBadgeSettings
(
GROUP_BADGE
);
});
app/assets/javascripts/pages/projects/settings/badges/index/index.js
0 → 100644
View file @
31dd86b6
import
Vue
from
'
vue
'
;
import
Translate
from
'
~/vue_shared/translate
'
;
import
{
PROJECT_BADGE
}
from
'
~/badges/constants
'
;
import
mountBadgeSettings
from
'
~/pages/shared/mount_badge_settings
'
;
Vue
.
use
(
Translate
);
document
.
addEventListener
(
'
DOMContentLoaded
'
,
()
=>
{
mountBadgeSettings
(
PROJECT_BADGE
);
});
app/assets/javascripts/pages/shared/mount_badge_settings.js
0 → 100644
View file @
31dd86b6
import
Vue
from
'
vue
'
;
import
BadgeSettings
from
'
~/badges/components/badge_settings.vue
'
;
import
store
from
'
~/badges/store
'
;
export
default
kind
=>
{
const
badgeSettingsElement
=
document
.
getElementById
(
'
badge-settings
'
);
store
.
dispatch
(
'
loadBadges
'
,
{
kind
,
apiEndpointUrl
:
badgeSettingsElement
.
dataset
.
apiEndpointUrl
,
docsUrl
:
badgeSettingsElement
.
dataset
.
docsUrl
,
});
return
new
Vue
({
el
:
badgeSettingsElement
,
store
,
components
:
{
BadgeSettings
,
},
render
(
createElement
)
{
return
createElement
(
BadgeSettings
);
},
});
};
app/assets/stylesheets/framework/responsive_tables.scss
View file @
31dd86b6
...
@@ -39,7 +39,7 @@
...
@@ -39,7 +39,7 @@
.table-section
{
.table-section
{
white-space
:
nowrap
;
white-space
:
nowrap
;
$section-widths
:
10
15
20
25
30
40
100
;
$section-widths
:
10
15
20
25
30
40
50
100
;
@each
$width
in
$section-widths
{
@each
$width
in
$section-widths
{
&
.section-
#{
$width
}
{
&
.section-
#{
$width
}
{
flex
:
0
0
#{
$width
+
'%'
}
;
flex
:
0
0
#{
$width
+
'%'
}
;
...
...
app/assets/stylesheets/pages/projects.scss
View file @
31dd86b6
...
@@ -1143,3 +1143,11 @@ pre.light-well {
...
@@ -1143,3 +1143,11 @@ pre.light-well {
white-space
:
pre-wrap
;
white-space
:
pre-wrap
;
}
}
}
}
.project-badge
{
opacity
:
0
.9
;
&
:hover
{
opacity
:
1
;
}
}
app/controllers/groups/settings/badges_controller.rb
0 → 100644
View file @
31dd86b6
module
Groups
module
Settings
class
BadgesController
<
Groups
::
ApplicationController
include
GrapeRouteHelpers
::
NamedRouteMatcher
before_action
:authorize_admin_group!
def
index
@badge_api_endpoint
=
api_v4_groups_badges_path
(
id:
@group
.
id
)
end
end
end
end
app/controllers/projects/settings/badges_controller.rb
0 → 100644
View file @
31dd86b6
module
Projects
module
Settings
class
BadgesController
<
Projects
::
ApplicationController
include
GrapeRouteHelpers
::
NamedRouteMatcher
before_action
:authorize_admin_project!
def
index
@badge_api_endpoint
=
api_v4_projects_badges_path
(
id:
@project
.
id
)
end
end
end
end
app/helpers/groups_helper.rb
View file @
31dd86b6
module
GroupsHelper
module
GroupsHelper
def
group_nav_link_paths
def
group_nav_link_paths
%w[groups#projects groups#edit ci_cd#show ldap_group_links#index hooks#index audit_events#index pipeline_quota#index]
%w[groups#projects groups#edit
badges#index
ci_cd#show ldap_group_links#index hooks#index audit_events#index pipeline_quota#index]
end
end
def
group_sidebar_links
def
group_sidebar_links
...
...
app/views/groups/settings/badges/index.html.haml
0 → 100644
View file @
31dd86b6
-
breadcrumb_title
_
(
'Project Badges'
)
-
page_title
_
(
'Project Badges'
)
=
render
'shared/badges/badge_settings'
app/views/layouts/nav/sidebar/_group.html.haml
View file @
31dd86b6
...
@@ -112,7 +112,7 @@
...
@@ -112,7 +112,7 @@
%span
.nav-item-name
%span
.nav-item-name
Settings
Settings
%ul
.sidebar-sub-level-items
%ul
.sidebar-sub-level-items
=
nav_link
(
path:
%w[groups#projects groups#edit ci_cd#show]
,
html_options:
{
class:
"fly-out-top-item"
}
)
do
=
nav_link
(
path:
%w[groups#projects groups#edit
badges#index
ci_cd#show]
,
html_options:
{
class:
"fly-out-top-item"
}
)
do
=
link_to
edit_group_path
(
@group
)
do
=
link_to
edit_group_path
(
@group
)
do
%strong
.fly-out-top-item-name
%strong
.fly-out-top-item-name
#{
_
(
'Settings'
)
}
#{
_
(
'Settings'
)
}
...
@@ -122,6 +122,12 @@
...
@@ -122,6 +122,12 @@
%span
%span
General
General
=
nav_link
(
controller: :badges
)
do
=
link_to
group_settings_badges_path
(
@group
),
title:
_
(
'Project Badges'
)
do
%span
=
_
(
'Project Badges'
)
=
nav_link
(
path:
'groups#projects'
)
do
=
nav_link
(
path:
'groups#projects'
)
do
=
link_to
projects_group_path
(
@group
),
title:
'Projects'
do
=
link_to
projects_group_path
(
@group
),
title:
'Projects'
do
%span
%span
...
...
app/views/layouts/nav/sidebar/_project.html.haml
View file @
31dd86b6
...
@@ -258,7 +258,7 @@
...
@@ -258,7 +258,7 @@
#{
_
(
'Snippets'
)
}
#{
_
(
'Snippets'
)
}
-
if
project_nav_tab?
:settings
-
if
project_nav_tab?
:settings
=
nav_link
(
path:
%w[projects#edit project_members#index integrations#show services#edit repository#show ci_cd#show pages#show]
)
do
=
nav_link
(
path:
%w[projects#edit project_members#index integrations#show services#edit repository#show ci_cd#show
badges#index
pages#show]
)
do
=
link_to
edit_project_path
(
@project
),
class:
'shortcuts-tree'
do
=
link_to
edit_project_path
(
@project
),
class:
'shortcuts-tree'
do
.nav-icon-container
.nav-icon-container
=
sprite_icon
(
'settings'
)
=
sprite_icon
(
'settings'
)
...
@@ -268,7 +268,7 @@
...
@@ -268,7 +268,7 @@
%ul
.sidebar-sub-level-items
%ul
.sidebar-sub-level-items
-
can_edit
=
can?
(
current_user
,
:admin_project
,
@project
)
-
can_edit
=
can?
(
current_user
,
:admin_project
,
@project
)
-
if
can_edit
-
if
can_edit
=
nav_link
(
path:
%w[projects#edit project_members#index integrations#show services#edit repository#show ci_cd#show pages#show]
,
html_options:
{
class:
"fly-out-top-item"
}
)
do
=
nav_link
(
path:
%w[projects#edit project_members#index integrations#show services#edit repository#show ci_cd#show
badges#index
pages#show]
,
html_options:
{
class:
"fly-out-top-item"
}
)
do
=
link_to
edit_project_path
(
@project
)
do
=
link_to
edit_project_path
(
@project
)
do
%strong
.fly-out-top-item-name
%strong
.fly-out-top-item-name
#{
_
(
'Settings'
)
}
#{
_
(
'Settings'
)
}
...
@@ -281,6 +281,11 @@
...
@@ -281,6 +281,11 @@
=
link_to
project_project_members_path
(
@project
),
title:
'Members'
do
=
link_to
project_project_members_path
(
@project
),
title:
'Members'
do
%span
%span
Members
Members
-
if
can_edit
=
nav_link
(
controller: :badges
)
do
=
link_to
project_settings_badges_path
(
@project
),
title:
_
(
'Badges'
)
do
%span
=
_
(
'Badges'
)
-
if
can_edit
-
if
can_edit
=
nav_link
(
controller:
[
:integrations
,
:services
,
:hooks
,
:hook_logs
])
do
=
nav_link
(
controller:
[
:integrations
,
:services
,
:hooks
,
:hook_logs
])
do
=
link_to
project_settings_integrations_path
(
@project
),
title:
'Integrations'
do
=
link_to
project_settings_integrations_path
(
@project
),
title:
'Integrations'
do
...
...
app/views/projects/_home_panel.html.haml
View file @
31dd86b6
...
@@ -23,11 +23,14 @@
...
@@ -23,11 +23,14 @@
-
deleted_message
=
s_
(
'ForkedFromProjectPath|Forked from %{project_name} (deleted)'
)
-
deleted_message
=
s_
(
'ForkedFromProjectPath|Forked from %{project_name} (deleted)'
)
=
deleted_message
%
{
project_name:
fork_source_name
(
@project
)
}
=
deleted_message
%
{
project_name:
fork_source_name
(
@project
)
}
.project-badges
.project-badges
.prepend-top-default.append-bottom-default
-
@project
.
badges
.
each
do
|
badge
|
-
@project
.
badges
.
each
do
|
badge
|
-
badge_link_url
=
badge
.
rendered_link_url
(
@project
)
%a
.append-right-8
{
href:
badge
.
rendered_link_url
(
@project
),
%a
{
href:
badge_link_url
,
target:
'_blank'
,
rel:
'noopener noreferrer'
}
target:
'_blank'
,
%img
{
src:
badge
.
rendered_image_url
(
@project
),
alt:
badge_link_url
}
rel:
'noopener noreferrer'
}
>
%img
.project-badge
{
src:
badge
.
rendered_image_url
(
@project
),
'aria-hidden'
:
true
,
alt:
''
}
>
.project-repo-buttons
.project-repo-buttons
.count-buttons
.count-buttons
...
...
app/views/projects/settings/badges/index.html.haml
0 → 100644
View file @
31dd86b6
-
breadcrumb_title
_
(
'Badges'
)
-
page_title
_
(
'Badges'
)
=
render
'shared/badges/badge_settings'
app/views/shared/badges/_badge_settings.html.haml
0 → 100644
View file @
31dd86b6
#badge-settings
{
data:
{
api_endpoint_url:
@badge_api_endpoint
,
docs_url:
help_page_path
(
'user/project/badges'
)}
}
.text-center.prepend-top-default
=
icon
(
'spinner spin 2x'
)
changelogs/unreleased/winh-41174-projects-groups-badges-ui.yml
0 → 100644
View file @
31dd86b6
---
title
:
Projects and groups badges settings UI
merge_request
:
17114
author
:
type
:
added
config/karma.config.js
View file @
31dd86b6
...
@@ -39,7 +39,7 @@ module.exports = function(config) {
...
@@ -39,7 +39,7 @@ module.exports = function(config) {
frameworks
:
[
'
jasmine
'
],
frameworks
:
[
'
jasmine
'
],
files
:
[
files
:
[
{
pattern
:
'
spec/javascripts/test_bundle.js
'
,
watched
:
false
},
{
pattern
:
'
spec/javascripts/test_bundle.js
'
,
watched
:
false
},
{
pattern
:
'
spec/javascripts/fixtures/**/*@(.json|.html|.html.raw)
'
,
included
:
false
},
{
pattern
:
'
spec/javascripts/fixtures/**/*@(.json|.html|.html.raw
|.png
)
'
,
included
:
false
},
],
],
preprocessors
:
{
preprocessors
:
{
'
spec/javascripts/**/*.js
'
:
[
'
webpack
'
,
'
sourcemap
'
],
'
spec/javascripts/**/*.js
'
:
[
'
webpack
'
,
'
sourcemap
'
],
...
...
config/routes/group.rb
View file @
31dd86b6
...
@@ -24,6 +24,7 @@ constraints(::Constraints::GroupUrlConstrainer.new) do
...
@@ -24,6 +24,7 @@ constraints(::Constraints::GroupUrlConstrainer.new) do
constraints:
{
group_id:
Gitlab
::
PathRegex
.
full_namespace_route_regex
})
do
constraints:
{
group_id:
Gitlab
::
PathRegex
.
full_namespace_route_regex
})
do
namespace
:settings
do
namespace
:settings
do
resource
:ci_cd
,
only:
[
:show
],
controller:
'ci_cd'
resource
:ci_cd
,
only:
[
:show
],
controller:
'ci_cd'
resources
:badges
,
only:
[
:index
]
end
end
resource
:variables
,
only:
[
:show
,
:update
]
resource
:variables
,
only:
[
:show
,
:update
]
...
...
config/routes/project.rb
View file @
31dd86b6
...
@@ -435,6 +435,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
...
@@ -435,6 +435,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
resource
:repository
,
only:
[
:show
],
controller: :repository
do
resource
:repository
,
only:
[
:show
],
controller: :repository
do
post
:create_deploy_token
,
path:
'deploy_token/create'
post
:create_deploy_token
,
path:
'deploy_token/create'
end
end
resources
:badges
,
only:
[
:index
]
end
end
# Since both wiki and repository routing contains wildcard characters
# Since both wiki and repository routing contains wildcard characters
...
...
lib/api/badges.rb
View file @
31dd86b6
...
@@ -127,6 +127,7 @@ module API
...
@@ -127,6 +127,7 @@ module API
end
end
destroy_conditionally!
(
badge
)
destroy_conditionally!
(
badge
)
body
false
end
end
end
end
end
end
...
...
spec/features/groups/settings/group_badges_spec.rb
0 → 100644
View file @
31dd86b6
require
'spec_helper'
feature
'Group Badges'
do
include
WaitForRequests
let
(
:user
)
{
create
(
:user
)
}
let
(
:group
)
{
create
(
:group
)
}
let
(
:badge_link_url
)
{
'https://gitlab.com/gitlab-org/gitlab-ee/commits/master'
}
let
(
:badge_image_url
)
{
'https://gitlab.com/gitlab-org/gitlab-ee/badges/master/build.svg'
}
let!
(
:badge_1
)
{
create
(
:group_badge
,
group:
group
)
}
let!
(
:badge_2
)
{
create
(
:group_badge
,
group:
group
)
}
before
do
group
.
add_owner
(
user
)
sign_in
(
user
)
visit
(
group_settings_badges_path
(
group
))
end
it
'shows a list of badges'
,
:js
do
page
.
within
'.badge-settings'
do
wait_for_requests
rows
=
all
(
'.panel-body > div'
)
expect
(
rows
.
length
).
to
eq
2
expect
(
rows
[
0
]).
to
have_content
badge_1
.
link_url
expect
(
rows
[
1
]).
to
have_content
badge_2
.
link_url
end
end
context
'adding a badge'
,
:js
do
it
'user can preview a badge'
do
page
.
within
'.badge-settings form'
do
fill_in
'badge-link-url'
,
with:
badge_link_url
fill_in
'badge-image-url'
,
with:
badge_image_url
within
'#badge-preview'
do
expect
(
find
(
'a'
)[
:href
]).
to
eq
badge_link_url
expect
(
find
(
'a img'
)[
:src
]).
to
eq
badge_image_url
end
end
end
it
do
page
.
within
'.badge-settings'
do
fill_in
'badge-link-url'
,
with:
badge_link_url
fill_in
'badge-image-url'
,
with:
badge_image_url
click_button
'Add badge'
wait_for_requests
within
'.panel-body'
do
expect
(
find
(
'a'
)[
:href
]).
to
eq
badge_link_url
expect
(
find
(
'a img'
)[
:src
]).
to
eq
badge_image_url
end
end
end
end
context
'editing a badge'
,
:js
do
it
'form is shown when clicking edit button in list'
do
page
.
within
'.badge-settings'
do
wait_for_requests
rows
=
all
(
'.panel-body > div'
)
expect
(
rows
.
length
).
to
eq
2
rows
[
1
].
find
(
'[aria-label="Edit"]'
).
click
within
'form'
do
expect
(
find
(
'#badge-link-url'
).
value
).
to
eq
badge_2
.
link_url
expect
(
find
(
'#badge-image-url'
).
value
).
to
eq
badge_2
.
image_url
end
end
end
it
'updates a badge when submitting the edit form'
do
page
.
within
'.badge-settings'
do
wait_for_requests
rows
=
all
(
'.panel-body > div'
)
expect
(
rows
.
length
).
to
eq
2
rows
[
1
].
find
(
'[aria-label="Edit"]'
).
click
within
'form'
do
fill_in
'badge-link-url'
,
with:
badge_link_url
fill_in
'badge-image-url'
,
with:
badge_image_url
click_button
'Save changes'
wait_for_requests
end
rows
=
all
(
'.panel-body > div'
)
expect
(
rows
.
length
).
to
eq
2
expect
(
rows
[
1
]).
to
have_content
badge_link_url
end
end
end
context
'deleting a badge'
,
:js
do
def
click_delete_button
(
badge_row
)
badge_row
.
find
(
'[aria-label="Delete"]'
).
click
end
it
'shows a modal when deleting a badge'
do
wait_for_requests
rows
=
all
(
'.panel-body > div'
)
expect
(
rows
.
length
).
to
eq
2
click_delete_button
(
rows
[
1
])
expect
(
find
(
'.modal .modal-title'
)).
to
have_content
'Delete badge?'
end
it
'deletes a badge when confirming the modal'
do
wait_for_requests
rows
=
all
(
'.panel-body > div'
)
expect
(
rows
.
length
).
to
eq
2
click_delete_button
(
rows
[
1
])
find
(
'.modal .btn-danger'
).
click
wait_for_requests
rows
=
all
(
'.panel-body > div'
)
expect
(
rows
.
length
).
to
eq
1
expect
(
rows
[
0
]).
to
have_content
badge_1
.
link_url
end
end
end
spec/features/projects/settings/project_badges_spec.rb
0 → 100644
View file @
31dd86b6
require
'spec_helper'
feature
'Project Badges'
do
include
WaitForRequests
let
(
:user
)
{
create
(
:user
)
}
let
(
:group
)
{
create
(
:group
)
}
let
(
:project
)
{
create
(
:project
,
namespace:
group
)
}
let
(
:badge_link_url
)
{
'https://gitlab.com/gitlab-org/gitlab-ee/commits/master'
}
let
(
:badge_image_url
)
{
'https://gitlab.com/gitlab-org/gitlab-ee/badges/master/build.svg'
}
let!
(
:project_badge
)
{
create
(
:project_badge
,
project:
project
)
}
let!
(
:group_badge
)
{
create
(
:group_badge
,
group:
group
)
}
before
do
group
.
add_master
(
user
)
sign_in
(
user
)
visit
(
project_settings_badges_path
(
project
))
end
it
'shows a list of badges'
,
:js
do
page
.
within
'.badge-settings'
do
wait_for_requests
rows
=
all
(
'.panel-body > div'
)
expect
(
rows
.
length
).
to
eq
2
expect
(
rows
[
0
]).
to
have_content
group_badge
.
link_url
expect
(
rows
[
1
]).
to
have_content
project_badge
.
link_url
end
end
context
'adding a badge'
,
:js
do
it
'user can preview a badge'
do
page
.
within
'.badge-settings form'
do
fill_in
'badge-link-url'
,
with:
badge_link_url
fill_in
'badge-image-url'
,
with:
badge_image_url
within
'#badge-preview'
do
expect
(
find
(
'a'
)[
:href
]).
to
eq
badge_link_url
expect
(
find
(
'a img'
)[
:src
]).
to
eq
badge_image_url
end
end
end
it
do
page
.
within
'.badge-settings'
do
fill_in
'badge-link-url'
,
with:
badge_link_url
fill_in
'badge-image-url'
,
with:
badge_image_url
click_button
'Add badge'
wait_for_requests
within
'.panel-body'
do
expect
(
find
(
'a'
)[
:href
]).
to
eq
badge_link_url
expect
(
find
(
'a img'
)[
:src
]).
to
eq
badge_image_url
end
end
end
end
context
'editing a badge'
,
:js
do
it
'form is shown when clicking edit button in list'
do
page
.
within
'.badge-settings'
do
wait_for_requests
rows
=
all
(
'.panel-body > div'
)
expect
(
rows
.
length
).
to
eq
2
rows
[
1
].
find
(
'[aria-label="Edit"]'
).
click
within
'form'
do
expect
(
find
(
'#badge-link-url'
).
value
).
to
eq
project_badge
.
link_url
expect
(
find
(
'#badge-image-url'
).
value
).
to
eq
project_badge
.
image_url
end
end
end
it
'updates a badge when submitting the edit form'
do
page
.
within
'.badge-settings'
do
wait_for_requests
rows
=
all
(
'.panel-body > div'
)
expect
(
rows
.
length
).
to
eq
2
rows
[
1
].
find
(
'[aria-label="Edit"]'
).
click
within
'form'
do
fill_in
'badge-link-url'
,
with:
badge_link_url
fill_in
'badge-image-url'
,
with:
badge_image_url
click_button
'Save changes'
wait_for_requests
end
rows
=
all
(
'.panel-body > div'
)
expect
(
rows
.
length
).
to
eq
2
expect
(
rows
[
1
]).
to
have_content
badge_link_url
end
end
end
context
'deleting a badge'
,
:js
do
def
click_delete_button
(
badge_row
)
badge_row
.
find
(
'[aria-label="Delete"]'
).
click
end
it
'shows a modal when deleting a badge'
do
wait_for_requests
rows
=
all
(
'.panel-body > div'
)
expect
(
rows
.
length
).
to
eq
2
click_delete_button
(
rows
[
1
])
expect
(
find
(
'.modal .modal-title'
)).
to
have_content
'Delete badge?'
end
it
'deletes a badge when confirming the modal'
do
wait_for_requests
rows
=
all
(
'.panel-body > div'
)
expect
(
rows
.
length
).
to
eq
2
click_delete_button
(
rows
[
1
])
find
(
'.modal .btn-danger'
).
click
wait_for_requests
rows
=
all
(
'.panel-body > div'
)
expect
(
rows
.
length
).
to
eq
1
expect
(
rows
[
0
]).
to
have_content
group_badge
.
link_url
end
end
end
spec/javascripts/badges/components/badge_form_spec.js
0 → 100644
View file @
31dd86b6
import
Vue
from
'
vue
'
;
import
store
from
'
~/badges/store
'
;
import
BadgeForm
from
'
~/badges/components/badge_form.vue
'
;
import
{
mountComponentWithStore
}
from
'
spec/helpers/vue_mount_component_helper
'
;
import
{
createDummyBadge
}
from
'
../dummy_badge
'
;
describe
(
'
BadgeForm component
'
,
()
=>
{
const
Component
=
Vue
.
extend
(
BadgeForm
);
let
vm
;
beforeEach
(()
=>
{
setFixtures
(
`
<div id="dummy-element"></div>
`
);
});
afterEach
(()
=>
{
vm
.
$destroy
();
});
describe
(
'
methods
'
,
()
=>
{
beforeEach
(()
=>
{
vm
=
mountComponentWithStore
(
Component
,
{
el
:
'
#dummy-element
'
,
store
,
props
:
{
isEditing
:
false
,
},
});
});
describe
(
'
onCancel
'
,
()
=>
{
it
(
'
calls stopEditing
'
,
()
=>
{
spyOn
(
vm
,
'
stopEditing
'
);
vm
.
onCancel
();
expect
(
vm
.
stopEditing
).
toHaveBeenCalled
();
});
});
describe
(
'
onSubmit
'
,
()
=>
{
describe
(
'
if isEditing is true
'
,
()
=>
{
beforeEach
(()
=>
{
spyOn
(
vm
,
'
saveBadge
'
).
and
.
returnValue
(
Promise
.
resolve
());
store
.
replaceState
({
...
store
.
state
,
isSaving
:
false
,
badgeInEditForm
:
createDummyBadge
(),
});
vm
.
isEditing
=
true
;
});
it
(
'
returns immediately if imageUrl is empty
'
,
()
=>
{
store
.
state
.
badgeInEditForm
.
imageUrl
=
''
;
vm
.
onSubmit
();
expect
(
vm
.
saveBadge
).
not
.
toHaveBeenCalled
();
});
it
(
'
returns immediately if linkUrl is empty
'
,
()
=>
{
store
.
state
.
badgeInEditForm
.
linkUrl
=
''
;
vm
.
onSubmit
();
expect
(
vm
.
saveBadge
).
not
.
toHaveBeenCalled
();
});
it
(
'
returns immediately if isSaving is true
'
,
()
=>
{
store
.
state
.
isSaving
=
true
;
vm
.
onSubmit
();
expect
(
vm
.
saveBadge
).
not
.
toHaveBeenCalled
();
});
it
(
'
calls saveBadge
'
,
()
=>
{
vm
.
onSubmit
();
expect
(
vm
.
saveBadge
).
toHaveBeenCalled
();
});
});
describe
(
'
if isEditing is false
'
,
()
=>
{
beforeEach
(()
=>
{
spyOn
(
vm
,
'
addBadge
'
).
and
.
returnValue
(
Promise
.
resolve
());
store
.
replaceState
({
...
store
.
state
,
isSaving
:
false
,
badgeInAddForm
:
createDummyBadge
(),
});
vm
.
isEditing
=
false
;
});
it
(
'
returns immediately if imageUrl is empty
'
,
()
=>
{
store
.
state
.
badgeInAddForm
.
imageUrl
=
''
;
vm
.
onSubmit
();
expect
(
vm
.
addBadge
).
not
.
toHaveBeenCalled
();
});
it
(
'
returns immediately if linkUrl is empty
'
,
()
=>
{
store
.
state
.
badgeInAddForm
.
linkUrl
=
''
;
vm
.
onSubmit
();
expect
(
vm
.
addBadge
).
not
.
toHaveBeenCalled
();
});
it
(
'
returns immediately if isSaving is true
'
,
()
=>
{
store
.
state
.
isSaving
=
true
;
vm
.
onSubmit
();
expect
(
vm
.
addBadge
).
not
.
toHaveBeenCalled
();
});
it
(
'
calls addBadge
'
,
()
=>
{
vm
.
onSubmit
();
expect
(
vm
.
addBadge
).
toHaveBeenCalled
();
});
});
});
});
describe
(
'
if isEditing is false
'
,
()
=>
{
beforeEach
(()
=>
{
vm
=
mountComponentWithStore
(
Component
,
{
el
:
'
#dummy-element
'
,
store
,
props
:
{
isEditing
:
false
,
},
});
});
it
(
'
renders one button
'
,
()
=>
{
const
buttons
=
vm
.
$el
.
querySelectorAll
(
'
.row-content-block button
'
);
expect
(
buttons
.
length
).
toBe
(
1
);
const
buttonAddElement
=
buttons
[
0
];
expect
(
buttonAddElement
).
toBeVisible
();
expect
(
buttonAddElement
).
toHaveText
(
'
Add badge
'
);
});
});
describe
(
'
if isEditing is true
'
,
()
=>
{
beforeEach
(()
=>
{
vm
=
mountComponentWithStore
(
Component
,
{
el
:
'
#dummy-element
'
,
store
,
props
:
{
isEditing
:
true
,
},
});
});
it
(
'
renders two buttons
'
,
()
=>
{
const
buttons
=
vm
.
$el
.
querySelectorAll
(
'
.row-content-block button
'
);
expect
(
buttons
.
length
).
toBe
(
2
);
const
buttonSaveElement
=
buttons
[
0
];
expect
(
buttonSaveElement
).
toBeVisible
();
expect
(
buttonSaveElement
).
toHaveText
(
'
Save changes
'
);
const
buttonCancelElement
=
buttons
[
1
];
expect
(
buttonCancelElement
).
toBeVisible
();
expect
(
buttonCancelElement
).
toHaveText
(
'
Cancel
'
);
});
});
});
spec/javascripts/badges/components/badge_list_row_spec.js
0 → 100644
View file @
31dd86b6
import
$
from
'
jquery
'
;
import
Vue
from
'
vue
'
;
import
{
GROUP_BADGE
,
PROJECT_BADGE
}
from
'
~/badges/constants
'
;
import
store
from
'
~/badges/store
'
;
import
BadgeListRow
from
'
~/badges/components/badge_list_row.vue
'
;
import
{
mountComponentWithStore
}
from
'
spec/helpers/vue_mount_component_helper
'
;
import
{
createDummyBadge
}
from
'
../dummy_badge
'
;
describe
(
'
BadgeListRow component
'
,
()
=>
{
const
Component
=
Vue
.
extend
(
BadgeListRow
);
let
badge
;
let
vm
;
beforeEach
(()
=>
{
setFixtures
(
`
<div id="delete-badge-modal" class="modal"></div>
<div id="dummy-element"></div>
`
);
store
.
replaceState
({
...
store
.
state
,
kind
:
PROJECT_BADGE
,
});
badge
=
createDummyBadge
();
vm
=
mountComponentWithStore
(
Component
,
{
el
:
'
#dummy-element
'
,
store
,
props
:
{
badge
},
});
});
afterEach
(()
=>
{
vm
.
$destroy
();
});
it
(
'
renders the badge
'
,
()
=>
{
const
badgeElement
=
vm
.
$el
.
querySelector
(
'
.project-badge
'
);
expect
(
badgeElement
).
not
.
toBeNull
();
expect
(
badgeElement
.
getAttribute
(
'
src
'
)).
toBe
(
badge
.
renderedImageUrl
);
});
it
(
'
renders the badge link
'
,
()
=>
{
expect
(
vm
.
$el
).
toContainText
(
badge
.
linkUrl
);
});
it
(
'
renders the badge kind
'
,
()
=>
{
expect
(
vm
.
$el
).
toContainText
(
'
Project Badge
'
);
});
it
(
'
shows edit and delete buttons
'
,
()
=>
{
const
buttons
=
vm
.
$el
.
querySelectorAll
(
'
.table-button-footer button
'
);
expect
(
buttons
).
toHaveLength
(
2
);
const
buttonEditElement
=
buttons
[
0
];
expect
(
buttonEditElement
).
toBeVisible
();
expect
(
buttonEditElement
).
toHaveSpriteIcon
(
'
pencil
'
);
const
buttonDeleteElement
=
buttons
[
1
];
expect
(
buttonDeleteElement
).
toBeVisible
();
expect
(
buttonDeleteElement
).
toHaveSpriteIcon
(
'
remove
'
);
});
it
(
'
calls editBadge when clicking then edit button
'
,
()
=>
{
spyOn
(
vm
,
'
editBadge
'
);
const
editButton
=
vm
.
$el
.
querySelector
(
'
.table-button-footer button:first-of-type
'
);
editButton
.
click
();
expect
(
vm
.
editBadge
).
toHaveBeenCalled
();
});
it
(
'
calls updateBadgeInModal and shows modal when clicking then delete button
'
,
done
=>
{
spyOn
(
vm
,
'
updateBadgeInModal
'
);
$
(
'
#delete-badge-modal
'
).
on
(
'
shown.bs.modal
'
,
()
=>
done
());
const
deleteButton
=
vm
.
$el
.
querySelector
(
'
.table-button-footer button:last-of-type
'
);
deleteButton
.
click
();
expect
(
vm
.
updateBadgeInModal
).
toHaveBeenCalled
();
});
describe
(
'
for a group badge
'
,
()
=>
{
beforeEach
(
done
=>
{
badge
.
kind
=
GROUP_BADGE
;
Vue
.
nextTick
()
.
then
(
done
)
.
catch
(
done
.
fail
);
});
it
(
'
renders the badge kind
'
,
()
=>
{
expect
(
vm
.
$el
).
toContainText
(
'
Group Badge
'
);
});
it
(
'
hides edit and delete buttons
'
,
()
=>
{
const
buttons
=
vm
.
$el
.
querySelectorAll
(
'
.table-button-footer button
'
);
expect
(
buttons
).
toHaveLength
(
0
);
});
});
});
spec/javascripts/badges/components/badge_list_spec.js
0 → 100644
View file @
31dd86b6
import
Vue
from
'
vue
'
;
import
{
GROUP_BADGE
,
PROJECT_BADGE
}
from
'
~/badges/constants
'
;
import
store
from
'
~/badges/store
'
;
import
BadgeList
from
'
~/badges/components/badge_list.vue
'
;
import
{
mountComponentWithStore
}
from
'
spec/helpers/vue_mount_component_helper
'
;
import
{
createDummyBadge
}
from
'
../dummy_badge
'
;
describe
(
'
BadgeList component
'
,
()
=>
{
const
Component
=
Vue
.
extend
(
BadgeList
);
const
numberOfDummyBadges
=
3
;
let
vm
;
beforeEach
(()
=>
{
setFixtures
(
'
<div id="dummy-element"></div>
'
);
const
badges
=
[];
for
(
let
id
=
0
;
id
<
numberOfDummyBadges
;
id
+=
1
)
{
badges
.
push
({
id
,
...
createDummyBadge
()
});
}
store
.
replaceState
({
...
store
.
state
,
badges
,
kind
:
PROJECT_BADGE
,
isLoading
:
false
,
});
vm
=
mountComponentWithStore
(
Component
,
{
el
:
'
#dummy-element
'
,
store
,
});
});
afterEach
(()
=>
{
vm
.
$destroy
();
});
it
(
'
renders a header with the badge count
'
,
()
=>
{
const
header
=
vm
.
$el
.
querySelector
(
'
.panel-heading
'
);
expect
(
header
).
toHaveText
(
new
RegExp
(
`Your badges\\s+
${
numberOfDummyBadges
}
`
));
});
it
(
'
renders a row for each badge
'
,
()
=>
{
const
rows
=
vm
.
$el
.
querySelectorAll
(
'
.gl-responsive-table-row
'
);
expect
(
rows
).
toHaveLength
(
numberOfDummyBadges
);
});
it
(
'
renders a message if no badges exist
'
,
done
=>
{
store
.
state
.
badges
=
[];
Vue
.
nextTick
()
.
then
(()
=>
{
expect
(
vm
.
$el
).
toContainText
(
'
This project has no badges
'
);
})
.
then
(
done
)
.
catch
(
done
.
fail
);
});
it
(
'
shows a loading icon when loading
'
,
done
=>
{
store
.
state
.
isLoading
=
true
;
Vue
.
nextTick
()
.
then
(()
=>
{
const
loadingIcon
=
vm
.
$el
.
querySelector
(
'
.fa-spinner
'
);
expect
(
loadingIcon
).
toBeVisible
();
})
.
then
(
done
)
.
catch
(
done
.
fail
);
});
describe
(
'
for group badges
'
,
()
=>
{
beforeEach
(
done
=>
{
store
.
state
.
kind
=
GROUP_BADGE
;
Vue
.
nextTick
()
.
then
(
done
)
.
catch
(
done
.
fail
);
});
it
(
'
renders a message if no badges exist
'
,
done
=>
{
store
.
state
.
badges
=
[];
Vue
.
nextTick
()
.
then
(()
=>
{
expect
(
vm
.
$el
).
toContainText
(
'
This group has no badges
'
);
})
.
then
(
done
)
.
catch
(
done
.
fail
);
});
});
});
spec/javascripts/badges/components/badge_settings_spec.js
0 → 100644
View file @
31dd86b6
import
$
from
'
jquery
'
;
import
Vue
from
'
vue
'
;
import
store
from
'
~/badges/store
'
;
import
BadgeSettings
from
'
~/badges/components/badge_settings.vue
'
;
import
{
mountComponentWithStore
}
from
'
spec/helpers/vue_mount_component_helper
'
;
import
{
createDummyBadge
}
from
'
../dummy_badge
'
;
describe
(
'
BadgeSettings component
'
,
()
=>
{
const
Component
=
Vue
.
extend
(
BadgeSettings
);
let
vm
;
beforeEach
(()
=>
{
setFixtures
(
`
<div id="dummy-element"></div>
<button
id="dummy-modal-button"
type="button"
data-toggle="modal"
data-target="#delete-badge-modal"
>Show modal</button>
`
);
vm
=
mountComponentWithStore
(
Component
,
{
el
:
'
#dummy-element
'
,
store
,
});
});
afterEach
(()
=>
{
vm
.
$destroy
();
});
it
(
'
displays modal if button is clicked
'
,
done
=>
{
const
badge
=
createDummyBadge
();
store
.
state
.
badgeInModal
=
badge
;
const
modal
=
vm
.
$el
.
querySelector
(
'
#delete-badge-modal
'
);
const
button
=
document
.
getElementById
(
'
dummy-modal-button
'
);
$
(
modal
).
on
(
'
shown.bs.modal
'
,
()
=>
{
expect
(
modal
).
toContainText
(
'
Delete badge?
'
);
const
badgeElement
=
modal
.
querySelector
(
'
img.project-badge
'
);
expect
(
badgeElement
).
not
.
toBe
(
null
);
expect
(
badgeElement
.
getAttribute
(
'
src
'
)).
toBe
(
badge
.
renderedImageUrl
);
done
();
});
Vue
.
nextTick
()
.
then
(()
=>
{
button
.
click
();
})
.
catch
(
done
.
fail
);
});
it
(
'
displays a form to add a badge
'
,
()
=>
{
const
form
=
vm
.
$el
.
querySelector
(
'
form:nth-of-type(2)
'
);
expect
(
form
).
not
.
toBe
(
null
);
const
button
=
form
.
querySelector
(
'
.btn-success
'
);
expect
(
button
).
not
.
toBe
(
null
);
expect
(
button
).
toHaveText
(
/Add badge/
);
});
it
(
'
displays badge list
'
,
()
=>
{
const
badgeListElement
=
vm
.
$el
.
querySelector
(
'
.panel
'
);
expect
(
badgeListElement
).
not
.
toBe
(
null
);
expect
(
badgeListElement
).
toBeVisible
();
expect
(
badgeListElement
).
toContainText
(
'
Your badges
'
);
});
describe
(
'
when editing
'
,
()
=>
{
beforeEach
(
done
=>
{
store
.
state
.
isEditing
=
true
;
Vue
.
nextTick
()
.
then
(
done
)
.
catch
(
done
.
fail
);
});
it
(
'
displays a form to edit a badge
'
,
()
=>
{
const
form
=
vm
.
$el
.
querySelector
(
'
form:nth-of-type(1)
'
);
expect
(
form
).
not
.
toBe
(
null
);
const
submitButton
=
form
.
querySelector
(
'
.btn-success
'
);
expect
(
submitButton
).
not
.
toBe
(
null
);
expect
(
submitButton
).
toHaveText
(
/Save changes/
);
const
cancelButton
=
form
.
querySelector
(
'
.btn-cancel
'
);
expect
(
cancelButton
).
not
.
toBe
(
null
);
expect
(
cancelButton
).
toHaveText
(
/Cancel/
);
});
it
(
'
displays no badge list
'
,
()
=>
{
const
badgeListElement
=
vm
.
$el
.
querySelector
(
'
.panel
'
);
expect
(
badgeListElement
).
toBeHidden
();
});
});
describe
(
'
methods
'
,
()
=>
{
describe
(
'
onSubmitModal
'
,
()
=>
{
it
(
'
triggers
'
,
()
=>
{
spyOn
(
vm
,
'
deleteBadge
'
).
and
.
callFake
(()
=>
Promise
.
resolve
());
const
modal
=
vm
.
$el
.
querySelector
(
'
#delete-badge-modal
'
);
const
deleteButton
=
modal
.
querySelector
(
'
.btn-danger
'
);
deleteButton
.
click
();
const
badge
=
store
.
state
.
badgeInModal
;
expect
(
vm
.
deleteBadge
).
toHaveBeenCalledWith
(
badge
);
});
});
});
});
spec/javascripts/badges/components/badge_spec.js
0 → 100644
View file @
31dd86b6
import
Vue
from
'
vue
'
;
import
Badge
from
'
~/badges/components/badge.vue
'
;
import
mountComponent
from
'
spec/helpers/vue_mount_component_helper
'
;
import
{
DUMMY_IMAGE_URL
,
TEST_HOST
}
from
'
spec/test_constants
'
;
describe
(
'
Badge component
'
,
()
=>
{
const
Component
=
Vue
.
extend
(
Badge
);
const
dummyProps
=
{
imageUrl
:
DUMMY_IMAGE_URL
,
linkUrl
:
`
${
TEST_HOST
}
/badge/link/url`
,
};
let
vm
;
const
findElements
=
()
=>
{
const
buttons
=
vm
.
$el
.
querySelectorAll
(
'
button
'
);
return
{
badgeImage
:
vm
.
$el
.
querySelector
(
'
img.project-badge
'
),
loadingIcon
:
vm
.
$el
.
querySelector
(
'
.fa-spinner
'
),
reloadButton
:
buttons
[
buttons
.
length
-
1
],
};
};
const
createComponent
=
(
props
,
el
=
null
)
=>
{
vm
=
mountComponent
(
Component
,
props
,
el
);
const
{
badgeImage
}
=
findElements
();
return
new
Promise
(
resolve
=>
badgeImage
.
addEventListener
(
'
load
'
,
resolve
)).
then
(()
=>
Vue
.
nextTick
(),
);
};
afterEach
(()
=>
{
vm
.
$destroy
();
});
describe
(
'
watchers
'
,
()
=>
{
describe
(
'
imageUrl
'
,
()
=>
{
it
(
'
sets isLoading and resets numRetries and hasError
'
,
done
=>
{
const
props
=
{
...
dummyProps
};
createComponent
(
props
)
.
then
(()
=>
{
expect
(
vm
.
isLoading
).
toBe
(
false
);
vm
.
hasError
=
true
;
vm
.
numRetries
=
42
;
vm
.
imageUrl
=
`
${
props
.
imageUrl
}
#something/else`
;
return
Vue
.
nextTick
();
})
.
then
(()
=>
{
expect
(
vm
.
isLoading
).
toBe
(
true
);
expect
(
vm
.
numRetries
).
toBe
(
0
);
expect
(
vm
.
hasError
).
toBe
(
false
);
})
.
then
(
done
)
.
catch
(
done
.
fail
);
});
});
});
describe
(
'
methods
'
,
()
=>
{
beforeEach
(
done
=>
{
createComponent
({
...
dummyProps
})
.
then
(
done
)
.
catch
(
done
.
fail
);
});
it
(
'
onError resets isLoading and sets hasError
'
,
()
=>
{
vm
.
hasError
=
false
;
vm
.
isLoading
=
true
;
vm
.
onError
();
expect
(
vm
.
hasError
).
toBe
(
true
);
expect
(
vm
.
isLoading
).
toBe
(
false
);
});
it
(
'
onLoad sets isLoading
'
,
()
=>
{
vm
.
isLoading
=
true
;
vm
.
onLoad
();
expect
(
vm
.
isLoading
).
toBe
(
false
);
});
it
(
'
reloadImage resets isLoading and hasError and increases numRetries
'
,
()
=>
{
vm
.
hasError
=
true
;
vm
.
isLoading
=
false
;
vm
.
numRetries
=
0
;
vm
.
reloadImage
();
expect
(
vm
.
hasError
).
toBe
(
false
);
expect
(
vm
.
isLoading
).
toBe
(
true
);
expect
(
vm
.
numRetries
).
toBe
(
1
);
});
});
describe
(
'
behavior
'
,
()
=>
{
beforeEach
(
done
=>
{
setFixtures
(
'
<div id="dummy-element"></div>
'
);
createComponent
({
...
dummyProps
},
'
#dummy-element
'
)
.
then
(
done
)
.
catch
(
done
.
fail
);
});
it
(
'
shows a badge image after loading
'
,
()
=>
{
expect
(
vm
.
isLoading
).
toBe
(
false
);
expect
(
vm
.
hasError
).
toBe
(
false
);
const
{
badgeImage
,
loadingIcon
,
reloadButton
}
=
findElements
();
expect
(
badgeImage
).
toBeVisible
();
expect
(
loadingIcon
).
toBeHidden
();
expect
(
reloadButton
).
toBeHidden
();
expect
(
vm
.
$el
.
innerText
).
toBe
(
''
);
});
it
(
'
shows a loading icon when loading
'
,
done
=>
{
vm
.
isLoading
=
true
;
Vue
.
nextTick
()
.
then
(()
=>
{
const
{
badgeImage
,
loadingIcon
,
reloadButton
}
=
findElements
();
expect
(
badgeImage
).
toBeHidden
();
expect
(
loadingIcon
).
toBeVisible
();
expect
(
reloadButton
).
toBeHidden
();
expect
(
vm
.
$el
.
innerText
).
toBe
(
''
);
})
.
then
(
done
)
.
catch
(
done
.
fail
);
});
it
(
'
shows an error and reload button if loading failed
'
,
done
=>
{
vm
.
hasError
=
true
;
Vue
.
nextTick
()
.
then
(()
=>
{
const
{
badgeImage
,
loadingIcon
,
reloadButton
}
=
findElements
();
expect
(
badgeImage
).
toBeHidden
();
expect
(
loadingIcon
).
toBeHidden
();
expect
(
reloadButton
).
toBeVisible
();
expect
(
reloadButton
).
toHaveSpriteIcon
(
'
retry
'
);
expect
(
vm
.
$el
.
innerText
.
trim
()).
toBe
(
'
No badge image
'
);
})
.
then
(
done
)
.
catch
(
done
.
fail
);
});
});
});
spec/javascripts/badges/dummy_badge.js
0 → 100644
View file @
31dd86b6
import
{
PROJECT_BADGE
}
from
'
~/badges/constants
'
;
import
{
DUMMY_IMAGE_URL
,
TEST_HOST
}
from
'
spec/test_constants
'
;
export
const
createDummyBadge
=
()
=>
{
const
id
=
Math
.
floor
(
1000
*
Math
.
random
());
return
{
id
,
imageUrl
:
`
${
TEST_HOST
}
/badges/
${
id
}
/image/url`
,
isDeleting
:
false
,
linkUrl
:
`
${
TEST_HOST
}
/badges/
${
id
}
/link/url`
,
kind
:
PROJECT_BADGE
,
renderedImageUrl
:
`
${
DUMMY_IMAGE_URL
}
?id=
${
id
}
`
,
renderedLinkUrl
:
`
${
TEST_HOST
}
/badges/
${
id
}
/rendered/link/url`
,
};
};
export
const
createDummyBadgeResponse
=
()
=>
({
image_url
:
`
${
TEST_HOST
}
/badge/image/url`
,
link_url
:
`
${
TEST_HOST
}
/badge/link/url`
,
kind
:
PROJECT_BADGE
,
rendered_image_url
:
DUMMY_IMAGE_URL
,
rendered_link_url
:
`
${
TEST_HOST
}
/rendered/badge/link/url`
,
});
spec/javascripts/badges/store/actions_spec.js
0 → 100644
View file @
31dd86b6
This diff is collapsed.
Click to expand it.
spec/javascripts/badges/store/mutations_spec.js
0 → 100644
View file @
31dd86b6
This diff is collapsed.
Click to expand it.
spec/javascripts/fixtures/one_white_pixel.png
0 → 100644
View file @
31dd86b6
68 Bytes
spec/javascripts/helpers/vue_mount_component_helper.js
View file @
31dd86b6
...
@@ -3,6 +3,12 @@ export const createComponentWithStore = (Component, store, propsData = {}) => ne
...
@@ -3,6 +3,12 @@ export const createComponentWithStore = (Component, store, propsData = {}) => ne
propsData
,
propsData
,
});
});
export
const
mountComponentWithStore
=
(
Component
,
{
el
,
props
,
store
})
=>
new
Component
({
store
,
propsData
:
props
||
{
},
}).
$mount
(
el
);
export
default
(
Component
,
props
=
{},
el
=
null
)
=>
new
Component
({
export
default
(
Component
,
props
=
{},
el
=
null
)
=>
new
Component
({
propsData
:
props
,
propsData
:
props
,
}).
$mount
(
el
);
}).
$mount
(
el
);
spec/javascripts/matchers.js
0 → 100644
View file @
31dd86b6
export
default
{
toHaveSpriteIcon
:
()
=>
({
compare
(
element
,
iconName
)
{
if
(
!
iconName
)
{
throw
new
Error
(
'
toHaveSpriteIcon is missing iconName argument!
'
);
}
if
(
!
(
element
instanceof
HTMLElement
))
{
throw
new
Error
(
`
${
element
}
is not a DOM element!`
);
}
const
iconReferences
=
[].
slice
.
apply
(
element
.
querySelectorAll
(
'
svg use
'
));
const
matchingIcon
=
iconReferences
.
find
(
reference
=>
reference
.
getAttribute
(
'
xlink:href
'
).
endsWith
(
`#
${
iconName
}
`
));
const
result
=
{
pass
:
!!
matchingIcon
,
};
if
(
result
.
pass
)
{
result
.
message
=
`
${
element
.
outerHTML
}
contains the sprite icon "
${
iconName
}
"!`
;
}
else
{
result
.
message
=
`
${
element
.
outerHTML
}
does not contain the sprite icon "
${
iconName
}
"!`
;
const
existingIcons
=
iconReferences
.
map
((
reference
)
=>
{
const
iconUrl
=
reference
.
getAttribute
(
'
xlink:href
'
);
return
`"
${
iconUrl
.
replace
(
/^.+#/
,
''
)}
"`
;
});
if
(
existingIcons
.
length
>
0
)
{
result
.
message
+=
` (only found
${
existingIcons
.
join
(
'
,
'
)}
)`
;
}
}
return
result
;
},
}),
};
spec/javascripts/test_bundle.js
View file @
31dd86b6
...
@@ -7,6 +7,9 @@ import Vue from 'vue';
...
@@ -7,6 +7,9 @@ import Vue from 'vue';
import
VueResource
from
'
vue-resource
'
;
import
VueResource
from
'
vue-resource
'
;
import
{
getDefaultAdapter
}
from
'
~/lib/utils/axios_utils
'
;
import
{
getDefaultAdapter
}
from
'
~/lib/utils/axios_utils
'
;
import
{
FIXTURES_PATH
,
TEST_HOST
}
from
'
./test_constants
'
;
import
customMatchers
from
'
./matchers
'
;
const
isHeadlessChrome
=
/
\b
HeadlessChrome
\/
/
.
test
(
navigator
.
userAgent
);
const
isHeadlessChrome
=
/
\b
HeadlessChrome
\/
/
.
test
(
navigator
.
userAgent
);
Vue
.
config
.
devtools
=
!
isHeadlessChrome
;
Vue
.
config
.
devtools
=
!
isHeadlessChrome
;
...
@@ -27,15 +30,17 @@ Vue.config.errorHandler = function (err) {
...
@@ -27,15 +30,17 @@ Vue.config.errorHandler = function (err) {
Vue
.
use
(
VueResource
);
Vue
.
use
(
VueResource
);
// enable test fixtures
// enable test fixtures
jasmine
.
getFixtures
().
fixturesPath
=
'
/base/spec/javascripts/fixtures
'
;
jasmine
.
getFixtures
().
fixturesPath
=
FIXTURES_PATH
;
jasmine
.
getJSONFixtures
().
fixturesPath
=
'
/base/spec/javascripts/fixtures
'
;
jasmine
.
getJSONFixtures
().
fixturesPath
=
FIXTURES_PATH
;
beforeAll
(()
=>
jasmine
.
addMatchers
(
customMatchers
));
// globalize common libraries
// globalize common libraries
window
.
$
=
window
.
jQuery
=
$
;
window
.
$
=
window
.
jQuery
=
$
;
// stub expected globals
// stub expected globals
window
.
gl
=
window
.
gl
||
{};
window
.
gl
=
window
.
gl
||
{};
window
.
gl
.
TEST_HOST
=
'
http://test.host
'
;
window
.
gl
.
TEST_HOST
=
TEST_HOST
;
window
.
gon
=
window
.
gon
||
{};
window
.
gon
=
window
.
gon
||
{};
window
.
gon
.
test_env
=
true
;
window
.
gon
.
test_env
=
true
;
...
...
spec/javascripts/test_constants.js
0 → 100644
View file @
31dd86b6
export
const
FIXTURES_PATH
=
'
/base/spec/javascripts/fixtures
'
;
export
const
TEST_HOST
=
'
http://test.host
'
;
export
const
DUMMY_IMAGE_URL
=
`
${
FIXTURES_PATH
}
/one_white_pixel.png`
;
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