Commit 51c05183 authored by Daniel Tian's avatar Daniel Tian Committed by Andrew Fontaine

Switch to GlTabs for markdown header component

MR:
Changelog: changed
parent 63c25c73
<script>
import { GlPopover, GlButton, GlTooltipDirective } from '@gitlab/ui';
import { GlPopover, GlButton, GlTooltipDirective, GlTabs, GlTab } from '@gitlab/ui';
import $ from 'jquery';
import { keysFor, BOLD_TEXT, ITALIC_TEXT, LINK_TEXT } from '~/behaviors/shortcuts/keybindings';
import { getSelectedFragment } from '~/lib/utils/common_utils';
import { s__ } from '~/locale';
import { s__, __ } from '~/locale';
import { CopyAsGFM } from '../../../behaviors/markdown/copy_as_gfm';
import ToolbarButton from './toolbar_button.vue';
......@@ -12,6 +12,8 @@ export default {
ToolbarButton,
GlPopover,
GlButton,
GlTabs,
GlTab,
},
directives: {
GlTooltip: GlTooltipDirective,
......@@ -144,136 +146,143 @@ export default {
italic: keysFor(ITALIC_TEXT),
link: keysFor(LINK_TEXT),
},
i18n: {
writeTabTitle: __('Write'),
previewTabTitle: __('Preview'),
},
};
</script>
<template>
<div class="md-header">
<ul class="nav-links clearfix">
<li :class="{ active: !previewMarkdown }" class="md-header-tab">
<button class="js-write-link" type="button" @click="writeMarkdownTab($event)">
{{ __('Write') }}
</button>
</li>
<li :class="{ active: previewMarkdown }" class="md-header-tab">
<button
class="js-preview-link js-md-preview-button"
type="button"
@click="previewMarkdownTab($event)"
>
{{ __('Preview') }}
</button>
</li>
<li :class="{ active: !previewMarkdown }" class="md-header-toolbar">
<toolbar-button
tag="**"
:button-title="
sprintf(s__('MarkdownEditor|Add bold text (%{modifierKey}B)'), { modifierKey })
"
:shortcuts="$options.shortcuts.bold"
icon="bold"
/>
<toolbar-button
tag="_"
:button-title="
sprintf(s__('MarkdownEditor|Add italic text (%{modifierKey}I)'), { modifierKey })
"
:shortcuts="$options.shortcuts.italic"
icon="italic"
/>
<toolbar-button
:prepend="true"
:tag="tag"
:button-title="__('Insert a quote')"
icon="quote"
@click="handleQuote"
/>
<template v-if="canSuggest">
<gl-tabs content-class="gl-display-none">
<gl-tab
title-link-class="gl-pt-3 gl-px-3 js-md-write-button"
:title="$options.i18n.writeTabTitle"
:active="!previewMarkdown"
data-testid="write-tab"
@click="writeMarkdownTab($event)"
/>
<gl-tab
title-link-class="gl-pt-3 gl-px-3 js-md-preview-button"
:title="$options.i18n.previewTabTitle"
:active="previewMarkdown"
data-testid="preview-tab"
@click="previewMarkdownTab($event)"
/>
<template v-if="!previewMarkdown" #tabs-end>
<div class="md-header-toolbar gl-ml-auto gl-pb-3 gl-justify-content-center">
<toolbar-button
tag="**"
:button-title="
sprintf(s__('MarkdownEditor|Add bold text (%{modifierKey}B)'), { modifierKey })
"
:shortcuts="$options.shortcuts.bold"
icon="bold"
/>
<toolbar-button
tag="_"
:button-title="
sprintf(s__('MarkdownEditor|Add italic text (%{modifierKey}I)'), { modifierKey })
"
:shortcuts="$options.shortcuts.italic"
icon="italic"
/>
<toolbar-button
ref="suggestButton"
:tag="mdSuggestion"
:prepend="true"
:button-title="__('Insert suggestion')"
:cursor-offset="4"
:tag-content="lineContent"
icon="doc-code"
data-qa-selector="suggestion_button"
class="js-suggestion-btn"
@click="handleSuggestDismissed"
:tag="tag"
:button-title="__('Insert a quote')"
icon="quote"
@click="handleQuote"
/>
<gl-popover
v-if="suggestPopoverVisible"
:target="$refs.suggestButton.$el"
:css-classes="['diff-suggest-popover']"
placement="bottom"
:show="suggestPopoverVisible"
>
<strong>{{ __('New! Suggest changes directly') }}</strong>
<p class="mb-2">
{{
__(
'Suggest code changes which can be immediately applied in one click. Try it out!',
)
}}
</p>
<gl-button
variant="info"
category="primary"
size="small"
<template v-if="canSuggest">
<toolbar-button
ref="suggestButton"
:tag="mdSuggestion"
:prepend="true"
:button-title="__('Insert suggestion')"
:cursor-offset="4"
:tag-content="lineContent"
icon="doc-code"
data-qa-selector="suggestion_button"
class="js-suggestion-btn"
@click="handleSuggestDismissed"
/>
<gl-popover
v-if="suggestPopoverVisible"
:target="$refs.suggestButton.$el"
:css-classes="['diff-suggest-popover']"
placement="bottom"
:show="suggestPopoverVisible"
>
{{ __('Got it') }}
</gl-button>
</gl-popover>
</template>
<toolbar-button tag="`" tag-block="```" :button-title="__('Insert code')" icon="code" />
<toolbar-button
tag="[{text}](url)"
tag-select="url"
:button-title="
sprintf(s__('MarkdownEditor|Add a link (%{modifierKey}K)'), { modifierKey })
"
:shortcuts="$options.shortcuts.link"
icon="link"
/>
<toolbar-button
:prepend="true"
tag="- "
:button-title="__('Add a bullet list')"
icon="list-bulleted"
/>
<toolbar-button
:prepend="true"
tag="1. "
:button-title="__('Add a numbered list')"
icon="list-numbered"
/>
<toolbar-button
:prepend="true"
tag="- [ ] "
:button-title="__('Add a task list')"
icon="list-task"
/>
<toolbar-button
:tag="mdCollapsibleSection"
:prepend="true"
tag-select="Click to expand"
:button-title="__('Add a collapsible section')"
icon="details-block"
/>
<toolbar-button
:tag="mdTable"
:prepend="true"
:button-title="__('Add a table')"
icon="table"
/>
<toolbar-button
class="js-zen-enter"
:prepend="true"
:button-title="__('Go full screen')"
icon="maximize"
/>
</li>
</ul>
<strong>{{ __('New! Suggest changes directly') }}</strong>
<p class="mb-2">
{{
__(
'Suggest code changes which can be immediately applied in one click. Try it out!',
)
}}
</p>
<gl-button
variant="info"
category="primary"
size="small"
@click="handleSuggestDismissed"
>
{{ __('Got it') }}
</gl-button>
</gl-popover>
</template>
<toolbar-button tag="`" tag-block="```" :button-title="__('Insert code')" icon="code" />
<toolbar-button
tag="[{text}](url)"
tag-select="url"
:button-title="
sprintf(s__('MarkdownEditor|Add a link (%{modifierKey}K)'), { modifierKey })
"
:shortcuts="$options.shortcuts.link"
icon="link"
/>
<toolbar-button
:prepend="true"
tag="- "
:button-title="__('Add a bullet list')"
icon="list-bulleted"
/>
<toolbar-button
:prepend="true"
tag="1. "
:button-title="__('Add a numbered list')"
icon="list-numbered"
/>
<toolbar-button
:prepend="true"
tag="- [ ] "
:button-title="__('Add a task list')"
icon="list-task"
/>
<toolbar-button
:tag="mdCollapsibleSection"
:prepend="true"
tag-select="Click to expand"
:button-title="__('Add a collapsible section')"
icon="details-block"
/>
<toolbar-button
:tag="mdTable"
:prepend="true"
:button-title="__('Add a table')"
icon="table"
/>
<toolbar-button
class="js-zen-enter"
:prepend="true"
:button-title="__('Go full screen')"
icon="maximize"
/>
</div>
</template>
</gl-tabs>
</div>
</template>
......@@ -67,6 +67,27 @@
}
}
}
.gl-tabs-nav {
@include media-breakpoint-down(xs) {
.nav-item {
flex: 1;
border-bottom: 1px solid $border-color;
}
.gl-tab-nav-item {
padding-top: $gl-padding-4;
padding-bottom: $gl-padding-8;
}
.md-header-toolbar {
width: 100%;
display: flex;
flex-wrap: wrap;
margin-top: $gl-padding-8;
}
}
}
}
.md-header-tab {
......
......@@ -49,7 +49,7 @@ RSpec.describe 'Update Epic', :js do
fill_in 'issue-description', with: 'New epic description'
page.within('.detail-page-description') do
click_button('Preview')
click_link('Preview')
expect(find('.md-preview-holder')).to have_content('New epic description')
end
......@@ -63,7 +63,7 @@ RSpec.describe 'Update Epic', :js do
fill_in 'issue-description', with: 'New epic description'
page.within('.detail-page-description') do
click_button('Preview')
click_link('Preview')
expect(find('.md-preview-holder')).to have_content('New epic description')
end
......@@ -76,7 +76,7 @@ RSpec.describe 'Update Epic', :js do
find('.btn-edit').click
page.within('.detail-page-description') do
click_button('Preview')
click_link('Preview')
expect(find('.md-preview-holder')).to have_content('New epic description')
end
end
......@@ -122,7 +122,7 @@ RSpec.describe 'Update Epic', :js do
expect(page.find_field("issue-description").value).to have_content('banana_sample')
page.within('.detail-page-description') do
click_button('Preview')
click_link('Preview')
wait_for_requests
within('.md-preview-holder') do
......
......@@ -28,14 +28,14 @@ RSpec.describe 'Group iterations' do
it 'renders description preview' do
description = find(description_selector)
description.native.send_keys('')
click_button('Preview')
click_link('Preview')
preview = find('.js-vue-md-preview')
expect(preview).to have_content('Nothing to preview.')
click_button('Write')
click_link('Write')
description.native.send_keys(':+1: Nice')
click_button('Preview')
click_link('Preview')
expect(preview).to have_css('gl-emoji')
expect(find('#iteration-description', visible: false)).not_to be_visible
......
......@@ -133,7 +133,7 @@ RSpec.describe 'Merge request > User posts notes', :js do
describe 'when previewing a note' do
it 'shows the toolbar buttons when editing a note' do
page.within('.js-main-target-form') do
expect(page).to have_css('.md-header-toolbar.active')
expect(page).to have_css('.md-header-toolbar')
end
end
......@@ -141,7 +141,7 @@ RSpec.describe 'Merge request > User posts notes', :js do
wait_for_requests
find('.js-md-preview-button').click
page.within('.js-main-target-form') do
expect(page).not_to have_css('.md-header-toolbar.active')
expect(page).not_to have_css('.md-header-toolbar')
end
end
end
......
import { mount } from '@vue/test-utils';
import { nextTick } from 'vue';
import AxiosMockAdapter from 'axios-mock-adapter';
import $ from 'jquery';
import { TEST_HOST, FIXTURES_PATH } from 'spec/test_constants';
import axios from '~/lib/utils/axios_utils';
import MarkdownField from '~/vue_shared/components/markdown/field.vue';
import { mountExtended } from 'helpers/vue_test_utils_helper';
const markdownPreviewPath = `${TEST_HOST}/preview`;
const markdownDocsPath = `${TEST_HOST}/docs`;
......@@ -12,8 +12,8 @@ const textareaValue = 'testing\n123';
const uploadsPath = 'test/uploads';
function assertMarkdownTabs(isWrite, writeLink, previewLink, wrapper) {
expect(writeLink.element.parentNode.classList.contains('active')).toBe(isWrite);
expect(previewLink.element.parentNode.classList.contains('active')).toBe(!isWrite);
expect(writeLink.element.children[0].classList.contains('active')).toBe(isWrite);
expect(previewLink.element.children[0].classList.contains('active')).toBe(!isWrite);
expect(wrapper.find('.md-preview-holder').element.style.display).toBe(isWrite ? 'none' : '');
}
......@@ -29,14 +29,13 @@ describe('Markdown field component', () => {
afterEach(() => {
subject.destroy();
subject = null;
axiosMock.restore();
});
function createSubject(lines = []) {
// We actually mount a wrapper component so that we can force Vue to rerender classes in order to test a regression
// caused by mixing Vanilla JS and Vue.
subject = mount(
subject = mountExtended(
{
components: {
MarkdownField,
......@@ -72,8 +71,8 @@ describe('Markdown field component', () => {
);
}
const getPreviewLink = () => subject.find('.nav-links .js-preview-link');
const getWriteLink = () => subject.find('.nav-links .js-write-link');
const getPreviewLink = () => subject.findByTestId('preview-tab');
const getWriteLink = () => subject.findByTestId('write-tab');
const getMarkdownButton = () => subject.find('.js-md');
const getAllMarkdownButtons = () => subject.findAll('.js-md');
const getVideo = () => subject.find('video');
......@@ -107,15 +106,15 @@ describe('Markdown field component', () => {
it('sets preview link as active', async () => {
previewLink = getPreviewLink();
previewLink.trigger('click');
previewLink.vm.$emit('click', { target: {} });
await nextTick();
expect(previewLink.element.parentNode.classList.contains('active')).toBeTruthy();
expect(previewLink.element.children[0].classList.contains('active')).toBe(true);
});
it('shows preview loading text', async () => {
previewLink = getPreviewLink();
previewLink.trigger('click');
previewLink.vm.$emit('click', { target: {} });
await nextTick();
expect(subject.find('.md-preview-holder').element.textContent.trim()).toContain('Loading…');
......@@ -126,7 +125,7 @@ describe('Markdown field component', () => {
previewLink = getPreviewLink();
previewLink.trigger('click');
previewLink.vm.$emit('click', { target: {} });
await axios.waitFor(markdownPreviewPath);
expect(subject.find('.md-preview-holder').element.innerHTML).toContain(previewHTML);
......@@ -135,7 +134,7 @@ describe('Markdown field component', () => {
it('calls video.pause() on comment input when isSubmitting is changed to true', async () => {
previewLink = getPreviewLink();
previewLink.trigger('click');
previewLink.vm.$emit('click', { target: {} });
await axios.waitFor(markdownPreviewPath);
const video = getVideo();
......@@ -151,19 +150,19 @@ describe('Markdown field component', () => {
writeLink = getWriteLink();
previewLink = getPreviewLink();
writeLink.trigger('click');
writeLink.vm.$emit('click', { target: {} });
await nextTick();
assertMarkdownTabs(true, writeLink, previewLink, subject);
writeLink.trigger('click');
writeLink.vm.$emit('click', { target: {} });
await nextTick();
assertMarkdownTabs(true, writeLink, previewLink, subject);
previewLink.trigger('click');
previewLink.vm.$emit('click', { target: {} });
await nextTick();
assertMarkdownTabs(false, writeLink, previewLink, subject);
previewLink.trigger('click');
previewLink.vm.$emit('click', { target: {} });
await nextTick();
assertMarkdownTabs(false, writeLink, previewLink, subject);
......
import { shallowMount } from '@vue/test-utils';
import $ from 'jquery';
import { nextTick } from 'vue';
import { GlTabs } from '@gitlab/ui';
import HeaderComponent from '~/vue_shared/components/markdown/header.vue';
import ToolbarButton from '~/vue_shared/components/markdown/toolbar_button.vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
describe('Markdown field header component', () => {
let wrapper;
const createWrapper = (props) => {
wrapper = shallowMount(HeaderComponent, {
wrapper = shallowMountExtended(HeaderComponent, {
propsData: {
previewMarkdown: false,
...props,
},
stubs: { GlTabs },
});
};
const findWriteTab = () => wrapper.findByTestId('write-tab');
const findPreviewTab = () => wrapper.findByTestId('preview-tab');
const findToolbarButtons = () => wrapper.findAll(ToolbarButton);
const findToolbarButtonByProp = (prop, value) =>
findToolbarButtons()
......@@ -34,7 +38,6 @@ describe('Markdown field header component', () => {
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
describe('markdown header buttons', () => {
......@@ -75,23 +78,26 @@ describe('Markdown field header component', () => {
});
});
it('renders `write` link as active when previewMarkdown is false', () => {
expect(wrapper.find('li:nth-child(1)').classes()).toContain('active');
it('activates `write` tab when previewMarkdown is false', () => {
expect(findWriteTab().attributes('active')).toBe('true');
expect(findPreviewTab().attributes('active')).toBeUndefined();
});
it('renders `preview` link as active when previewMarkdown is true', () => {
it('activates `preview` tab when previewMarkdown is true', () => {
createWrapper({ previewMarkdown: true });
expect(wrapper.find('li:nth-child(2)').classes()).toContain('active');
expect(findWriteTab().attributes('active')).toBeUndefined();
expect(findPreviewTab().attributes('active')).toBe('true');
});
it('emits toggle markdown event when clicking preview', async () => {
wrapper.find('.js-preview-link').trigger('click');
it('emits toggle markdown event when clicking preview tab', async () => {
const eventData = { target: {} };
findPreviewTab().vm.$emit('click', eventData);
await nextTick();
expect(wrapper.emitted('preview-markdown').length).toEqual(1);
wrapper.find('.js-write-link').trigger('click');
findWriteTab().vm.$emit('click', eventData);
await nextTick();
expect(wrapper.emitted('write-markdown').length).toEqual(1);
......@@ -109,12 +115,10 @@ describe('Markdown field header component', () => {
});
it('blurs preview link after click', () => {
const link = wrapper.find('li:nth-child(2) button');
jest.spyOn(HTMLElement.prototype, 'blur').mockImplementation();
const target = { blur: jest.fn() };
findPreviewTab().vm.$emit('click', { target });
link.trigger('click');
expect(link.element.blur).toHaveBeenCalled();
expect(target.blur).toHaveBeenCalled();
});
it('renders markdown table template', () => {
......
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