Commit aff240cb authored by Martin Wortschack's avatar Martin Wortschack

Merge branch 'add-badge-name-field' into 'master'

Add badge name field

Closes #23135

See merge request gitlab-org/gitlab!16998
parents ed00cd2e ed36e359
......@@ -14,6 +14,11 @@ export default {
GlTooltip: GlTooltipDirective,
},
props: {
name: {
type: String,
required: false,
default: '',
},
imageUrl: {
type: String,
required: true,
......
......@@ -4,7 +4,7 @@ import { mapActions, mapState } from 'vuex';
import createFlash from '~/flash';
import { s__, sprintf } from '~/locale';
import LoadingButton from '~/vue_shared/components/loading_button.vue';
import { GlLoadingIcon } from '@gitlab/ui';
import { GlLoadingIcon, GlFormInput, GlFormGroup } from '@gitlab/ui';
import createEmptyBadge from '../empty_badge';
import Badge from './badge.vue';
......@@ -16,6 +16,8 @@ export default {
Badge,
LoadingButton,
GlLoadingIcon,
GlFormInput,
GlFormGroup,
},
props: {
isEditing: {
......@@ -64,6 +66,18 @@ export default {
renderedLinkUrl() {
return this.renderedBadge ? this.renderedBadge.renderedLinkUrl : '';
},
name: {
get() {
return this.badge ? this.badge.name : '';
},
set(name) {
const badge = this.badge || createEmptyBadge();
this.updateBadgeInForm({
...badge,
name,
});
},
},
imageUrl: {
get() {
return this.badge ? this.badge.imageUrl : '';
......@@ -154,6 +168,10 @@ export default {
novalidate
@submit.prevent.stop="onSubmit"
>
<gl-form-group :label="s__('Badges|Name')" label-for="badge-name">
<gl-form-input id="badge-name" v-model="name" />
</gl-form-group>
<div class="form-group">
<label for="badge-link-url" class="label-bold">{{ s__('Badges|Link') }}</label>
<p v-html="helpText"></p>
......
......@@ -43,13 +43,14 @@ export default {
<badge
:image-url="badge.renderedImageUrl"
:link-url="badge.renderedLinkUrl"
class="table-section section-40"
class="table-section section-30"
/>
<span class="table-section section-30 str-truncated">{{ badge.linkUrl }}</span>
<div class="table-section section-15">
<div class="table-section section-30">
<label class="label-bold str-truncated mb-0">{{ badge.name }}</label>
<span class="badge badge-pill">{{ badgeKindText }}</span>
</div>
<div class="table-section section-15 table-button-footer">
<span class="table-section section-30 str-truncated">{{ badge.linkUrl }}</span>
<div class="table-section section-10 table-button-footer">
<div v-if="canEditBadge" class="table-action-buttons">
<button
:disabled="badge.isDeleting"
......
export default () => ({
name: '',
imageUrl: '',
isDeleting: false,
linkUrl: '',
......
import axios from '~/lib/utils/axios_utils';
import types from './mutation_types';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
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,
...convertObjectPropsToCamelCase(badge, true),
isDeleting: false,
});
......@@ -27,6 +23,7 @@ export default {
dispatch('requestNewBadge');
return axios
.post(endpoint, {
name: newBadge.name,
image_url: newBadge.imageUrl,
link_url: newBadge.linkUrl,
})
......@@ -141,6 +138,7 @@ export default {
dispatch('requestUpdatedBadge');
return axios
.put(endpoint, {
name: badge.name,
image_url: badge.imageUrl,
link_url: badge.linkUrl,
})
......
......@@ -22,6 +22,8 @@ class Badge < ApplicationRecord
scope :order_created_at_asc, -> { reorder(created_at: :asc) }
scope :with_name, ->(name) { where(name: name) }
validates :link_url, :image_url, addressable_url: true
validates :type, presence: true
......
---
title: Add badge name field
merge_request: 16998
author: Lee Tickett
type: added
# frozen_string_literal: true
class AddNameToBadges < ActiveRecord::Migration[5.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def change
add_column :badges, :name, :string, null: true, limit: 255
end
end
......@@ -498,6 +498,7 @@ ActiveRecord::Schema.define(version: 2019_11_19_023952) do
t.integer "project_id"
t.integer "group_id"
t.string "type", null: false
t.string "name", limit: 255
t.datetime_with_timezone "created_at", null: false
t.datetime_with_timezone "updated_at", null: false
t.index ["group_id"], name: "index_badges_on_group_id"
......
......@@ -26,9 +26,10 @@ GET /groups/:id/badges
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user |
| `name` | string | no | Name of the badges to return (case-sensitive). |
```bash
curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/groups/:id/badges
curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/groups/:id/badges?name=Coverage
```
Example response:
......@@ -36,21 +37,14 @@ Example response:
```json
[
{
"name": "Coverage",
"id": 1,
"link_url": "http://example.com/ci_status.svg?project=%{project_path}&ref=%{default_branch}",
"image_url": "https://shields.io/my/badge",
"rendered_link_url": "http://example.com/ci_status.svg?project=example-org/example-project&ref=master",
"rendered_image_url": "https://shields.io/my/badge",
"kind": "group"
},
{
"id": 2,
"link_url": "http://example.com/ci_status.svg?project=%{project_path}&ref=%{default_branch}",
"image_url": "https://shields.io/my/badge",
"rendered_link_url": "http://example.com/ci_status.svg?project=example-org/example-project&ref=master",
"rendered_image_url": "https://shields.io/my/badge",
"kind": "group"
},
}
]
```
......
......@@ -23,6 +23,7 @@ GET /projects/:id/badges
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `name` | string | no | Name of the badges to return (case-sensitive). |
```bash
curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/:id/badges
......@@ -33,6 +34,7 @@ Example response:
```json
[
{
"name": "Coverage",
"id": 1,
"link_url": "http://example.com/ci_status.svg?project=%{project_path}&ref=%{default_branch}",
"image_url": "https://shields.io/my/badge",
......@@ -41,6 +43,7 @@ Example response:
"kind": "project"
},
{
"name": "Pipeline",
"id": 2,
"link_url": "http://example.com/ci_status.svg?project=%{project_path}&ref=%{default_branch}",
"image_url": "https://shields.io/my/badge",
......
......@@ -33,7 +33,11 @@ module API
get ":id/badges" do
source = find_source(source_type, params[:id])
present_badges(source, paginate(source.badges))
badges = source.badges
name = params[:name]
badges = badges.with_name(name) if name
present_badges(source, paginate(badges))
end
desc "Preview a badge from a #{source_type}." do
......@@ -80,6 +84,7 @@ module API
params do
requires :link_url, type: String, desc: 'URL of the badge link'
requires :image_url, type: String, desc: 'URL of the badge image'
optional :name, type: String, desc: 'Name for the badge'
end
post ":id/badges" do
source = find_source_if_admin(source_type)
......@@ -100,6 +105,7 @@ module API
params do
optional :link_url, type: String, desc: 'URL of the badge link'
optional :image_url, type: String, desc: 'URL of the badge image'
optional :name, type: String, desc: 'Name for the badge'
end
put ":id/badges/:badge_id" do
source = find_source_if_admin(source_type)
......
......@@ -1736,6 +1736,7 @@ module API
end
class BasicBadgeDetails < Grape::Entity
expose :name
expose :link_url
expose :image_url
expose :rendered_link_url do |badge, options|
......
......@@ -2386,6 +2386,9 @@ msgstr ""
msgid "Badges|Link"
msgstr ""
msgid "Badges|Name"
msgstr ""
msgid "Badges|No badge image"
msgstr ""
......
......@@ -51,13 +51,14 @@ describe('BadgeForm component', () => {
});
const sharedSubmitTests = submitAction => {
const nameSelector = '#badge-name';
const imageUrlSelector = '#badge-image-url';
const findImageUrlElement = () => vm.$el.querySelector(imageUrlSelector);
const linkUrlSelector = '#badge-link-url';
const findLinkUrlElement = () => vm.$el.querySelector(linkUrlSelector);
const setValue = (inputElementSelector, url) => {
const setValue = (inputElementSelector, value) => {
const inputElement = vm.$el.querySelector(inputElementSelector);
inputElement.value = url;
inputElement.value = value;
inputElement.dispatchEvent(new Event('input'));
};
const submitForm = () => {
......@@ -82,6 +83,7 @@ describe('BadgeForm component', () => {
isSaving: false,
});
setValue(nameSelector, 'TestBadge');
setValue(linkUrlSelector, `${TEST_HOST}/link/url`);
setValue(imageUrlSelector, `${window.location.origin}${DUMMY_IMAGE_URL}`);
});
......
......@@ -39,6 +39,10 @@ describe('BadgeListRow component', () => {
expect(badgeElement.getAttribute('src')).toBe(badge.renderedImageUrl);
});
it('renders the badge name', () => {
expect(vm.$el).toContainText(badge.name);
});
it('renders the badge link', () => {
expect(vm.$el).toContainText(badge.linkUrl);
});
......
......@@ -6,6 +6,7 @@ export const createDummyBadge = () => {
const id = _.uniqueId();
return {
id,
name: 'TestBadge',
imageUrl: `${TEST_HOST}/badges/${id}/image/url`,
isDeleting: false,
linkUrl: `${TEST_HOST}/badges/${id}/link/url`,
......@@ -16,6 +17,7 @@ export const createDummyBadge = () => {
};
export const createDummyBadgeResponse = () => ({
name: 'TestBadge',
image_url: `${TEST_HOST}/badge/image/url`,
link_url: `${TEST_HOST}/badge/link/url`,
kind: PROJECT_BADGE,
......
......@@ -90,6 +90,7 @@ describe('Badges store actions', () => {
endpointMock.replyOnce(req => {
expect(req.data).toBe(
JSON.stringify({
name: 'TestBadge',
image_url: badgeInAddForm.imageUrl,
link_url: badgeInAddForm.linkUrl,
}),
......@@ -114,6 +115,7 @@ describe('Badges store actions', () => {
endpointMock.replyOnce(req => {
expect(req.data).toBe(
JSON.stringify({
name: 'TestBadge',
image_url: badgeInAddForm.imageUrl,
link_url: badgeInAddForm.linkUrl,
}),
......@@ -526,6 +528,7 @@ describe('Badges store actions', () => {
endpointMock.replyOnce(req => {
expect(req.data).toBe(
JSON.stringify({
name: 'TestBadge',
image_url: badgeInEditForm.imageUrl,
link_url: badgeInEditForm.linkUrl,
}),
......@@ -550,6 +553,7 @@ describe('Badges store actions', () => {
endpointMock.replyOnce(req => {
expect(req.data).toBe(
JSON.stringify({
name: 'TestBadge',
image_url: badgeInEditForm.imageUrl,
link_url: badgeInEditForm.linkUrl,
}),
......
......@@ -651,6 +651,7 @@ PrometheusAlert:
- prometheus_metric_id
Badge:
- id
- name
- link_url
- image_url
- project_id
......
......@@ -81,6 +81,7 @@ describe API::Badges do
get api("/#{source_type.pluralize}/#{source.id}/badges/#{badge.id}", user)
expect(response).to have_gitlab_http_status(200)
expect(json_response['name']).to eq(badge.name)
expect(json_response['id']).to eq(badge.id)
expect(json_response['link_url']).to eq(badge.link_url)
expect(json_response['rendered_link_url']).to eq(badge.rendered_link_url)
......@@ -98,6 +99,7 @@ describe API::Badges do
include_context 'source helpers'
let(:source) { get_source(source_type) }
let(:example_name) { 'BadgeName' }
let(:example_url) { 'http://www.example.com' }
let(:example_url2) { 'http://www.example1.com' }
......@@ -105,7 +107,7 @@ describe API::Badges do
it_behaves_like 'a 404 response when source is private' do
let(:route) do
post api("/#{source_type.pluralize}/#{source.id}/badges", stranger),
params: { link_url: example_url, image_url: example_url2 }
params: { name: example_name, link_url: example_url, image_url: example_url2 }
end
end
......@@ -128,11 +130,12 @@ describe API::Badges do
it 'creates a new badge' do
expect do
post api("/#{source_type.pluralize}/#{source.id}/badges", maintainer),
params: { link_url: example_url, image_url: example_url2 }
params: { name: example_name, link_url: example_url, image_url: example_url2 }
expect(response).to have_gitlab_http_status(201)
end.to change { source.badges.count }.by(1)
expect(json_response['name']).to eq(example_name)
expect(json_response['link_url']).to eq(example_url)
expect(json_response['image_url']).to eq(example_url2)
expect(json_response['kind']).to eq source_type
......@@ -169,6 +172,7 @@ describe API::Badges do
context "with :sources == #{source_type.pluralize}" do
let(:badge) { source.badges.first }
let(:example_name) { 'BadgeName' }
let(:example_url) { 'http://www.example.com' }
let(:example_url2) { 'http://www.example1.com' }
......@@ -197,9 +201,10 @@ describe API::Badges do
context 'when authenticated as a maintainer/owner' do
it 'updates the member', :quarantine do
put api("/#{source_type.pluralize}/#{source.id}/badges/#{badge.id}", maintainer),
params: { link_url: example_url, image_url: example_url2 }
params: { name: example_name, link_url: example_url, image_url: example_url2 }
expect(response).to have_gitlab_http_status(200)
expect(json_response['name']).to eq(example_name)
expect(json_response['link_url']).to eq(example_url)
expect(json_response['image_url']).to eq(example_url2)
expect(json_response['kind']).to eq source_type
......@@ -297,7 +302,7 @@ describe API::Badges do
expect(response).to have_gitlab_http_status(200)
expect(json_response.keys).to contain_exactly('link_url', 'rendered_link_url', 'image_url', 'rendered_image_url')
expect(json_response.keys).to contain_exactly('name', 'link_url', 'rendered_link_url', 'image_url', 'rendered_image_url')
expect(json_response['link_url']).to eq(example_url)
expect(json_response['image_url']).to eq(example_url2)
expect(json_response['rendered_link_url']).to eq(example_url)
......@@ -351,9 +356,9 @@ describe API::Badges do
project.add_developer(developer)
project.add_maintainer(maintainer)
project.request_access(access_requester)
project.project_badges << build(:project_badge, project: project)
project.project_badges << build(:project_badge, project: project)
project_group.badges << build(:group_badge, group: group)
project.project_badges << build(:project_badge, project: project, name: 'ExampleBadge1')
project.project_badges << build(:project_badge, project: project, name: 'ExampleBadge2')
project_group.badges << build(:group_badge, group: group, name: 'ExampleBadge3')
end
end
......@@ -362,8 +367,8 @@ describe API::Badges do
group.add_developer(developer)
group.add_owner(maintainer)
group.request_access(access_requester)
group.badges << build(:group_badge, group: group)
group.badges << build(:group_badge, group: group)
group.badges << build(:group_badge, group: group, name: 'ExampleBadge4')
group.badges << build(:group_badge, group: group, name: 'ExampleBadge5')
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment