Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
38261f234b
commit
ae30db7b18
9 changed files with 453 additions and 0 deletions
|
@ -0,0 +1,70 @@
|
|||
<script>
|
||||
import { isEmpty } from 'lodash';
|
||||
import { GlButtonGroup } from '@gitlab/ui';
|
||||
import getToolbarItemsQuery from '~/editor/graphql/get_items.query.graphql';
|
||||
import { EDITOR_TOOLBAR_LEFT_GROUP, EDITOR_TOOLBAR_RIGHT_GROUP } from '~/editor/constants';
|
||||
import SourceEditorToolbarButton from './source_editor_toolbar_button.vue';
|
||||
|
||||
export default {
|
||||
name: 'SourceEditorToolbar',
|
||||
components: {
|
||||
SourceEditorToolbarButton,
|
||||
GlButtonGroup,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
items: [],
|
||||
};
|
||||
},
|
||||
apollo: {
|
||||
items: {
|
||||
query: getToolbarItemsQuery,
|
||||
update(data) {
|
||||
return this.setDefaultGroup(data?.items?.nodes);
|
||||
},
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
isVisible() {
|
||||
return this.items.length;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
setDefaultGroup(nodes = []) {
|
||||
return nodes.map((item) => {
|
||||
return {
|
||||
...item,
|
||||
group:
|
||||
(this.$options.groups.includes(item.group) && item.group) || EDITOR_TOOLBAR_RIGHT_GROUP,
|
||||
};
|
||||
});
|
||||
},
|
||||
getGroupItems(group) {
|
||||
return this.items.filter((item) => item.group === group);
|
||||
},
|
||||
hasGroupItems(group) {
|
||||
return !isEmpty(this.getGroupItems(group));
|
||||
},
|
||||
},
|
||||
groups: [EDITOR_TOOLBAR_LEFT_GROUP, EDITOR_TOOLBAR_RIGHT_GROUP],
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<section
|
||||
v-if="isVisible"
|
||||
id="se-toolbar"
|
||||
class="gl-py-3 gl-px-5 gl-bg-white gl-border-t gl-border-b gl-display-flex gl-justify-content-space-between gl-align-items-center"
|
||||
>
|
||||
<template v-for="group in $options.groups">
|
||||
<gl-button-group v-if="hasGroupItems(group)" :key="group">
|
||||
<template v-for="item in getGroupItems(group)">
|
||||
<source-editor-toolbar-button
|
||||
:key="item.id"
|
||||
:button="item"
|
||||
@click="$emit('click', item)"
|
||||
/>
|
||||
</template>
|
||||
</gl-button-group>
|
||||
</template>
|
||||
</section>
|
||||
</template>
|
|
@ -0,0 +1,89 @@
|
|||
<script>
|
||||
import { GlButton, GlTooltipDirective } from '@gitlab/ui';
|
||||
import updateToolbarItemMutation from '~/editor/graphql/update_item.mutation.graphql';
|
||||
import getToolbarItemQuery from '~/editor/graphql/get_item.query.graphql';
|
||||
|
||||
export default {
|
||||
name: 'SourceEditorToolbarButton',
|
||||
components: {
|
||||
GlButton,
|
||||
},
|
||||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
},
|
||||
props: {
|
||||
button: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default() {
|
||||
return {};
|
||||
},
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
buttonItem: this.button,
|
||||
};
|
||||
},
|
||||
apollo: {
|
||||
buttonItem: {
|
||||
query: getToolbarItemQuery,
|
||||
variables() {
|
||||
return {
|
||||
id: this.button.id,
|
||||
};
|
||||
},
|
||||
update({ item }) {
|
||||
return item;
|
||||
},
|
||||
skip() {
|
||||
return !this.button.id;
|
||||
},
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
icon() {
|
||||
return this.buttonItem.selected
|
||||
? this.buttonItem.selectedIcon || this.buttonItem.icon
|
||||
: this.buttonItem.icon;
|
||||
},
|
||||
label() {
|
||||
return this.buttonItem.selected
|
||||
? this.buttonItem.selectedLabel || this.buttonItem.label
|
||||
: this.buttonItem.label;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
clickHandler() {
|
||||
if (this.buttonItem.onClick) {
|
||||
this.buttonItem.onClick();
|
||||
}
|
||||
this.$apollo.mutate({
|
||||
mutation: updateToolbarItemMutation,
|
||||
variables: {
|
||||
id: this.buttonItem.id,
|
||||
propsToUpdate: {
|
||||
selected: !this.buttonItem.selected,
|
||||
},
|
||||
},
|
||||
});
|
||||
this.$emit('click');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div>
|
||||
<gl-button
|
||||
v-gl-tooltip.hover
|
||||
:category="buttonItem.category"
|
||||
:variant="buttonItem.variant"
|
||||
type="button"
|
||||
:selected="buttonItem.selected"
|
||||
:icon="icon"
|
||||
:title="label"
|
||||
:aria-label="label"
|
||||
@click="clickHandler"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
|
@ -12,6 +12,9 @@ export const EDITOR_TYPE_DIFF = 'vs.editor.IDiffEditor';
|
|||
export const EDITOR_CODE_INSTANCE_FN = 'createInstance';
|
||||
export const EDITOR_DIFF_INSTANCE_FN = 'createDiffInstance';
|
||||
|
||||
export const EDITOR_TOOLBAR_LEFT_GROUP = 'left';
|
||||
export const EDITOR_TOOLBAR_RIGHT_GROUP = 'right';
|
||||
|
||||
export const SOURCE_EDITOR_INSTANCE_ERROR_NO_EL = s__(
|
||||
'SourceEditor|"el" parameter is required for createInstance()',
|
||||
);
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
query ToolbarItem($id: String!) {
|
||||
item(id: $id) @client {
|
||||
id
|
||||
label
|
||||
icon
|
||||
selected
|
||||
group
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
query ToolbarItems {
|
||||
items @client {
|
||||
nodes
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
mutation updateItem($id: String!, $propsToUpdate: Item!) {
|
||||
updateToolbarItem(id: $id, propsToUpdate: $propsToUpdate) @client
|
||||
}
|
12
spec/frontend/editor/components/helpers.js
Normal file
12
spec/frontend/editor/components/helpers.js
Normal file
|
@ -0,0 +1,12 @@
|
|||
import { EDITOR_TOOLBAR_RIGHT_GROUP } from '~/editor/constants';
|
||||
|
||||
export const buildButton = (id = 'foo-bar-btn', options = {}) => {
|
||||
return {
|
||||
__typename: 'Item',
|
||||
id,
|
||||
label: options.label || 'Foo Bar Button',
|
||||
icon: options.icon || 'foo-bar',
|
||||
selected: options.selected || false,
|
||||
group: options.group || EDITOR_TOOLBAR_RIGHT_GROUP,
|
||||
};
|
||||
};
|
|
@ -0,0 +1,146 @@
|
|||
import Vue, { nextTick } from 'vue';
|
||||
import VueApollo from 'vue-apollo';
|
||||
import { GlButton } from '@gitlab/ui';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||
import SourceEditorToolbarButton from '~/editor/components/source_editor_toolbar_button.vue';
|
||||
import getToolbarItemQuery from '~/editor/graphql/get_item.query.graphql';
|
||||
import updateToolbarItemMutation from '~/editor/graphql/update_item.mutation.graphql';
|
||||
import { buildButton } from './helpers';
|
||||
|
||||
Vue.use(VueApollo);
|
||||
|
||||
describe('Source Editor Toolbar button', () => {
|
||||
let wrapper;
|
||||
let mockApollo;
|
||||
const defaultBtn = buildButton();
|
||||
|
||||
const findButton = () => wrapper.findComponent(GlButton);
|
||||
|
||||
const createComponentWithApollo = ({ propsData } = {}) => {
|
||||
mockApollo = createMockApollo();
|
||||
mockApollo.clients.defaultClient.cache.writeQuery({
|
||||
query: getToolbarItemQuery,
|
||||
variables: { id: defaultBtn.id },
|
||||
data: {
|
||||
item: {
|
||||
...defaultBtn,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
wrapper = shallowMount(SourceEditorToolbarButton, {
|
||||
propsData,
|
||||
apolloProvider: mockApollo,
|
||||
});
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
mockApollo = null;
|
||||
});
|
||||
|
||||
describe('default', () => {
|
||||
const defaultProps = {
|
||||
category: 'primary',
|
||||
variant: 'default',
|
||||
};
|
||||
const customProps = {
|
||||
category: 'secondary',
|
||||
variant: 'info',
|
||||
};
|
||||
it('renders a default button without props', async () => {
|
||||
createComponentWithApollo();
|
||||
const btn = findButton();
|
||||
expect(btn.exists()).toBe(true);
|
||||
expect(btn.props()).toMatchObject(defaultProps);
|
||||
});
|
||||
|
||||
it('renders a button based on the props passed', async () => {
|
||||
createComponentWithApollo({
|
||||
propsData: {
|
||||
button: customProps,
|
||||
},
|
||||
});
|
||||
const btn = findButton();
|
||||
expect(btn.props()).toMatchObject(customProps);
|
||||
});
|
||||
});
|
||||
|
||||
describe('button updates', () => {
|
||||
it('it properly updates button on Apollo cache update', async () => {
|
||||
const { id } = defaultBtn;
|
||||
|
||||
createComponentWithApollo({
|
||||
propsData: {
|
||||
button: {
|
||||
id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(findButton().props('selected')).toBe(false);
|
||||
|
||||
mockApollo.clients.defaultClient.cache.writeQuery({
|
||||
query: getToolbarItemQuery,
|
||||
variables: { id },
|
||||
data: {
|
||||
item: {
|
||||
...defaultBtn,
|
||||
selected: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
jest.runOnlyPendingTimers();
|
||||
await nextTick();
|
||||
|
||||
expect(findButton().props('selected')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('click handler', () => {
|
||||
it('fires the click handler on the button when available', () => {
|
||||
const spy = jest.fn();
|
||||
createComponentWithApollo({
|
||||
propsData: {
|
||||
button: {
|
||||
onClick: spy,
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(spy).not.toHaveBeenCalled();
|
||||
findButton().vm.$emit('click');
|
||||
expect(spy).toHaveBeenCalled();
|
||||
});
|
||||
it('emits the "click" event', () => {
|
||||
createComponentWithApollo();
|
||||
jest.spyOn(wrapper.vm, '$emit');
|
||||
expect(wrapper.vm.$emit).not.toHaveBeenCalled();
|
||||
findButton().vm.$emit('click');
|
||||
expect(wrapper.vm.$emit).toHaveBeenCalledWith('click');
|
||||
});
|
||||
it('triggers the mutation exposing the changed "selected" prop', () => {
|
||||
const { id } = defaultBtn;
|
||||
createComponentWithApollo({
|
||||
propsData: {
|
||||
button: {
|
||||
id,
|
||||
},
|
||||
},
|
||||
});
|
||||
jest.spyOn(wrapper.vm.$apollo, 'mutate');
|
||||
expect(wrapper.vm.$apollo.mutate).not.toHaveBeenCalled();
|
||||
findButton().vm.$emit('click');
|
||||
expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
|
||||
mutation: updateToolbarItemMutation,
|
||||
variables: {
|
||||
id,
|
||||
propsToUpdate: {
|
||||
selected: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
116
spec/frontend/editor/components/source_editor_toolbar_spec.js
Normal file
116
spec/frontend/editor/components/source_editor_toolbar_spec.js
Normal file
|
@ -0,0 +1,116 @@
|
|||
import Vue, { nextTick } from 'vue';
|
||||
import VueApollo from 'vue-apollo';
|
||||
import { GlButtonGroup } from '@gitlab/ui';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||
import SourceEditorToolbar from '~/editor/components/source_editor_toolbar.vue';
|
||||
import SourceEditorToolbarButton from '~/editor/components/source_editor_toolbar_button.vue';
|
||||
import { EDITOR_TOOLBAR_LEFT_GROUP, EDITOR_TOOLBAR_RIGHT_GROUP } from '~/editor/constants';
|
||||
import getToolbarItemsQuery from '~/editor/graphql/get_items.query.graphql';
|
||||
import { buildButton } from './helpers';
|
||||
|
||||
Vue.use(VueApollo);
|
||||
|
||||
describe('Source Editor Toolbar', () => {
|
||||
let wrapper;
|
||||
let mockApollo;
|
||||
|
||||
const findButtons = () => wrapper.findAllComponents(SourceEditorToolbarButton);
|
||||
|
||||
const createApolloMockWithCache = (items = []) => {
|
||||
mockApollo = createMockApollo();
|
||||
mockApollo.clients.defaultClient.cache.writeQuery({
|
||||
query: getToolbarItemsQuery,
|
||||
data: {
|
||||
items: {
|
||||
nodes: items,
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const createComponentWithApollo = (items = []) => {
|
||||
createApolloMockWithCache(items);
|
||||
wrapper = shallowMount(SourceEditorToolbar, {
|
||||
apolloProvider: mockApollo,
|
||||
stubs: {
|
||||
GlButtonGroup,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
mockApollo = null;
|
||||
});
|
||||
|
||||
describe('groups', () => {
|
||||
it.each`
|
||||
group | expectedGroup
|
||||
${EDITOR_TOOLBAR_LEFT_GROUP} | ${EDITOR_TOOLBAR_LEFT_GROUP}
|
||||
${EDITOR_TOOLBAR_RIGHT_GROUP} | ${EDITOR_TOOLBAR_RIGHT_GROUP}
|
||||
${undefined} | ${EDITOR_TOOLBAR_RIGHT_GROUP}
|
||||
${'non-existing'} | ${EDITOR_TOOLBAR_RIGHT_GROUP}
|
||||
`('puts item with group="$group" into $expectedGroup group', ({ group, expectedGroup }) => {
|
||||
const item = buildButton('first', {
|
||||
group,
|
||||
});
|
||||
createComponentWithApollo([item]);
|
||||
expect(findButtons()).toHaveLength(1);
|
||||
[EDITOR_TOOLBAR_RIGHT_GROUP, EDITOR_TOOLBAR_LEFT_GROUP].forEach((g) => {
|
||||
if (g === expectedGroup) {
|
||||
expect(wrapper.vm.getGroupItems(g)).toEqual([expect.objectContaining({ id: 'first' })]);
|
||||
} else {
|
||||
expect(wrapper.vm.getGroupItems(g)).toHaveLength(0);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('buttons update', () => {
|
||||
it('it properly updates buttons on Apollo cache update', async () => {
|
||||
const item = buildButton('first', {
|
||||
group: EDITOR_TOOLBAR_RIGHT_GROUP,
|
||||
});
|
||||
createComponentWithApollo();
|
||||
|
||||
expect(findButtons()).toHaveLength(0);
|
||||
|
||||
mockApollo.clients.defaultClient.cache.writeQuery({
|
||||
query: getToolbarItemsQuery,
|
||||
data: {
|
||||
items: {
|
||||
nodes: [item],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
jest.runOnlyPendingTimers();
|
||||
await nextTick();
|
||||
|
||||
expect(findButtons()).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('click handler', () => {
|
||||
it('emits the "click" event when a button is clicked', () => {
|
||||
const item1 = buildButton('first', {
|
||||
group: EDITOR_TOOLBAR_LEFT_GROUP,
|
||||
});
|
||||
const item2 = buildButton('second', {
|
||||
group: EDITOR_TOOLBAR_RIGHT_GROUP,
|
||||
});
|
||||
createComponentWithApollo([item1, item2]);
|
||||
jest.spyOn(wrapper.vm, '$emit');
|
||||
expect(wrapper.vm.$emit).not.toHaveBeenCalled();
|
||||
|
||||
findButtons().at(0).vm.$emit('click');
|
||||
expect(wrapper.vm.$emit).toHaveBeenCalledWith('click', item1);
|
||||
|
||||
findButtons().at(1).vm.$emit('click');
|
||||
expect(wrapper.vm.$emit).toHaveBeenCalledWith('click', item2);
|
||||
|
||||
expect(wrapper.vm.$emit.mock.calls).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Reference in a new issue