Commit c3e79104 authored by Bob Van Landuyt's avatar Bob Van Landuyt

Merge branch 'justin_ho-extract-integration-trigger-fields-to-vue' into 'master'

Extract integration trigger fields to Vue

See merge request gitlab-org/gitlab!31074
parents 2d3117cf 1395fd64
<script> <script>
import ActiveToggle from './active_toggle.vue'; import ActiveToggle from './active_toggle.vue';
import JiraTriggerFields from './jira_trigger_fields.vue'; import JiraTriggerFields from './jira_trigger_fields.vue';
import TriggerFields from './trigger_fields.vue';
export default { export default {
name: 'IntegrationForm', name: 'IntegrationForm',
components: { components: {
ActiveToggle, ActiveToggle,
JiraTriggerFields, JiraTriggerFields,
TriggerFields,
}, },
props: { props: {
activeToggleProps: { activeToggleProps: {
...@@ -21,6 +23,11 @@ export default { ...@@ -21,6 +23,11 @@ export default {
type: Object, type: Object,
required: true, required: true,
}, },
triggerEvents: {
type: Array,
required: false,
default: () => [],
},
type: { type: {
type: String, type: String,
required: true, required: true,
...@@ -38,5 +45,6 @@ export default { ...@@ -38,5 +45,6 @@ export default {
<div> <div>
<active-toggle v-if="showActive" v-bind="activeToggleProps" /> <active-toggle v-if="showActive" v-bind="activeToggleProps" />
<jira-trigger-fields v-if="isJira" v-bind="triggerFieldsProps" /> <jira-trigger-fields v-if="isJira" v-bind="triggerFieldsProps" />
<trigger-fields v-else-if="triggerEvents.length" :events="triggerEvents" :type="type" />
</div> </div>
</template> </template>
<script>
import { startCase } from 'lodash';
import { __ } from '~/locale';
import { GlFormGroup, GlFormCheckbox, GlFormInput } from '@gitlab/ui';
const typeWithPlaceholder = {
SLACK: 'slack',
MATTERMOST: 'mattermost',
};
const placeholderForType = {
[typeWithPlaceholder.SLACK]: __('Slack channels (e.g. general, development)'),
[typeWithPlaceholder.MATTERMOST]: __('Channel handle (e.g. town-square)'),
};
export default {
name: 'TriggerFields',
components: {
GlFormGroup,
GlFormCheckbox,
GlFormInput,
},
props: {
events: {
type: Array,
required: false,
default: null,
},
type: {
type: String,
required: true,
},
},
computed: {
placeholder() {
return placeholderForType[this.type];
},
},
methods: {
checkboxName(name) {
return `service[${name}]`;
},
fieldName(name) {
return `service[${name}]`;
},
startCase,
},
};
</script>
<template>
<gl-form-group
class="gl-pt-3"
:label="__('Trigger')"
label-for="trigger-fields"
data-testid="trigger-fields-group"
>
<div id="trigger-fields" class="gl-pt-3">
<gl-form-group v-for="event in events" :key="event.title" :description="event.description">
<input :name="checkboxName(event.name)" type="hidden" value="false" />
<gl-form-checkbox v-model="event.value" :name="checkboxName(event.name)">
{{ startCase(event.title) }}
</gl-form-checkbox>
<gl-form-input
v-if="event.field"
v-model="event.field.value"
:name="fieldName(event.field.name)"
:placeholder="placeholder"
/>
</gl-form-group>
</div>
</gl-form-group>
</template>
...@@ -15,7 +15,7 @@ export default el => { ...@@ -15,7 +15,7 @@ export default el => {
return result; return result;
} }
const { type, commentDetail, ...booleanAttributes } = el.dataset; const { type, commentDetail, triggerEvents, ...booleanAttributes } = el.dataset;
const { const {
showActive, showActive,
activated, activated,
...@@ -40,6 +40,7 @@ export default el => { ...@@ -40,6 +40,7 @@ export default el => {
initialEnableComments: enableComments, initialEnableComments: enableComments,
initialCommentDetail: commentDetail, initialCommentDetail: commentDetail,
}, },
triggerEvents: JSON.parse(triggerEvents),
}, },
}); });
}, },
......
# frozen_string_literal: true
class ServiceEventEntity < Grape::Entity
include RequestAwareEntity
expose :title do |event|
event
end
expose :event_field_name, as: :name
expose :value do |event|
service[event_field_name]
end
expose :description do |event|
service.class.event_description(event)
end
expose :field, if: -> (_, _) { event_field } do
expose :name do |event|
event_field[:name]
end
expose :value do |event|
service.public_send(event_field[:name]) # rubocop:disable GitlabSecurity/PublicSend
end
end
private
alias_method :event, :object
def event_field_name
ServicesHelper.service_event_field_name(event)
end
def event_field
@event_field ||= service.event_field(event)
end
def service
request.service
end
end
# frozen_string_literal: true
class ServiceEventSerializer < BaseSerializer
entity ServiceEventEntity
end
.row.prepend-top-default.append-bottom-default .row.prepend-top-default.append-bottom-default
.col-lg-3 .col-lg-4
%h4.prepend-top-0 %h4.prepend-top-0
= @service.title = @service.title
- [true, false].each do |value| - [true, false].each do |value|
...@@ -9,7 +9,7 @@ ...@@ -9,7 +9,7 @@
- if @service.respond_to?(:detailed_description) - if @service.respond_to?(:detailed_description)
%p= @service.detailed_description %p= @service.detailed_description
.col-lg-9 .col-lg-8
= form_for(@service, as: :service, url: scoped_integration_path(@service), method: :put, html: { class: 'gl-show-field-errors integration-settings-form js-integration-settings-form', data: { 'can-test' => @service.can_test?, 'test-url' => test_project_service_path(@project, @service) } }) do |form| = form_for(@service, as: :service, url: scoped_integration_path(@service), method: :put, html: { class: 'gl-show-field-errors integration-settings-form js-integration-settings-form', data: { 'can-test' => @service.can_test?, 'test-url' => test_project_service_path(@project, @service) } }) do |form|
= render 'shared/service_settings', form: form, service: @service = render 'shared/service_settings', form: form, service: @service
.footer-block.row-content-block .footer-block.row-content-block
......
- breadcrumb_title @service.title - breadcrumb_title @service.title
- add_to_breadcrumbs _('Integration Settings'), project_settings_integrations_path(@project) - add_to_breadcrumbs _('Integration Settings'), project_settings_integrations_path(@project)
- page_title @service.title, _('Integrations') - page_title @service.title, _('Integrations')
- @content_class = 'limit-container-width' unless fluid_layout
= render 'form' = render 'form'
- if @web_hook_logs - if @web_hook_logs
......
= form_errors(@service) = form_errors(@service)
- trigger_events = Feature.enabled?(:integration_form_refactor) ? ServiceEventSerializer.new(service: @service).represent(@service.configurable_events).to_json : []
- if lookup_context.template_exists?('help', "projects/services/#{@service.to_param}", true) - if lookup_context.template_exists?('help', "projects/services/#{@service.to_param}", true)
= render "projects/services/#{@service.to_param}/help", subject: @service = render "projects/services/#{@service.to_param}/help", subject: @service
...@@ -9,9 +10,9 @@ ...@@ -9,9 +10,9 @@
.service-settings .service-settings
.js-vue-integration-settings{ data: { show_active: @service.show_active_box?.to_s, activated: (@service.active || @service.new_record?).to_s, type: @service.to_param, merge_request_events: @service.merge_requests_events.to_s, .js-vue-integration-settings{ data: { show_active: @service.show_active_box?.to_s, activated: (@service.active || @service.new_record?).to_s, type: @service.to_param, merge_request_events: @service.merge_requests_events.to_s,
commit_events: @service.commit_events.to_s, enable_comments: @service.comment_on_event_enabled.to_s, comment_detail: @service.comment_detail } } commit_events: @service.commit_events.to_s, enable_comments: @service.comment_on_event_enabled.to_s, comment_detail: @service.comment_detail, trigger_events: trigger_events } }
- if @service.configurable_events.present? && !@service.is_a?(JiraService) - if @service.configurable_events.present? && !@service.is_a?(JiraService) && Feature.disabled?(:integration_form_refactor)
.form-group.row .form-group.row
%label.col-form-label.col-sm-2= _('Trigger') %label.col-form-label.col-sm-2= _('Trigger')
...@@ -33,15 +34,4 @@ commit_events: @service.commit_events.to_s, enable_comments: @service.comment_on ...@@ -33,15 +34,4 @@ commit_events: @service.commit_events.to_s, enable_comments: @service.comment_on
= @service.class.event_description(event) = @service.class.event_description(event)
- @service.global_fields.each do |field| - @service.global_fields.each do |field|
- type = field[:type] = render 'shared/field', form: form, field: field
- if type == 'fieldset'
- fields = field[:fields]
- legend = field[:legend]
%fieldset
%legend= legend
- fields.each do |subfield|
= render 'shared/field', form: form, field: subfield
- else
= render 'shared/field', form: form, field: field
...@@ -14,13 +14,6 @@ FactoryBot.define do ...@@ -14,13 +14,6 @@ FactoryBot.define do
type { 'GithubService' } type { 'GithubService' }
end end
factory :slack_service do
project
active { true }
webhook { 'https://slack.service.url' }
type { 'SlackService' }
end
factory :slack_slash_commands_service do factory :slack_slash_commands_service do
project project
active { true } active { true }
......
...@@ -3797,6 +3797,9 @@ msgstr "" ...@@ -3797,6 +3797,9 @@ msgstr ""
msgid "Changing group path can have unintended side effects." msgid "Changing group path can have unintended side effects."
msgstr "" msgstr ""
msgid "Channel handle (e.g. town-square)"
msgstr ""
msgid "Charts" msgid "Charts"
msgstr "" msgstr ""
......
...@@ -158,6 +158,13 @@ FactoryBot.define do ...@@ -158,6 +158,13 @@ FactoryBot.define do
token { 'test_token' } token { 'test_token' }
end end
factory :slack_service do
project
active { true }
webhook { 'https://slack.service.url' }
type { 'SlackService' }
end
# this is for testing storing values inside properties, which is deprecated and will be removed in # this is for testing storing values inside properties, which is deprecated and will be removed in
# https://gitlab.com/gitlab-org/gitlab/issues/29404 # https://gitlab.com/gitlab-org/gitlab/issues/29404
trait :without_properties_callback do trait :without_properties_callback do
......
...@@ -212,12 +212,12 @@ describe 'Admin updates settings', :clean_gitlab_redis_shared_state, :do_not_moc ...@@ -212,12 +212,12 @@ describe 'Admin updates settings', :clean_gitlab_redis_shared_state, :do_not_moc
expect(current_settings.hide_third_party_offers).to be true expect(current_settings.hide_third_party_offers).to be true
end end
it 'Change Slack Notifications Service template settings' do it 'Change Slack Notifications Service template settings', :js do
first(:link, 'Service Templates').click first(:link, 'Service Templates').click
click_link 'Slack notifications' click_link 'Slack notifications'
fill_in 'Webhook', with: 'http://localhost' fill_in 'Webhook', with: 'http://localhost'
fill_in 'Username', with: 'test_user' fill_in 'Username', with: 'test_user'
fill_in 'service_push_channel', with: '#test_channel' fill_in 'service[push_channel]', with: '#test_channel'
page.check('Notify only broken pipelines') page.check('Notify only broken pipelines')
page.select 'All branches', from: 'Branches to be notified' page.select 'All branches', from: 'Branches to be notified'
...@@ -231,10 +231,10 @@ describe 'Admin updates settings', :clean_gitlab_redis_shared_state, :do_not_moc ...@@ -231,10 +231,10 @@ describe 'Admin updates settings', :clean_gitlab_redis_shared_state, :do_not_moc
expect(page.all('input[type=checkbox]')).to all(be_checked) expect(page.all('input[type=checkbox]')).to all(be_checked)
expect(find_field('Webhook').value).to eq 'http://localhost' expect(find_field('Webhook').value).to eq 'http://localhost'
expect(find_field('Username').value).to eq 'test_user' expect(find_field('Username').value).to eq 'test_user'
expect(find('#service_push_channel').value).to eq '#test_channel' expect(find('[name="service[push_channel]"]').value).to eq '#test_channel'
end end
it 'defaults Deployment events to false for chat notification template settings' do it 'defaults Deployment events to false for chat notification template settings', :js do
first(:link, 'Service Templates').click first(:link, 'Service Templates').click
click_link 'Slack notifications' click_link 'Slack notifications'
...@@ -500,13 +500,13 @@ describe 'Admin updates settings', :clean_gitlab_redis_shared_state, :do_not_moc ...@@ -500,13 +500,13 @@ describe 'Admin updates settings', :clean_gitlab_redis_shared_state, :do_not_moc
def check_all_events def check_all_events
page.check('Push') page.check('Push')
page.check('Issue') page.check('Issue')
page.check('Confidential issue') page.check('Confidential Issue')
page.check('Merge request') page.check('Merge Request')
page.check('Note') page.check('Note')
page.check('Confidential note') page.check('Confidential Note')
page.check('Tag push') page.check('Tag Push')
page.check('Pipeline') page.check('Pipeline')
page.check('Wiki page') page.check('Wiki Page')
page.check('Deployment') page.check('Deployment')
end end
......
...@@ -18,7 +18,10 @@ describe('ActiveToggle', () => { ...@@ -18,7 +18,10 @@ describe('ActiveToggle', () => {
}; };
afterEach(() => { afterEach(() => {
if (wrapper) wrapper.destroy(); if (wrapper) {
wrapper.destroy();
wrapper = null;
}
}); });
const findGlToggle = () => wrapper.find(GlToggle); const findGlToggle = () => wrapper.find(GlToggle);
......
...@@ -2,6 +2,7 @@ import { shallowMount } from '@vue/test-utils'; ...@@ -2,6 +2,7 @@ import { shallowMount } from '@vue/test-utils';
import IntegrationForm from '~/integrations/edit/components/integration_form.vue'; import IntegrationForm from '~/integrations/edit/components/integration_form.vue';
import ActiveToggle from '~/integrations/edit/components/active_toggle.vue'; import ActiveToggle from '~/integrations/edit/components/active_toggle.vue';
import JiraTriggerFields from '~/integrations/edit/components/jira_trigger_fields.vue'; import JiraTriggerFields from '~/integrations/edit/components/jira_trigger_fields.vue';
import TriggerFields from '~/integrations/edit/components/trigger_fields.vue';
describe('IntegrationForm', () => { describe('IntegrationForm', () => {
let wrapper; let wrapper;
...@@ -38,6 +39,7 @@ describe('IntegrationForm', () => { ...@@ -38,6 +39,7 @@ describe('IntegrationForm', () => {
const findActiveToggle = () => wrapper.find(ActiveToggle); const findActiveToggle = () => wrapper.find(ActiveToggle);
const findJiraTriggerFields = () => wrapper.find(JiraTriggerFields); const findJiraTriggerFields = () => wrapper.find(JiraTriggerFields);
const findTriggerFields = () => wrapper.find(TriggerFields);
describe('template', () => { describe('template', () => {
describe('showActive is true', () => { describe('showActive is true', () => {
...@@ -77,5 +79,21 @@ describe('IntegrationForm', () => { ...@@ -77,5 +79,21 @@ describe('IntegrationForm', () => {
expect(findJiraTriggerFields().exists()).toBe(true); expect(findJiraTriggerFields().exists()).toBe(true);
}); });
}); });
describe('triggerEvents is present', () => {
it('renders TriggerFields', () => {
const events = [{ title: 'push' }];
const type = 'slack';
createComponent({
triggerEvents: events,
type,
});
expect(findTriggerFields().exists()).toBe(true);
expect(findTriggerFields().props('events')).toBe(events);
expect(findTriggerFields().props('type')).toBe(type);
});
});
}); });
}); });
...@@ -18,7 +18,10 @@ describe('JiraTriggerFields', () => { ...@@ -18,7 +18,10 @@ describe('JiraTriggerFields', () => {
}; };
afterEach(() => { afterEach(() => {
if (wrapper) wrapper.destroy(); if (wrapper) {
wrapper.destroy();
wrapper = null;
}
}); });
const findCommentSettings = () => wrapper.find('[data-testid="comment-settings"]'); const findCommentSettings = () => wrapper.find('[data-testid="comment-settings"]');
......
import { mount } from '@vue/test-utils';
import TriggerFields from '~/integrations/edit/components/trigger_fields.vue';
import { GlFormGroup, GlFormCheckbox, GlFormInput } from '@gitlab/ui';
describe('TriggerFields', () => {
let wrapper;
const defaultProps = {
type: 'slack',
};
const createComponent = props => {
wrapper = mount(TriggerFields, {
propsData: { ...defaultProps, ...props },
});
};
afterEach(() => {
if (wrapper) {
wrapper.destroy();
wrapper = null;
}
});
const findAllGlFormCheckboxes = () => wrapper.findAll(GlFormCheckbox);
const findAllGlFormInputs = () => wrapper.findAll(GlFormInput);
describe('template', () => {
it('renders a label with text "Trigger"', () => {
createComponent();
const triggerLabel = wrapper.find('[data-testid="trigger-fields-group"]').find('label');
expect(triggerLabel.exists()).toBe(true);
expect(triggerLabel.text()).toBe('Trigger');
});
describe('events without field property', () => {
const events = [
{
title: 'push',
name: 'push_event',
description: 'Event on push',
value: true,
},
{
title: 'merge_request',
name: 'merge_requests_event',
description: 'Event on merge_request',
value: false,
},
];
beforeEach(() => {
createComponent({
events,
});
});
it('does not render GlFormInput for each event', () => {
expect(findAllGlFormInputs().exists()).toBe(false);
});
it('renders GlFormInput with description for each event', () => {
const groups = wrapper.find('#trigger-fields').findAll(GlFormGroup);
expect(groups).toHaveLength(2);
groups.wrappers.forEach((group, index) => {
expect(group.find('small').text()).toBe(events[index].description);
});
});
it('renders GlFormCheckbox for each event', () => {
const checkboxes = findAllGlFormCheckboxes();
const expectedResults = [
{ labelText: 'Push', inputName: 'service[push_event]' },
{ labelText: 'Merge Request', inputName: 'service[merge_requests_event]' },
];
expect(checkboxes).toHaveLength(2);
checkboxes.wrappers.forEach((checkbox, index) => {
expect(checkbox.find('label').text()).toBe(expectedResults[index].labelText);
expect(checkbox.find('input').attributes('name')).toBe(expectedResults[index].inputName);
expect(checkbox.vm.$attrs.checked).toBe(events[index].value);
});
});
});
describe('events with field property', () => {
const events = [
{
field: {
name: 'push_channel',
value: '',
},
},
{
field: {
name: 'merge_request_channel',
value: 'gitlab-development',
},
},
];
beforeEach(() => {
createComponent({
events,
});
});
it('renders GlFormCheckbox for each event', () => {
expect(findAllGlFormCheckboxes()).toHaveLength(2);
});
it('renders GlFormInput for each event', () => {
const fields = findAllGlFormInputs();
const expectedResults = [
{
name: 'service[push_channel]',
placeholder: 'Slack channels (e.g. general, development)',
},
{
name: 'service[merge_request_channel]',
placeholder: 'Slack channels (e.g. general, development)',
},
];
expect(fields).toHaveLength(2);
fields.wrappers.forEach((field, index) => {
expect(field.attributes()).toMatchObject(expectedResults[index]);
expect(field.vm.$attrs.value).toBe(events[index].field.value);
});
});
});
});
});
# frozen_string_literal: true
require 'spec_helper'
describe ServiceEventEntity do
let(:request) { double('request') }
subject { described_class.new(event, request: request, service: service).as_json }
before do
allow(request).to receive(:service).and_return(service)
end
describe '#as_json' do
context 'service without fields' do
let(:service) { create(:emails_on_push_service, push_events: true) }
let(:event) { 'push' }
it 'exposes correct attributes' do
expect(subject[:description]).to eq('Event will be triggered by a push to the repository')
expect(subject[:name]).to eq('push_events')
expect(subject[:title]).to eq('push')
expect(subject[:value]).to be(true)
end
end
context 'service with fields' do
let(:service) { create(:slack_service, note_events: false, note_channel: 'note-channel') }
let(:event) { 'note' }
it 'exposes correct attributes' do
expect(subject[:description]).to eq('Event will be triggered when someone adds a comment')
expect(subject[:name]).to eq('note_events')
expect(subject[:title]).to eq('note')
expect(subject[:value]).to eq(false)
expect(subject[:field][:name]).to eq('note_channel')
expect(subject[:field][:value]).to eq('note-channel')
end
end
end
end
...@@ -5,6 +5,7 @@ shared_context 'project service activation' do ...@@ -5,6 +5,7 @@ shared_context 'project service activation' do
let(:user) { create(:user) } let(:user) { create(:user) }
before do before do
stub_feature_flags(integration_form_refactor: false)
project.add_maintainer(user) project.add_maintainer(user)
sign_in(user) sign_in(user)
end end
......
...@@ -7,6 +7,8 @@ describe 'projects/services/_form' do ...@@ -7,6 +7,8 @@ describe 'projects/services/_form' do
let(:user) { create(:admin) } let(:user) { create(:admin) }
before do before do
stub_feature_flags(integration_form_refactor: false)
assign(:project, project) assign(:project, project)
allow(controller).to receive(:current_user).and_return(user) allow(controller).to receive(:current_user).and_return(user)
......
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