Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-04-16 09:08:34 +00:00
parent 38261f234b
commit ae30db7b18
9 changed files with 453 additions and 0 deletions

View file

@ -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>

View file

@ -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>

View file

@ -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()',
);

View file

@ -0,0 +1,9 @@
query ToolbarItem($id: String!) {
item(id: $id) @client {
id
label
icon
selected
group
}
}

View file

@ -0,0 +1,5 @@
query ToolbarItems {
items @client {
nodes
}
}

View file

@ -0,0 +1,3 @@
mutation updateItem($id: String!, $propsToUpdate: Item!) {
updateToolbarItem(id: $id, propsToUpdate: $propsToUpdate) @client
}

View 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,
};
};

View file

@ -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,
},
},
});
});
});
});

View 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);
});
});
});