Commit f683d9b8 authored by Florie Guibert's avatar Florie Guibert

Epic boards - Ancestors sidebar widget

Create ancestors GraphQL and Apollo widget and use it in epic boards
sidebar
parent 6f1e7016
<script>
import { GlDrawer } from '@gitlab/ui';
import { mapState, mapActions, mapGetters } from 'vuex';
import SidebarAncestorsWidget from 'ee_component/sidebar/components/ancestors_tree/sidebar_ancestors_widget.vue';
import BoardSidebarLabelsSelect from '~/boards/components/sidebar/board_sidebar_labels_select.vue';
import BoardSidebarTitle from '~/boards/components/sidebar/board_sidebar_title.vue';
import { ISSUABLE } from '~/boards/constants';
......@@ -20,6 +21,7 @@ export default {
SidebarDateWidget,
SidebarParticipantsWidget,
SidebarSubscriptionsWidget,
SidebarAncestorsWidget,
},
computed: {
...mapGetters(['isSidebarOpen', 'activeBoardItem']),
......@@ -65,16 +67,21 @@ export default {
:can-inherit="true"
/>
<board-sidebar-labels-select class="labels" />
<sidebar-participants-widget
<sidebar-confidentiality-widget
:iid="activeBoardItem.iid"
:full-path="fullPath"
:issuable-type="issuableType"
@confidentialityUpdated="setActiveItemConfidential($event)"
/>
<sidebar-ancestors-widget
:iid="activeBoardItem.iid"
:full-path="fullPath"
issuable-type="epic"
/>
<sidebar-confidentiality-widget
<sidebar-participants-widget
:iid="activeBoardItem.iid"
:full-path="fullPath"
:issuable-type="issuableType"
@confidentialityUpdated="setActiveItemConfidential($event)"
issuable-type="epic"
/>
<sidebar-subscriptions-widget
:iid="activeBoardItem.iid"
......
......@@ -81,7 +81,7 @@ export default {
<gl-icon :name="getIcon(ancestor)" />
</div>
<div class="vertical-timeline-content">
<gl-link :href="ancestor.url">{{ ancestor.title }}</gl-link>
<gl-link :href="ancestor.url" class="gl-text-gray-900">{{ ancestor.title }}</gl-link>
</div>
</li>
</ul>
......
<script>
import { __ } from '~/locale';
import { ancestorsQueries } from '../../constants';
import Ancestors from './ancestors_tree.vue';
export default {
i18n: {
fetchingError: __('An error occurred while fetching ancestors'),
},
components: {
Ancestors,
},
props: {
iid: {
type: String,
required: true,
},
fullPath: {
type: String,
required: true,
},
issuableType: {
required: true,
type: String,
},
},
data() {
return {
ancestors: [],
};
},
apollo: {
ancestors: {
query() {
return ancestorsQueries[this.issuableType].query;
},
variables() {
return {
fullPath: this.fullPath,
iid: this.iid,
};
},
update(data) {
return data.workspace?.issuable?.ancestors.nodes || [];
},
error(error) {
this.$emit('fetch-error', {
message: this.$options.i18n.fetchingError,
error,
});
},
},
},
computed: {
isLoading() {
return this.$apollo.queries.ancestors.loading;
},
},
};
</script>
<template>
<ancestors :is-fetching="isLoading" :ancestors="ancestors" class="block ancestors" />
</template>
......@@ -5,6 +5,7 @@ import {
IssuableAttributeState as IssuableAttributeStateFoss,
issuableAttributesQueries as issuableAttributesQueriesFoss,
} from '~/sidebar/constants';
import epicAncestorsQuery from './queries/epic_ancestors.query.graphql';
import groupEpicsQuery from './queries/group_epics.query.graphql';
import groupIterationsQuery from './queries/group_iterations.query.graphql';
import projectIssueEpicMutation from './queries/project_issue_epic.mutation.graphql';
......@@ -122,3 +123,9 @@ export const issuableAttributesQueries = {
list: epicsQueries,
},
};
export const ancestorsQueries = {
[IssuableType.Epic]: {
query: epicAncestorsQuery,
},
};
query epicAncestors($fullPath: ID!, $iid: ID) {
workspace: group(fullPath: $fullPath) {
__typename
issuable: epic(iid: $iid) {
__typename
id
ancestors {
nodes {
id
title
state
url: webUrl
}
}
}
}
}
......@@ -2,6 +2,7 @@ import { GlDrawer } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Vuex from 'vuex';
import EpicBoardContentSidebar from 'ee_component/boards/components/epic_board_content_sidebar.vue';
import SidebarAncestorsWidget from 'ee_component/sidebar/components/ancestors_tree/sidebar_ancestors_widget.vue';
import { stubComponent } from 'helpers/stub_component';
import BoardSidebarLabelsSelect from '~/boards/components/sidebar/board_sidebar_labels_select.vue';
import BoardSidebarTitle from '~/boards/components/sidebar/board_sidebar_title.vue';
......@@ -99,6 +100,9 @@ describe('EpicBoardContentSidebar', () => {
it('renders SidebarSubscriptionsWidget', () => {
expect(wrapper.findComponent(SidebarSubscriptionsWidget).exists()).toBe(true);
});
it('renders SidebarAncestorsWidget', () => {
expect(wrapper.findComponent(SidebarAncestorsWidget).exists()).toBe(true);
});
describe('when we emit close', () => {
let toggleBoardItem;
......
import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import Ancestors from 'ee_component/sidebar/components/ancestors_tree/ancestors_tree.vue';
import SidebarAncestorsWidget from 'ee_component/sidebar/components/ancestors_tree/sidebar_ancestors_widget.vue';
import epicAncestorsQuery from 'ee_component/sidebar/queries/epic_ancestors.query.graphql';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { epicAncestorsResponse } from '../../mock_data';
Vue.use(VueApollo);
describe('Sidebar Ancestors Widget', () => {
let wrapper;
let fakeApollo;
const findAncestors = () => wrapper.findComponent(Ancestors);
const createComponent = ({
ancestorsQueryHandler = jest.fn().mockResolvedValue(epicAncestorsResponse()),
} = {}) => {
fakeApollo = createMockApollo([[epicAncestorsQuery, ancestorsQueryHandler]]);
wrapper = shallowMount(SidebarAncestorsWidget, {
apolloProvider: fakeApollo,
propsData: {
fullPath: 'group',
iid: '1',
issuableType: 'epic',
},
});
};
afterEach(() => {
wrapper.destroy();
fakeApollo = null;
});
it('passes a `isFetching` prop as true to child component when query is loading', () => {
createComponent();
expect(findAncestors().props('isFetching')).toBe(true);
});
describe('when ancestors are loaded', () => {
beforeEach(() => {
createComponent({
ancestorsQueryHandler: jest.fn().mockResolvedValue(epicAncestorsResponse()),
});
return waitForPromises();
});
it('passes a `isFetching` prop as false to editable item', () => {
expect(findAncestors().props('isFetching')).toBe(false);
});
it('passes ancestors to child component', () => {
expect(findAncestors().props('ancestors')).toEqual(
epicAncestorsResponse().data.workspace.issuable.ancestors.nodes,
);
});
});
describe('when error occurs', () => {
it('emits error event with correct parameters', async () => {
const mockError = new Error('mayday');
createComponent({
ancestorsQueryHandler: jest.fn().mockRejectedValue(mockError),
});
await waitForPromises();
const [
[
{
message,
error: { networkError },
},
],
] = wrapper.emitted('fetch-error');
expect(message).toBe(wrapper.vm.$options.i18n.fetchingError);
expect(networkError).toEqual(mockError);
});
});
});
......@@ -144,3 +144,25 @@ export const mockEpicMutationResponse = {
},
},
};
export const epicAncestorsResponse = () => ({
data: {
workspace: {
__typename: 'Group',
issuable: {
__typename: 'Epic',
id: 'gid://gitlab/Epic/4',
ancestors: {
nodes: [
{
id: 'gid://gitlab/Epic/2',
title: 'Ancestor epic',
url: 'http://gdk.test:3000/groups/gitlab-org/-/epics/2',
state: 'opened',
},
],
},
},
},
},
});
......@@ -3504,6 +3504,9 @@ msgstr ""
msgid "An error occurred while enabling Service Desk."
msgstr ""
msgid "An error occurred while fetching ancestors"
msgstr ""
msgid "An error occurred while fetching branches. Retry the search."
msgstr ""
......
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