Commit c546d11f authored by Simon Knox's avatar Simon Knox Committed by Jose Ivan Vargas

Show cadence title in breadcrumb

parent 2fb58080
<script> <script>
// We are using gl-breadcrumb only at the last child of the handwritten breadcrumb // We are using gl-breadcrumb only at the last child of the handwritten breadcrumb
// until this gitlab-ui issue is resolved: https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1079 // until this gitlab-ui issue is resolved: https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1079
import { GlBreadcrumb, GlIcon } from '@gitlab/ui'; import { GlBreadcrumb, GlIcon, GlSkeletonLoader } from '@gitlab/ui';
import readCadence from '../queries/iteration_cadence.query.graphql';
const cadencePath = '/:cadenceId';
export default { export default {
components: { components: {
GlBreadcrumb, GlBreadcrumb,
GlIcon, GlIcon,
GlSkeletonLoader,
},
inject: ['groupPath'],
apollo: {
group: {
skip() {
return !this.cadenceId;
},
query: readCadence,
variables() {
return {
fullPath: this.groupPath,
id: this.cadenceId,
};
},
result({ data: { group, errors }, error }) {
const cadence = group?.iterationCadences?.nodes?.[0];
if (!cadence || error || errors?.length) {
this.cadenceTitle = this.cadenceId;
return;
}
this.cadenceTitle = cadence.title;
},
},
},
data() {
return {
cadenceTitle: '',
};
}, },
computed: { computed: {
allCrumbs() { cadenceId() {
return this.$route.params.cadenceId;
},
allBreadcrumbs() {
const pathArray = this.$route.path.split('/'); const pathArray = this.$route.path.split('/');
const breadcrumbs = []; const breadcrumbs = [];
pathArray.forEach((path, index) => { pathArray.forEach((path, index) => {
const text = this.$route.matched[index].meta?.breadcrumb || path; let text = this.$route.matched[index].meta?.breadcrumb || path;
if (text) {
if (this.$route.matched[index].path === cadencePath) {
text = this.cadenceTitle;
}
const prevPath = breadcrumbs[index - 1]?.to || ''; const prevPath = breadcrumbs[index - 1]?.to || '';
const to = `${prevPath}/${path}`.replace(/\/+/, '/'); const to = `${prevPath}/${path}`.replace(/\/+/, '/');
...@@ -24,8 +64,7 @@ export default { ...@@ -24,8 +64,7 @@ export default {
to, to,
text, text,
}); });
} });
}, []);
return breadcrumbs; return breadcrumbs;
}, },
...@@ -34,7 +73,13 @@ export default { ...@@ -34,7 +73,13 @@ export default {
</script> </script>
<template> <template>
<gl-breadcrumb :items="allCrumbs" class="gl-p-0 gl-shadow-none"> <gl-skeleton-loader
v-if="$apollo.queries.group.loading"
:width="200"
:lines="1"
class="gl-mx-3"
/>
<gl-breadcrumb v-else :items="allBreadcrumbs" class="gl-p-0 gl-shadow-none">
<template #separator> <template #separator>
<gl-icon name="angle-right" :size="8" /> <gl-icon name="angle-right" :size="8" />
</template> </template>
......
...@@ -100,7 +100,7 @@ export function initIterationReport({ namespaceType, initiallyEditing } = {}) { ...@@ -100,7 +100,7 @@ export function initIterationReport({ namespaceType, initiallyEditing } = {}) {
}); });
} }
function injectVueRouterIntoBreadcrumbs(router) { function injectVueRouterIntoBreadcrumbs(router, groupPath) {
const breadCrumbEls = document.querySelectorAll('nav .js-breadcrumbs-list li'); const breadCrumbEls = document.querySelectorAll('nav .js-breadcrumbs-list li');
const breadCrumbEl = breadCrumbEls[breadCrumbEls.length - 1]; const breadCrumbEl = breadCrumbEls[breadCrumbEls.length - 1];
const crumbs = [breadCrumbEl.querySelector('h2')]; const crumbs = [breadCrumbEl.querySelector('h2')];
...@@ -113,6 +113,9 @@ function injectVueRouterIntoBreadcrumbs(router) { ...@@ -113,6 +113,9 @@ function injectVueRouterIntoBreadcrumbs(router) {
components: { components: {
IterationBreadcrumb, IterationBreadcrumb,
}, },
provide: {
groupPath,
},
render(createElement) { render(createElement) {
// workaround pending https://gitlab.com/gitlab-org/gitlab/-/merge_requests/48115 // workaround pending https://gitlab.com/gitlab-org/gitlab/-/merge_requests/48115
const parentEl = breadCrumbEl.parentElement.parentElement; const parentEl = breadCrumbEl.parentElement.parentElement;
...@@ -135,6 +138,7 @@ export function initCadenceApp({ namespaceType }) { ...@@ -135,6 +138,7 @@ export function initCadenceApp({ namespaceType }) {
const { const {
fullPath, fullPath,
groupPath,
cadencesListPath, cadencesListPath,
canCreateCadence, canCreateCadence,
canEditCadence, canEditCadence,
...@@ -155,7 +159,7 @@ export function initCadenceApp({ namespaceType }) { ...@@ -155,7 +159,7 @@ export function initCadenceApp({ namespaceType }) {
}, },
}); });
injectVueRouterIntoBreadcrumbs(router); injectVueRouterIntoBreadcrumbs(router, groupPath);
return new Vue({ return new Vue({
el, el,
......
...@@ -104,7 +104,7 @@ export default function createRouter({ base, permissions = {} }) { ...@@ -104,7 +104,7 @@ export default function createRouter({ base, permissions = {} }) {
}, },
{ {
name: 'editIteration', name: 'editIteration',
path: 'edit', path: '/:cadenceId/iterations/:iterationId/edit',
component: IterationForm, component: IterationForm,
beforeEnter: checkPermission(permissions.canEditIteration), beforeEnter: checkPermission(permissions.canEditIteration),
meta: { meta: {
......
- page_title s_('Iterations|Iteration cadences') - page_title s_('Iterations|Iteration cadences')
.js-iteration-cadence-app{ data: { full_path: @group.full_path, .js-iteration-cadence-app{ data: { full_path: @group.full_path,
group_path: @group.full_path,
cadences_list_path: group_iteration_cadences_path(@group), cadences_list_path: group_iteration_cadences_path(@group),
can_create_cadence: can?(current_user, :create_iteration_cadence, @group).to_s, can_create_cadence: can?(current_user, :create_iteration_cadence, @group).to_s,
can_edit_cadence: can?(current_user, :admin_iteration_cadence, @group).to_s, can_edit_cadence: can?(current_user, :admin_iteration_cadence, @group).to_s,
......
- page_title s_('Iterations|Iteration cadences') - page_title s_('Iterations|Iteration cadences')
.js-iteration-cadence-app{ data: { full_path: @project.full_path, .js-iteration-cadence-app{ data: { full_path: @project.full_path,
group_path: @project.group.full_path,
cadences_list_path: project_iteration_cadences_path(@project), cadences_list_path: project_iteration_cadences_path(@project),
has_scoped_labels_feature: @project.licensed_feature_available?(:scoped_labels).to_s, has_scoped_labels_feature: @project.licensed_feature_available?(:scoped_labels).to_s,
labels_fetch_path: project_labels_path(@project, format: :json, include_ancestor_groups: true), labels_fetch_path: project_labels_path(@project, format: :json, include_ancestor_groups: true),
......
import { mount } from '@vue/test-utils'; import { mount, shallowMount, createLocalVue } from '@vue/test-utils';
import component from 'ee/iterations/components/iteration_breadcrumb.vue'; import { GlBreadcrumb, GlIcon, GlSkeletonLoader } from '@gitlab/ui';
import VueApollo from 'vue-apollo';
import IterationBreadcrumb from 'ee/iterations/components/iteration_breadcrumb.vue';
import createMockApollo from 'helpers/mock_apollo_helper';
import readCadenceQuery from 'ee/iterations/queries/iteration_cadence.query.graphql';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import createRouter from 'ee/iterations/router'; import createRouter from 'ee/iterations/router';
import waitForPromises from 'helpers/wait_for_promises';
const localVue = createLocalVue();
localVue.use(VueApollo);
describe('Iteration Breadcrumb', () => { describe('Iteration Breadcrumb', () => {
let router; let router;
let wrapper; let wrapper;
let mockApollo;
const base = '/'; const base = '/';
const permissions = { const permissions = {
...@@ -16,13 +26,86 @@ describe('Iteration Breadcrumb', () => { ...@@ -16,13 +26,86 @@ describe('Iteration Breadcrumb', () => {
const cadenceId = 1234; const cadenceId = 1234;
const iterationId = 4567; const iterationId = 4567;
const mountComponent = () => { const findBreadcrumb = () => wrapper.find(GlBreadcrumb);
const waitForApollo = async () => {
jest.runOnlyPendingTimers();
await waitForPromises();
};
const initRouter = () => {
router = createRouter({ base, permissions }); router = createRouter({ base, permissions });
wrapper = mount(component, { };
const mountComponent = (fn = mount, loading = false) => {
wrapper = fn(IterationBreadcrumb, {
router, router,
mocks: {
$apollo: {
queries: {
group: {
loading,
},
},
},
},
provide: {
groupPath: '',
},
propsData: {
cadenceId,
},
data() {
return {
cadenceTitle: 'cadenceTitle',
};
},
}); });
}; };
const createComponentWithApollo = async ({ requestHandlers = [], readCadenceSpy } = {}) => {
mockApollo = createMockApollo([[readCadenceQuery, readCadenceSpy], ...requestHandlers]);
wrapper = extendedWrapper(
shallowMount(IterationBreadcrumb, {
localVue,
router,
provide: { groupPath: '' },
apolloProvider: mockApollo,
propsData: {},
}),
);
await waitForApollo();
};
beforeEach(() => {
initRouter();
});
it('finds glbreadcrumb', () => {
mountComponent();
expect(findBreadcrumb().exists()).toBe(true);
});
describe('when fetching cadence', () => {
it('renders the GlSkeletonLoader', () => {
mountComponent(shallowMount, true);
expect(wrapper.findComponent(GlSkeletonLoader).exists()).toBe(true);
});
});
describe('when not fetching cadence', () => {
it('finds GlIcon', () => {
mountComponent(shallowMount);
expect(findBreadcrumb().find(GlIcon).exists()).toBe(true);
});
});
describe('when a user is on a cadence page', () => {
beforeEach(() => { beforeEach(() => {
mountComponent(); mountComponent();
}); });
...@@ -32,51 +115,138 @@ describe('Iteration Breadcrumb', () => { ...@@ -32,51 +115,138 @@ describe('Iteration Breadcrumb', () => {
router = null; router = null;
}); });
it('contains only a single link to list', () => { it('passes the correct items to GlBreadcrumb', async () => {
const links = wrapper.findAll('a'); await router.push({ name: 'editIteration', params: { cadenceId, iterationId } });
expect(links).toHaveLength(1);
expect(links.at(0).attributes('href')).toBe(base); expect(findBreadcrumb().props('items')).toEqual([
{ path: '', text: 'Iteration cadences', to: '/' },
{ path: '1234', text: 'cadenceTitle', to: '/1234' },
{ path: 'iterations', text: 'Iterations', to: `/${cadenceId}/iterations` },
{
path: `${iterationId}`,
text: `${iterationId}`,
to: `/${cadenceId}/iterations/${iterationId}`,
},
{ path: 'edit', text: 'Edit', to: `/${cadenceId}/iterations/${iterationId}/edit` },
]);
});
}); });
it('links to new cadence form page', async () => { describe('when cadenceId isnt present', () => {
await router.push({ name: 'new' }); it('skips the call to graphql', async () => {
const cadenceSpy = jest
.fn()
.mockResolvedValue({ data: { group: { id: '', iterationCadences: { nodes: [] } } } });
const links = wrapper.findAll('a'); createComponentWithApollo({ readCadenceSpy: cadenceSpy });
expect(links).toHaveLength(2);
expect(links.at(0).attributes('href')).toBe(base); expect(cadenceSpy).toHaveBeenCalledTimes(0);
expect(links.at(1).attributes('href')).toBe('/new'); });
}); });
it('links to edit cadence form page', async () => { describe('when cadenceId is present', () => {
await router.push({ name: 'edit', params: { cadenceId } }); it('calls the iteration cadence query', async () => {
const cadenceSpy = jest
.fn()
.mockResolvedValue({ data: { group: { id: '', iterationCadences: { nodes: [] } } } });
const links = wrapper.findAll('a'); await router.push({ name: 'editIteration', params: { iterationId: '1', cadenceId: '123' } });
expect(links).toHaveLength(3);
expect(links.at(2).attributes('href')).toBe(`/${cadenceId}/edit`); createComponentWithApollo({ readCadenceSpy: cadenceSpy });
expect(cadenceSpy).toHaveBeenCalledTimes(1);
});
});
describe('when cadence is present', () => {
const cadenceTitle = 'cadencetitle';
it('is found in crumb items', async () => {
const cadenceSpy = jest.fn().mockResolvedValue({
data: {
group: {
id: '',
iterationCadences: {
nodes: [
{
title: cadenceTitle,
id: 'cadenceid',
automatic: '',
startDate: '',
rollOver: '',
durationInWeeks: '',
iterationsInAdvance: '',
description: '',
},
],
},
},
},
}); });
it('links to iteration page', async () => { await router.push({ name: 'editIteration', params: { cadenceId: '123', iterationId: '1' } });
await router.push({ name: 'iteration', params: { cadenceId, iterationId } });
createComponentWithApollo({ readCadenceSpy: cadenceSpy });
await waitForPromises();
const breadcrumbProps = findBreadcrumb().props('items');
const links = wrapper.findAll('a'); expect(breadcrumbProps.some(({ text }) => text === cadenceTitle)).toBe(true);
expect(links).toHaveLength(4); });
expect(links.at(2).attributes('href')).toBe(`/${cadenceId}/iterations`);
expect(links.at(3).attributes('href')).toBe(`/${cadenceId}/iterations/${iterationId}`);
}); });
it('links to edit iteration page', async () => { describe('when cadence is not present', () => {
await router.push({ name: 'editIteration', params: { cadenceId, iterationId } }); it('cadence id found in crumb items', async () => {
const cadenceSpy = jest.fn().mockResolvedValue({
data: { group: { id: '', iterationCadences: { nodes: [] } } },
});
await router.push({ name: 'editIteration', params: { cadenceId: '123', iterationId: '1' } });
const links = wrapper.findAll('a'); createComponentWithApollo({ readCadenceSpy: cadenceSpy });
expect(links).toHaveLength(5);
expect(links.at(4).attributes('href')).toBe(`/${cadenceId}/iterations/${iterationId}/edit`); await waitForPromises();
const breadcrumbProps = findBreadcrumb().props('items');
expect(breadcrumbProps.some(({ text }) => text === '123')).toBe(true);
});
}); });
it('links to new iteration page', async () => { describe('when graphql returns error', () => {
await router.push({ name: 'newIteration', params: { cadenceId } }); it('cadence id is found in crumb items', async () => {
const cadenceSpy = jest.fn().mockResolvedValue({
data: { group: { id: '', iterationCadences: { nodes: [], errors: ['error'] } } },
});
await router.push({ name: 'editIteration', params: { cadenceId: '123', iterationId: '1' } });
createComponentWithApollo({ readCadenceSpy: cadenceSpy });
await waitForPromises();
const breadcrumbProps = findBreadcrumb().props('items');
expect(breadcrumbProps.some(({ text }) => text === '123')).toBe(true);
});
});
const links = wrapper.findAll('a'); describe('when server returns error', () => {
expect(links).toHaveLength(4); it('cadence id is found in crumb items', async () => {
expect(links.at(3).attributes('href')).toBe(`/${cadenceId}/iterations/new`); const cadenceSpy = jest.fn().mockResolvedValue({
data: { group: { id: '', iterationCadences: { nodes: [] }, error: 'error' } },
});
await router.push({ name: 'editIteration', params: { cadenceId: '123', iterationId: '1' } });
createComponentWithApollo({ readCadenceSpy: cadenceSpy });
await waitForPromises();
const breadcrumbProps = findBreadcrumb().props('items');
expect(breadcrumbProps.some(({ text }) => text === '123')).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