Commit 27d052ae authored by Illya Klymov's avatar Illya Klymov

Merge branch '273269-settings-selector-bar-placement-should-span-entire-width-2' into 'master'

Render "Integration Help" HTML in HAML, but reposition it in Vue

See merge request gitlab-org/gitlab!49323
parents ad0ec60b 7bc2ec58
<script> <script>
import { mapState, mapActions, mapGetters } from 'vuex'; import { mapState, mapActions, mapGetters } from 'vuex';
import { GlButton, GlModalDirective } from '@gitlab/ui'; import { GlButton, GlModalDirective, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
import { integrationLevels } from '../constants'; import { integrationLevels } from '../constants';
...@@ -28,9 +28,17 @@ export default { ...@@ -28,9 +28,17 @@ export default {
GlButton, GlButton,
}, },
directives: { directives: {
'gl-modal': GlModalDirective, GlModal: GlModalDirective,
SafeHtml,
}, },
mixins: [glFeatureFlagsMixin()], mixins: [glFeatureFlagsMixin()],
props: {
helpHtml: {
type: String,
required: false,
default: '',
},
},
computed: { computed: {
...mapGetters(['currentKey', 'propsSource', 'isDisabled']), ...mapGetters(['currentKey', 'propsSource', 'isDisabled']),
...mapState([ ...mapState([
...@@ -80,11 +88,17 @@ export default { ...@@ -80,11 +88,17 @@ export default {
this.fetchResetIntegration(); this.fetchResetIntegration();
}, },
}, },
helpHtmlConfig: {
ADD_TAGS: ['use'], // to support icon SVGs
},
}; };
</script> </script>
<template> <template>
<div> <div>
<!-- helpHtml is trusted input -->
<div v-if="helpHtml" v-safe-html:[$options.helpHtmlConfig]="helpHtml"></div>
<override-dropdown <override-dropdown
v-if="defaultState !== null" v-if="defaultState !== null"
:inherit-from-id="defaultState.id" :inherit-from-id="defaultState.id"
...@@ -92,6 +106,7 @@ export default { ...@@ -92,6 +106,7 @@ export default {
:learn-more-path="propsSource.learnMorePath" :learn-more-path="propsSource.learnMorePath"
@change="setOverride" @change="setOverride"
/> />
<active-checkbox v-if="propsSource.showActive" :key="`${currentKey}-active-checkbox`" /> <active-checkbox v-if="propsSource.showActive" :key="`${currentKey}-active-checkbox`" />
<jira-trigger-fields <jira-trigger-fields
v-if="isJira" v-if="isJira"
......
...@@ -80,21 +80,29 @@ export default (el, defaultEl) => { ...@@ -80,21 +80,29 @@ export default (el, defaultEl) => {
} }
const props = parseDatasetToProps(el.dataset); const props = parseDatasetToProps(el.dataset);
const initialState = { const initialState = {
defaultState: null, defaultState: null,
customState: props, customState: props,
}; };
if (defaultEl) { if (defaultEl) {
initialState.defaultState = Object.freeze(parseDatasetToProps(defaultEl.dataset)); initialState.defaultState = Object.freeze(parseDatasetToProps(defaultEl.dataset));
} }
// Here, we capture the "helpHtml", so we can pass it to the Vue component
// to position it where ever it wants.
// Because this node is a _child_ of `el`, it will be removed when the Vue component is mounted,
// so we don't need to manually remove it.
const helpHtml = el.querySelector('.js-integration-help-html')?.innerHTML;
return new Vue({ return new Vue({
el, el,
store: createStore(initialState), store: createStore(initialState),
render(createElement) { render(createElement) {
return createElement(IntegrationForm); return createElement(IntegrationForm, {
props: {
helpHtml,
},
});
}, },
}); });
}; };
= form_errors(integration) = form_errors(integration)
- if lookup_context.template_exists?('help', "projects/services/#{integration.to_param}", true)
= render "projects/services/#{integration.to_param}/help", subject: integration
- elsif integration.help.present?
.info-well
.well-segment
= markdown integration.help
.service-settings .service-settings
- if @default_integration - if @default_integration
.js-vue-default-integration-settings{ data: integration_form_data(@default_integration, group: @group) } .js-vue-default-integration-settings{ data: integration_form_data(@default_integration, group: @group) }
.js-vue-integration-settings{ data: integration_form_data(integration, group: @group) } .js-vue-integration-settings{ data: integration_form_data(integration, group: @group) }
.js-integration-help-html
-# All content below will be repositioned in Vue
- if lookup_context.template_exists?('help', "projects/services/#{integration.to_param}", true)
= render "projects/services/#{integration.to_param}/help", subject: integration
- elsif integration.help.present?
.info-well
.well-segment
= markdown integration.help
...@@ -40,4 +40,8 @@ RSpec.describe 'Slack slash commands', :js do ...@@ -40,4 +40,8 @@ RSpec.describe 'Slack slash commands', :js do
value = find_field('url').value value = find_field('url').value
expect(value).to match("api/v4/projects/#{project.id}/services/slack_slash_commands/trigger") expect(value).to match("api/v4/projects/#{project.id}/services/slack_slash_commands/trigger")
end end
it 'shows help content' do
expect(page).to have_content('This service allows users to perform common operations on this project by entering slash commands in Slack.')
end
end end
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import { mockIntegrationProps } from 'jest/integrations/edit/mock_data'; import { mockIntegrationProps } from 'jest/integrations/edit/mock_data';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import { setHTMLFixture } from 'helpers/fixtures';
import { createStore } from '~/integrations/edit/store'; import { createStore } from '~/integrations/edit/store';
import IntegrationForm from '~/integrations/edit/components/integration_form.vue'; import IntegrationForm from '~/integrations/edit/components/integration_form.vue';
import OverrideDropdown from '~/integrations/edit/components/override_dropdown.vue'; import OverrideDropdown from '~/integrations/edit/components/override_dropdown.vue';
...@@ -15,24 +17,31 @@ import { integrationLevels } from '~/integrations/edit/constants'; ...@@ -15,24 +17,31 @@ import { integrationLevels } from '~/integrations/edit/constants';
describe('IntegrationForm', () => { describe('IntegrationForm', () => {
let wrapper; let wrapper;
const createComponent = (customStateProps = {}, featureFlags = {}, initialState = {}) => { const createComponent = ({
wrapper = shallowMount(IntegrationForm, { customStateProps = {},
propsData: {}, featureFlags = {},
store: createStore({ initialState = {},
customState: { ...mockIntegrationProps, ...customStateProps }, props = {},
...initialState, } = {}) => {
wrapper = extendedWrapper(
shallowMount(IntegrationForm, {
propsData: { ...props },
store: createStore({
customState: { ...mockIntegrationProps, ...customStateProps },
...initialState,
}),
stubs: {
OverrideDropdown,
ActiveCheckbox,
ConfirmationModal,
JiraTriggerFields,
TriggerFields,
},
provide: {
glFeatures: featureFlags,
},
}), }),
stubs: { );
OverrideDropdown,
ActiveCheckbox,
ConfirmationModal,
JiraTriggerFields,
TriggerFields,
},
provide: {
glFeatures: featureFlags,
},
});
}; };
afterEach(() => { afterEach(() => {
...@@ -63,7 +72,9 @@ describe('IntegrationForm', () => { ...@@ -63,7 +72,9 @@ describe('IntegrationForm', () => {
describe('showActive is false', () => { describe('showActive is false', () => {
it('does not render ActiveCheckbox', () => { it('does not render ActiveCheckbox', () => {
createComponent({ createComponent({
showActive: false, customStateProps: {
showActive: false,
},
}); });
expect(findActiveCheckbox().exists()).toBe(false); expect(findActiveCheckbox().exists()).toBe(false);
...@@ -73,7 +84,9 @@ describe('IntegrationForm', () => { ...@@ -73,7 +84,9 @@ describe('IntegrationForm', () => {
describe('integrationLevel is instance', () => { describe('integrationLevel is instance', () => {
it('renders ConfirmationModal', () => { it('renders ConfirmationModal', () => {
createComponent({ createComponent({
integrationLevel: integrationLevels.INSTANCE, customStateProps: {
integrationLevel: integrationLevels.INSTANCE,
},
}); });
expect(findConfirmationModal().exists()).toBe(true); expect(findConfirmationModal().exists()).toBe(true);
...@@ -82,7 +95,9 @@ describe('IntegrationForm', () => { ...@@ -82,7 +95,9 @@ describe('IntegrationForm', () => {
describe('resetPath is empty', () => { describe('resetPath is empty', () => {
it('does not render ResetConfirmationModal and button', () => { it('does not render ResetConfirmationModal and button', () => {
createComponent({ createComponent({
integrationLevel: integrationLevels.INSTANCE, customStateProps: {
integrationLevel: integrationLevels.INSTANCE,
},
}); });
expect(findResetButton().exists()).toBe(false); expect(findResetButton().exists()).toBe(false);
...@@ -93,8 +108,10 @@ describe('IntegrationForm', () => { ...@@ -93,8 +108,10 @@ describe('IntegrationForm', () => {
describe('resetPath is present', () => { describe('resetPath is present', () => {
it('renders ResetConfirmationModal and button', () => { it('renders ResetConfirmationModal and button', () => {
createComponent({ createComponent({
integrationLevel: integrationLevels.INSTANCE, customStateProps: {
resetPath: 'resetPath', integrationLevel: integrationLevels.INSTANCE,
resetPath: 'resetPath',
},
}); });
expect(findResetButton().exists()).toBe(true); expect(findResetButton().exists()).toBe(true);
...@@ -106,7 +123,9 @@ describe('IntegrationForm', () => { ...@@ -106,7 +123,9 @@ describe('IntegrationForm', () => {
describe('integrationLevel is group', () => { describe('integrationLevel is group', () => {
it('renders ConfirmationModal', () => { it('renders ConfirmationModal', () => {
createComponent({ createComponent({
integrationLevel: integrationLevels.GROUP, customStateProps: {
integrationLevel: integrationLevels.GROUP,
},
}); });
expect(findConfirmationModal().exists()).toBe(true); expect(findConfirmationModal().exists()).toBe(true);
...@@ -115,7 +134,9 @@ describe('IntegrationForm', () => { ...@@ -115,7 +134,9 @@ describe('IntegrationForm', () => {
describe('resetPath is empty', () => { describe('resetPath is empty', () => {
it('does not render ResetConfirmationModal and button', () => { it('does not render ResetConfirmationModal and button', () => {
createComponent({ createComponent({
integrationLevel: integrationLevels.GROUP, customStateProps: {
integrationLevel: integrationLevels.GROUP,
},
}); });
expect(findResetButton().exists()).toBe(false); expect(findResetButton().exists()).toBe(false);
...@@ -126,8 +147,10 @@ describe('IntegrationForm', () => { ...@@ -126,8 +147,10 @@ describe('IntegrationForm', () => {
describe('resetPath is present', () => { describe('resetPath is present', () => {
it('renders ResetConfirmationModal and button', () => { it('renders ResetConfirmationModal and button', () => {
createComponent({ createComponent({
integrationLevel: integrationLevels.GROUP, customStateProps: {
resetPath: 'resetPath', integrationLevel: integrationLevels.GROUP,
resetPath: 'resetPath',
},
}); });
expect(findResetButton().exists()).toBe(true); expect(findResetButton().exists()).toBe(true);
...@@ -139,7 +162,9 @@ describe('IntegrationForm', () => { ...@@ -139,7 +162,9 @@ describe('IntegrationForm', () => {
describe('integrationLevel is project', () => { describe('integrationLevel is project', () => {
it('does not render ConfirmationModal', () => { it('does not render ConfirmationModal', () => {
createComponent({ createComponent({
integrationLevel: 'project', customStateProps: {
integrationLevel: 'project',
},
}); });
expect(findConfirmationModal().exists()).toBe(false); expect(findConfirmationModal().exists()).toBe(false);
...@@ -147,8 +172,10 @@ describe('IntegrationForm', () => { ...@@ -147,8 +172,10 @@ describe('IntegrationForm', () => {
it('does not render ResetConfirmationModal and button', () => { it('does not render ResetConfirmationModal and button', () => {
createComponent({ createComponent({
integrationLevel: 'project', customStateProps: {
resetPath: 'resetPath', integrationLevel: 'project',
resetPath: 'resetPath',
},
}); });
expect(findResetButton().exists()).toBe(false); expect(findResetButton().exists()).toBe(false);
...@@ -158,7 +185,9 @@ describe('IntegrationForm', () => { ...@@ -158,7 +185,9 @@ describe('IntegrationForm', () => {
describe('type is "slack"', () => { describe('type is "slack"', () => {
beforeEach(() => { beforeEach(() => {
createComponent({ type: 'slack' }); createComponent({
customStateProps: { type: 'slack' },
});
}); });
it('does not render JiraTriggerFields', () => { it('does not render JiraTriggerFields', () => {
...@@ -172,14 +201,19 @@ describe('IntegrationForm', () => { ...@@ -172,14 +201,19 @@ describe('IntegrationForm', () => {
describe('type is "jira"', () => { describe('type is "jira"', () => {
it('renders JiraTriggerFields', () => { it('renders JiraTriggerFields', () => {
createComponent({ type: 'jira' }); createComponent({
customStateProps: { type: 'jira' },
});
expect(findJiraTriggerFields().exists()).toBe(true); expect(findJiraTriggerFields().exists()).toBe(true);
}); });
describe('featureFlag jiraIssuesIntegration is false', () => { describe('featureFlag jiraIssuesIntegration is false', () => {
it('does not render JiraIssuesFields', () => { it('does not render JiraIssuesFields', () => {
createComponent({ type: 'jira' }, { jiraIssuesIntegration: false }); createComponent({
customStateProps: { type: 'jira' },
featureFlags: { jiraIssuesIntegration: false },
});
expect(findJiraIssuesFields().exists()).toBe(false); expect(findJiraIssuesFields().exists()).toBe(false);
}); });
...@@ -187,8 +221,10 @@ describe('IntegrationForm', () => { ...@@ -187,8 +221,10 @@ describe('IntegrationForm', () => {
describe('featureFlag jiraIssuesIntegration is true', () => { describe('featureFlag jiraIssuesIntegration is true', () => {
it('renders JiraIssuesFields', () => { it('renders JiraIssuesFields', () => {
createComponent({ type: 'jira' }, { jiraIssuesIntegration: true }); createComponent({
customStateProps: { type: 'jira' },
featureFlags: { jiraIssuesIntegration: true },
});
expect(findJiraIssuesFields().exists()).toBe(true); expect(findJiraIssuesFields().exists()).toBe(true);
}); });
}); });
...@@ -200,8 +236,10 @@ describe('IntegrationForm', () => { ...@@ -200,8 +236,10 @@ describe('IntegrationForm', () => {
const type = 'slack'; const type = 'slack';
createComponent({ createComponent({
triggerEvents: events, customStateProps: {
type, triggerEvents: events,
type,
},
}); });
expect(findTriggerFields().exists()).toBe(true); expect(findTriggerFields().exists()).toBe(true);
...@@ -218,7 +256,9 @@ describe('IntegrationForm', () => { ...@@ -218,7 +256,9 @@ describe('IntegrationForm', () => {
]; ];
createComponent({ createComponent({
fields, customStateProps: {
fields,
},
}); });
const dynamicFields = wrapper.findAll(DynamicField); const dynamicFields = wrapper.findAll(DynamicField);
...@@ -232,13 +272,11 @@ describe('IntegrationForm', () => { ...@@ -232,13 +272,11 @@ describe('IntegrationForm', () => {
describe('defaultState state is null', () => { describe('defaultState state is null', () => {
it('does not render OverrideDropdown', () => { it('does not render OverrideDropdown', () => {
createComponent( createComponent({
{}, initialState: {
{},
{
defaultState: null, defaultState: null,
}, },
); });
expect(findOverrideDropdown().exists()).toBe(false); expect(findOverrideDropdown().exists()).toBe(false);
}); });
...@@ -246,18 +284,43 @@ describe('IntegrationForm', () => { ...@@ -246,18 +284,43 @@ describe('IntegrationForm', () => {
describe('defaultState state is an object', () => { describe('defaultState state is an object', () => {
it('renders OverrideDropdown', () => { it('renders OverrideDropdown', () => {
createComponent( createComponent({
{}, initialState: {
{},
{
defaultState: { defaultState: {
...mockIntegrationProps, ...mockIntegrationProps,
}, },
}, },
); });
expect(findOverrideDropdown().exists()).toBe(true); expect(findOverrideDropdown().exists()).toBe(true);
}); });
}); });
describe('with `helpHtml` prop', () => {
const mockTestId = 'jest-help-html-test';
setHTMLFixture(`
<div data-testid="${mockTestId}">
<svg class="gl-icon">
<use></use>
</svg>
</div>
`);
it('renders `helpHtml`', async () => {
const mockHelpHtml = document.querySelector(`[data-testid="${mockTestId}"]`);
createComponent({
props: {
helpHtml: mockHelpHtml.outerHTML,
},
});
const helpHtml = wrapper.findByTestId(mockTestId);
expect(helpHtml.isVisible()).toBe(true);
expect(helpHtml.find('svg').isVisible()).toBe(true);
});
});
}); });
}); });
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