Move badge settings to general settings

This commit is contained in:
Winnie Hellmann 2018-09-03 13:16:23 +00:00 committed by Phil Hughes
parent c0625e5de1
commit 743add978a
24 changed files with 259 additions and 189 deletions

View file

@ -23,6 +23,11 @@ export default {
required: true,
},
},
data() {
return {
wasValidated: false,
};
},
computed: {
...mapState([
'badgeInAddForm',
@ -39,16 +44,6 @@ export default {
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>`)
@ -93,11 +88,18 @@ export default {
});
},
},
submitButtonLabel() {
if (this.isEditing) {
return s__('Badges|Save changes');
}
return s__('Badges|Add badge');
badgeImageUrlExample() {
const exampleUrl =
'https://example.gitlab.com/%{project_path}/badges/%{default_branch}/badge.svg';
return sprintf(s__('Badges|e.g. %{exampleUrl}'), {
exampleUrl,
});
},
badgeLinkUrlExample() {
const exampleUrl = 'https://example.gitlab.com/%{project_path}';
return sprintf(s__('Badges|e.g. %{exampleUrl}'), {
exampleUrl,
});
},
},
methods: {
@ -109,7 +111,9 @@ export default {
this.stopEditing();
},
onSubmit() {
if (!this.canSubmit) {
const form = this.$el;
if (!form.checkValidity()) {
this.wasValidated = true;
return Promise.resolve();
}
@ -117,6 +121,7 @@ export default {
return this.saveBadge()
.then(() => {
createFlash(s__('Badges|The badge was saved.'), 'notice');
this.wasValidated = false;
})
.catch(error => {
createFlash(
@ -129,6 +134,7 @@ export default {
return this.addBadge()
.then(() => {
createFlash(s__('Badges|A new badge was added.'), 'notice');
this.wasValidated = false;
})
.catch(error => {
createFlash(
@ -138,47 +144,58 @@ export default {
});
},
},
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"
:class="{ 'was-validated': wasValidated }"
class="prepend-top-default append-bottom-default needs-validation"
novalidate
@submit.prevent.stop="onSubmit"
>
<div class="form-group">
<label for="badge-link-url">{{ s__('Badges|Link') }}</label>
<label
for="badge-link-url"
class="label-bold"
>{{ s__('Badges|Link') }}</label>
<p v-html="helpText"></p>
<input
id="badge-link-url"
v-model="linkUrl"
:placeholder="$options.badgeLinkUrlPlaceholder"
type="text"
type="URL"
class="form-control"
required
@input="debouncedPreview"
/>
<span
class="form-text text-muted"
v-html="helpText"
></span>
<div class="invalid-feedback">
{{ s__('Badges|Please fill in a valid URL') }}
</div>
<span class="form-text text-muted">
{{ badgeLinkUrlExample }}
</span>
</div>
<div class="form-group">
<label for="badge-image-url">{{ s__('Badges|Badge image URL') }}</label>
<label
for="badge-image-url"
class="label-bold"
>{{ s__('Badges|Badge image URL') }}</label>
<p v-html="helpText"></p>
<input
id="badge-image-url"
v-model="imageUrl"
:placeholder="$options.badgeImageUrlPlaceholder"
type="text"
type="URL"
class="form-control"
required
@input="debouncedPreview"
/>
<span
class="form-text text-muted"
v-html="helpText"
></span>
<div class="invalid-feedback">
{{ s__('Badges|Please fill in a valid URL') }}
</div>
<span class="form-text text-muted">
{{ badgeImageUrlExample }}
</span>
</div>
<div class="form-group">
@ -200,20 +217,32 @@ export default {
>{{ s__('Badges|No image to preview') }}</p>
</div>
<div class="row-content-block">
<div
v-if="isEditing"
class="row-content-block"
>
<loading-button
:disabled="!canSubmit"
:loading="isSaving"
:label="submitButtonLabel"
:label="s__('Badges|Save changes')"
type="submit"
container-class="btn btn-success"
/>
<button
v-if="isEditing"
class="btn btn-cancel"
type="button"
@click="onCancel"
>{{ __('Cancel') }}</button>
</div>
<div
v-else
class="form-group"
>
<loading-button
:loading="isSaving"
:label="s__('Badges|Add badge')"
type="submit"
container-class="btn btn-success"
/>
</div>
</form>
</template>

View file

@ -28,7 +28,7 @@ export default {
{{ s__('Badges|Your badges') }}
<span
v-show="!isLoading"
class="badge"
class="badge badge-pill"
>{{ badges.length }}</span>
</div>
<loading-icon

View file

@ -43,13 +43,13 @@ export default {
<badge
:image-url="badge.renderedImageUrl"
:link-url="badge.renderedLinkUrl"
class="table-section section-30"
class="table-section section-40"
/>
<span class="table-section section-50 str-truncated">{{ badge.linkUrl }}</span>
<div class="table-section section-10">
<span class="badge">{{ badgeKindText }}</span>
<span class="table-section section-30 str-truncated">{{ badge.linkUrl }}</span>
<div class="table-section section-15">
<span class="badge badge-pill">{{ badgeKindText }}</span>
</div>
<div class="table-section section-10 table-button-footer">
<div class="table-section section-15 table-button-footer">
<div
v-if="canEditBadge"
class="table-action-buttons">

View file

@ -2,14 +2,13 @@ import groupAvatar from '~/group_avatar';
import TransferDropdown from '~/groups/transfer_dropdown';
import initConfirmDangerModal from '~/confirm_danger_modal';
import initSettingsPanels from '~/settings_panels';
import mountBadgeSettings from '~/pages/shared/mount_badge_settings';
import { GROUP_BADGE } from '~/badges/constants';
document.addEventListener('DOMContentLoaded', () => {
groupAvatar();
new TransferDropdown(); // eslint-disable-line no-new
initConfirmDangerModal();
});
document.addEventListener('DOMContentLoaded', () => {
// Initialize expandable settings panels
initSettingsPanels();
mountBadgeSettings(GROUP_BADGE);
});

View file

@ -1,6 +1,8 @@
import { PROJECT_BADGE } from '~/badges/constants';
import initSettingsPanels from '~/settings_panels';
import setupProjectEdit from '~/project_edit';
import initConfirmDangerModal from '~/confirm_danger_modal';
import mountBadgeSettings from '~/pages/shared/mount_badge_settings';
import initProjectLoadingSpinner from '../shared/save_project_loader';
import projectAvatar from '../shared/project_avatar';
import initProjectPermissionsSettings from '../shared/permissions';
@ -13,4 +15,5 @@ document.addEventListener('DOMContentLoaded', () => {
projectAvatar();
initProjectPermissionsSettings();
initConfirmDangerModal();
mountBadgeSettings(PROJECT_BADGE);
});

View file

@ -1,10 +0,0 @@
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);
});

View file

@ -1,13 +0,0 @@
module Groups
module Settings
class BadgesController < Groups::ApplicationController
include API::Helpers::RelatedResourcesHelpers
before_action :authorize_admin_group!
def index
@badge_api_endpoint = expose_url(api_v4_groups_badges_path(id: @group.id))
end
end
end
end

View file

@ -1,4 +1,5 @@
class GroupsController < Groups::ApplicationController
include API::Helpers::RelatedResourcesHelpers
include IssuesAction
include MergeRequestsAction
include ParamsBackwardCompatibility
@ -77,6 +78,7 @@ class GroupsController < Groups::ApplicationController
end
def edit
@badge_api_endpoint = expose_url(api_v4_groups_badges_path(id: @group.id))
end
def projects

View file

@ -1,13 +0,0 @@
module Projects
module Settings
class BadgesController < Projects::ApplicationController
include API::Helpers::RelatedResourcesHelpers
before_action :authorize_admin_project!
def index
@badge_api_endpoint = expose_url(api_v4_projects_badges_path(id: @project.id))
end
end
end
end

View file

@ -1,4 +1,5 @@
class ProjectsController < Projects::ApplicationController
include API::Helpers::RelatedResourcesHelpers
include IssuableCollections
include ExtractsPath
include PreviewMarkdown
@ -32,6 +33,7 @@ class ProjectsController < Projects::ApplicationController
end
def edit
@badge_api_endpoint = expose_url(api_v4_projects_badges_path(id: @project.id))
render 'edit'
end

View file

@ -25,6 +25,18 @@
.settings-content
= render 'groups/settings/permissions'
%section.settings.no-animate{ class: ('expanded' if expanded) }
.settings-header
%h4
= s_('GroupSettings|Badges')
%button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p
= s_('GroupSettings|Customize your group badges.')
= link_to s_('GroupSettings|Learn more about badges.'), help_page_path('user/project/badges')
.settings-content
= render 'shared/badges/badge_settings'
%section.settings.gs-advanced.no-animate#js-advanced-settings{ class: ('expanded' if expanded) }
.settings-header
%h4

View file

@ -122,12 +122,6 @@
%span
= _('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
= link_to projects_group_path(@group), title: _('Projects') do
%span

View file

@ -312,11 +312,6 @@
= link_to project_project_members_path(@project), title: _('Members') do
%span
= _('Members')
- if can_edit
= nav_link(controller: :badges) do
= link_to project_settings_badges_path(@project), title: _('Badges') do
%span
= _('Badges')
- if can_edit
= nav_link(controller: [:integrations, :services, :hooks, :hook_logs]) do
= link_to project_settings_integrations_path(@project), title: _('Integrations') do

View file

@ -102,6 +102,18 @@
= render_if_exists 'projects/service_desk_settings'
%section.settings.no-animate{ class: ('expanded' if expanded) }
.settings-header
%h4
= s_('ProjectSettings|Badges')
%button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p
= s_('ProjectSettings|Customize your project badges.')
= link_to s_('ProjectSettings|Learn more about badges.'), help_page_path('user/project/badges')
.settings-content
= render 'shared/badges/badge_settings'
= render 'export', project: @project
%section.qa-advanced-settings.settings.advanced-settings.no-animate#js-project-advanced-settings{ class: ('expanded' if expanded) }

View file

@ -0,0 +1,5 @@
---
title: Move badge settings to general settings
merge_request: 21333
author:
type: changed

View file

@ -25,7 +25,6 @@ constraints(::Constraints::GroupUrlConstrainer.new) do
constraints: { group_id: Gitlab::PathRegex.full_namespace_route_regex }) do
namespace :settings do
resource :ci_cd, only: [:show], controller: 'ci_cd'
resources :badges, only: [:index]
end
resource :variables, only: [:show, :update]

View file

@ -442,7 +442,6 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
resource :repository, only: [:show], controller: :repository do
post :create_deploy_token, path: 'deploy_token/create'
end
resources :badges, only: [:index]
end
# Since both wiki and repository routing contains wildcard characters

View file

@ -17,7 +17,7 @@ If you find that you have to add the same badges to several projects, you may wa
To add a new badge to a project:
1. Navigate to your project's **Settings > Badges**.
1. Navigate to your project's **Settings > General > Badges**.
1. Under "Link", enter the URL that the badges should point to and under
"Badge image URL" the URL of the image that should be displayed.
1. Submit the badge by clicking the **Add badge** button.
@ -39,7 +39,7 @@ project, consider adding them on the [project level](#project-badges) or use
To add a new badge to a group:
1. Navigate to your group's **Settings > Project Badges**.
1. Navigate to your group's **Settings > General > Badges**.
1. Under "Link", enter the URL that the badges should point to and under
"Badge image URL" the URL of the image that should be displayed.
1. Submit the badge by clicking the **Add badge** button.

View file

@ -805,6 +805,9 @@ msgstr ""
msgid "Badges|No image to preview"
msgstr ""
msgid "Badges|Please fill in a valid URL"
msgstr ""
msgid "Badges|Project Badge"
msgstr ""
@ -838,6 +841,9 @@ msgstr ""
msgid "Badges|Your badges"
msgstr ""
msgid "Badges|e.g. %{exampleUrl}"
msgstr ""
msgid "Begin with the selected commit"
msgstr ""
@ -2897,6 +2903,15 @@ msgstr ""
msgid "Group: %{group_name}"
msgstr ""
msgid "GroupSettings|Badges"
msgstr ""
msgid "GroupSettings|Customize your group badges."
msgstr ""
msgid "GroupSettings|Learn more about badges."
msgstr ""
msgid "GroupSettings|Prevent sharing a project within %{group} with other groups"
msgstr ""
@ -4536,6 +4551,15 @@ msgstr ""
msgid "ProjectPage|Project ID: %{project_id}"
msgstr ""
msgid "ProjectSettings|Badges"
msgstr ""
msgid "ProjectSettings|Customize your project badges."
msgstr ""
msgid "ProjectSettings|Learn more about badges."
msgstr ""
msgid "Projects"
msgstr ""

View file

@ -57,6 +57,16 @@ describe GroupsController do
end
end
describe 'GET edit' do
it 'sets the badge API endpoint' do
sign_in(owner)
get :edit, id: group.to_param
expect(assigns(:badge_api_endpoint)).not_to be_nil
end
end
describe 'GET #new' do
context 'when creating subgroups', :nested_groups do
[true, false].each do |can_create_group_status|

View file

@ -284,6 +284,19 @@ describe ProjectsController do
end
end
describe 'GET edit' do
it 'sets the badge API endpoint' do
sign_in(user)
project.add_maintainer(user)
get :edit,
namespace_id: project.namespace.path,
id: project.path
expect(assigns(:badge_api_endpoint)).not_to be_nil
end
end
describe "#update" do
render_views

View file

@ -14,7 +14,7 @@ describe 'Group Badges' do
group.add_owner(user)
sign_in(user)
visit(group_settings_badges_path(group))
visit(edit_group_path(group))
end
it 'shows a list of badges', :js do

View file

@ -15,7 +15,7 @@ describe 'Project Badges' do
group.add_maintainer(user)
sign_in(user)
visit(project_settings_badges_path(project))
visit(edit_project_path(project))
end
it 'shows a list of badges', :js do

View file

@ -1,21 +1,31 @@
import Vue from 'vue';
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import store from '~/badges/store';
import createEmptyBadge from '~/badges/empty_badge';
import BadgeForm from '~/badges/components/badge_form.vue';
import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import { createDummyBadge } from '../dummy_badge';
import { DUMMY_IMAGE_URL, TEST_HOST } from '../../test_constants';
// avoid preview background process
BadgeForm.methods.debouncedPreview = () => {};
describe('BadgeForm component', () => {
const Component = Vue.extend(BadgeForm);
let axiosMock;
let vm;
beforeEach(() => {
setFixtures(`
<div id="dummy-element"></div>
`);
axiosMock = new MockAdapter(axios);
});
afterEach(() => {
vm.$destroy();
axiosMock.restore();
});
describe('methods', () => {
@ -38,94 +48,87 @@ describe('BadgeForm component', () => {
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();
});
});
});
});
const sharedSubmitTests = submitAction => {
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 inputElement = vm.$el.querySelector(inputElementSelector);
inputElement.value = url;
inputElement.dispatchEvent(new Event('input'));
};
const submitForm = () => {
const submitButton = vm.$el.querySelector('button[type="submit"]');
submitButton.click();
};
const expectInvalidInput = inputElementSelector => {
const inputElement = vm.$el.querySelector(inputElementSelector);
expect(inputElement).toBeMatchedBy(':invalid');
const feedbackElement = vm.$el.querySelector(`${inputElementSelector} + .invalid-feedback`);
expect(feedbackElement).toBeVisible();
};
beforeEach(() => {
spyOn(vm, submitAction).and.returnValue(Promise.resolve());
store.replaceState({
...store.state,
badgeInAddForm: createEmptyBadge(),
badgeInEditForm: createEmptyBadge(),
isSaving: false,
});
setValue(linkUrlSelector, `${TEST_HOST}/link/url`);
setValue(imageUrlSelector, `${window.location.origin}${DUMMY_IMAGE_URL}`);
});
it('returns immediately if imageUrl is empty', () => {
setValue(imageUrlSelector, '');
submitForm();
expectInvalidInput(imageUrlSelector);
expect(vm[submitAction]).not.toHaveBeenCalled();
});
it('returns immediately if imageUrl is malformed', () => {
setValue(imageUrlSelector, 'not-a-url');
submitForm();
expectInvalidInput(imageUrlSelector);
expect(vm[submitAction]).not.toHaveBeenCalled();
});
it('returns immediately if linkUrl is empty', () => {
setValue(linkUrlSelector, '');
submitForm();
expectInvalidInput(linkUrlSelector);
expect(vm[submitAction]).not.toHaveBeenCalled();
});
it('returns immediately if linkUrl is malformed', () => {
setValue(linkUrlSelector, 'not-a-url');
submitForm();
expectInvalidInput(linkUrlSelector);
expect(vm[submitAction]).not.toHaveBeenCalled();
});
it(`calls ${submitAction}`, () => {
submitForm();
expect(findImageUrlElement()).toBeMatchedBy(':valid');
expect(findLinkUrlElement()).toBeMatchedBy(':valid');
expect(vm[submitAction]).toHaveBeenCalled();
});
};
describe('if isEditing is false', () => {
beforeEach(() => {
vm = mountComponentWithStore(Component, {
@ -138,12 +141,15 @@ describe('BadgeForm component', () => {
});
it('renders one button', () => {
const buttons = vm.$el.querySelectorAll('.row-content-block button');
expect(vm.$el.querySelector('.row-content-block')).toBeNull();
const buttons = vm.$el.querySelectorAll('.form-group:last-of-type button');
expect(buttons.length).toBe(1);
const buttonAddElement = buttons[0];
expect(buttonAddElement).toBeVisible();
expect(buttonAddElement).toHaveText('Add badge');
});
sharedSubmitTests('addBadge');
});
describe('if isEditing is true', () => {
@ -167,5 +173,7 @@ describe('BadgeForm component', () => {
expect(buttonCancelElement).toBeVisible();
expect(buttonCancelElement).toHaveText('Cancel');
});
sharedSubmitTests('saveBadge');
});
});