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

Show cadence title in breadcrumb

parent 2fb58080
<script>
// 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
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 {
components: {
GlBreadcrumb,
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: {
allCrumbs() {
cadenceId() {
return this.$route.params.cadenceId;
},
allBreadcrumbs() {
const pathArray = this.$route.path.split('/');
const breadcrumbs = [];
pathArray.forEach((path, index) => {
const text = this.$route.matched[index].meta?.breadcrumb || path;
if (text) {
const prevPath = breadcrumbs[index - 1]?.to || '';
const to = `${prevPath}/${path}`.replace(/\/+/, '/');
breadcrumbs.push({
path,
to,
text,
});
let text = this.$route.matched[index].meta?.breadcrumb || path;
if (this.$route.matched[index].path === cadencePath) {
text = this.cadenceTitle;
}
}, []);
const prevPath = breadcrumbs[index - 1]?.to || '';
const to = `${prevPath}/${path}`.replace(/\/+/, '/');
breadcrumbs.push({
path,
to,
text,
});
});
return breadcrumbs;
},
......@@ -34,7 +73,13 @@ export default {
</script>
<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>
<gl-icon name="angle-right" :size="8" />
</template>
......
......@@ -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 breadCrumbEl = breadCrumbEls[breadCrumbEls.length - 1];
const crumbs = [breadCrumbEl.querySelector('h2')];
......@@ -113,6 +113,9 @@ function injectVueRouterIntoBreadcrumbs(router) {
components: {
IterationBreadcrumb,
},
provide: {
groupPath,
},
render(createElement) {
// workaround pending https://gitlab.com/gitlab-org/gitlab/-/merge_requests/48115
const parentEl = breadCrumbEl.parentElement.parentElement;
......@@ -135,6 +138,7 @@ export function initCadenceApp({ namespaceType }) {
const {
fullPath,
groupPath,
cadencesListPath,
canCreateCadence,
canEditCadence,
......@@ -155,7 +159,7 @@ export function initCadenceApp({ namespaceType }) {
},
});
injectVueRouterIntoBreadcrumbs(router);
injectVueRouterIntoBreadcrumbs(router, groupPath);
return new Vue({
el,
......
......@@ -104,7 +104,7 @@ export default function createRouter({ base, permissions = {} }) {
},
{
name: 'editIteration',
path: 'edit',
path: '/:cadenceId/iterations/:iterationId/edit',
component: IterationForm,
beforeEnter: checkPermission(permissions.canEditIteration),
meta: {
......
- page_title s_('Iterations|Iteration cadences')
.js-iteration-cadence-app{ data: { full_path: @group.full_path,
group_path: @group.full_path,
cadences_list_path: group_iteration_cadences_path(@group),
can_create_cadence: can?(current_user, :create_iteration_cadence, @group).to_s,
can_edit_cadence: can?(current_user, :admin_iteration_cadence, @group).to_s,
......
- page_title s_('Iterations|Iteration cadences')
.js-iteration-cadence-app{ data: { full_path: @project.full_path,
group_path: @project.group.full_path,
cadences_list_path: project_iteration_cadences_path(@project),
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),
......
import { mount } from '@vue/test-utils';
import component from 'ee/iterations/components/iteration_breadcrumb.vue';
import { mount, shallowMount, createLocalVue } from '@vue/test-utils';
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 waitForPromises from 'helpers/wait_for_promises';
const localVue = createLocalVue();
localVue.use(VueApollo);
describe('Iteration Breadcrumb', () => {
let router;
let wrapper;
let mockApollo;
const base = '/';
const permissions = {
......@@ -16,67 +26,227 @@ describe('Iteration Breadcrumb', () => {
const cadenceId = 1234;
const iterationId = 4567;
const mountComponent = () => {
const findBreadcrumb = () => wrapper.find(GlBreadcrumb);
const waitForApollo = async () => {
jest.runOnlyPendingTimers();
await waitForPromises();
};
const initRouter = () => {
router = createRouter({ base, permissions });
wrapper = mount(component, {
};
const mountComponent = (fn = mount, loading = false) => {
wrapper = fn(IterationBreadcrumb, {
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);
});
afterEach(() => {
wrapper.destroy();
router = null;
describe('when fetching cadence', () => {
it('renders the GlSkeletonLoader', () => {
mountComponent(shallowMount, true);
expect(wrapper.findComponent(GlSkeletonLoader).exists()).toBe(true);
});
});
it('contains only a single link to list', () => {
const links = wrapper.findAll('a');
expect(links).toHaveLength(1);
expect(links.at(0).attributes('href')).toBe(base);
describe('when not fetching cadence', () => {
it('finds GlIcon', () => {
mountComponent(shallowMount);
expect(findBreadcrumb().find(GlIcon).exists()).toBe(true);
});
});
it('links to new cadence form page', async () => {
await router.push({ name: 'new' });
describe('when a user is on a cadence page', () => {
beforeEach(() => {
mountComponent();
});
afterEach(() => {
wrapper.destroy();
router = null;
});
const links = wrapper.findAll('a');
expect(links).toHaveLength(2);
expect(links.at(0).attributes('href')).toBe(base);
expect(links.at(1).attributes('href')).toBe('/new');
it('passes the correct items to GlBreadcrumb', async () => {
await router.push({ name: 'editIteration', params: { cadenceId, iterationId } });
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 edit cadence form page', async () => {
await router.push({ name: 'edit', params: { cadenceId } });
describe('when cadenceId isnt present', () => {
it('skips the call to graphql', async () => {
const cadenceSpy = jest
.fn()
.mockResolvedValue({ data: { group: { id: '', iterationCadences: { nodes: [] } } } });
const links = wrapper.findAll('a');
expect(links).toHaveLength(3);
expect(links.at(2).attributes('href')).toBe(`/${cadenceId}/edit`);
createComponentWithApollo({ readCadenceSpy: cadenceSpy });
expect(cadenceSpy).toHaveBeenCalledTimes(0);
});
});
it('links to iteration page', async () => {
await router.push({ name: 'iteration', params: { cadenceId, iterationId } });
describe('when cadenceId is present', () => {
it('calls the iteration cadence query', async () => {
const cadenceSpy = jest
.fn()
.mockResolvedValue({ data: { group: { id: '', iterationCadences: { nodes: [] } } } });
await router.push({ name: 'editIteration', params: { iterationId: '1', cadenceId: '123' } });
const links = wrapper.findAll('a');
expect(links).toHaveLength(4);
expect(links.at(2).attributes('href')).toBe(`/${cadenceId}/iterations`);
expect(links.at(3).attributes('href')).toBe(`/${cadenceId}/iterations/${iterationId}`);
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: '',
},
],
},
},
},
});
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 === cadenceTitle)).toBe(true);
});
});
it('links to edit iteration page', async () => {
await router.push({ name: 'editIteration', params: { cadenceId, iterationId } });
describe('when cadence is not present', () => {
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');
expect(links).toHaveLength(5);
expect(links.at(4).attributes('href')).toBe(`/${cadenceId}/iterations/${iterationId}/edit`);
createComponentWithApollo({ readCadenceSpy: cadenceSpy });
await waitForPromises();
const breadcrumbProps = findBreadcrumb().props('items');
expect(breadcrumbProps.some(({ text }) => text === '123')).toBe(true);
});
});
it('links to new iteration page', async () => {
await router.push({ name: 'newIteration', params: { cadenceId } });
describe('when graphql returns error', () => {
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 });
const links = wrapper.findAll('a');
expect(links).toHaveLength(4);
expect(links.at(3).attributes('href')).toBe(`/${cadenceId}/iterations/new`);
await waitForPromises();
const breadcrumbProps = findBreadcrumb().props('items');
expect(breadcrumbProps.some(({ text }) => text === '123')).toBe(true);
});
});
describe('when server returns error', () => {
it('cadence id is found in crumb items', async () => {
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