Commit 7b37a573 authored by Simon Knox's avatar Simon Knox Committed by Vitaly Slobodin

Start adding cadence breadcrumbs

parent 57e37611
<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';
export default {
components: {
GlBreadcrumb,
GlIcon,
},
computed: {
allCrumbs() {
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,
});
}
}, []);
return breadcrumbs;
},
},
};
</script>
<template>
<gl-breadcrumb :items="allCrumbs" class="gl-p-0 gl-shadow-none">
<template #separator>
<gl-icon name="angle-right" :size="8" />
</template>
</gl-breadcrumb>
</template>
......@@ -182,7 +182,7 @@ export default {
<div>
<div class="gl-display-flex">
<h3 ref="pageTitle" class="page-title">
{{ isEditing ? __('Edit iteration') : __('New iteration') }}
{{ isEditing ? s__('Iterations|Edit iteration') : s__('Iterations|New iteration') }}
</h3>
</div>
<hr class="gl-mt-0" />
......
......@@ -150,7 +150,7 @@ export default {
<div>
<div class="gl-display-flex">
<h3 ref="pageTitle" class="page-title">
{{ isEditing ? __('Edit iteration') : __('New iteration') }}
{{ isEditing ? s__('Iterations|Edit iteration') : s__('Iterations|New iteration') }}
</h3>
</div>
<hr class="gl-mt-0" />
......
......@@ -146,7 +146,7 @@ export default {
<template #button-content>
<gl-icon name="ellipsis_v" /><span class="gl-sr-only">{{ __('Actions') }}</span>
</template>
<gl-dropdown-item :to="editPage">{{ __('Edit iteration') }}</gl-dropdown-item>
<gl-dropdown-item :to="editPage">{{ __('Edit') }}</gl-dropdown-item>
</gl-dropdown>
</div>
<h3 ref="title" class="page-title">{{ iteration.title }}</h3>
......
......@@ -217,7 +217,7 @@ export default {
<template #button-content>
<gl-icon name="ellipsis_v" /><span class="gl-sr-only">{{ __('Actions') }}</span>
</template>
<gl-dropdown-item @click="loadEditPage">{{ __('Edit iteration') }}</gl-dropdown-item>
<gl-dropdown-item @click="loadEditPage">{{ __('Edit') }}</gl-dropdown-item>
</gl-dropdown>
</div>
<h3 ref="title" class="page-title">{{ iteration.title }}</h3>
......
......@@ -171,7 +171,7 @@ export default {
data-qa-selector="new_iteration_button"
:href="newIterationPath"
>
{{ __('New iteration') }}
{{ s__('Iterations|New iteration') }}
</gl-button>
</li>
</template>
......
......@@ -3,6 +3,7 @@ import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
import { parseBoolean } from '~/lib/utils/common_utils';
import App from './components/app.vue';
import IterationBreadcrumb from './components/iteration_breadcrumb.vue';
import IterationForm from './components/iteration_form_without_vue_router.vue';
import IterationReport from './components/iteration_report_without_vue_router.vue';
import Iterations from './components/iterations.vue';
......@@ -95,6 +96,32 @@ export function initIterationReport({ namespaceType, initiallyEditing } = {}) {
});
}
function injectVueRouterIntoBreadcrumbs(router) {
const breadCrumbEls = document.querySelectorAll('nav .js-breadcrumbs-list li');
const breadCrumbEl = breadCrumbEls[breadCrumbEls.length - 1];
const crumbs = [breadCrumbEl.querySelector('h2')];
const nestedBreadcrumbEl = document.createElement('div');
breadCrumbEl.replaceChild(nestedBreadcrumbEl, crumbs[0]);
return new Vue({
el: nestedBreadcrumbEl,
router,
apolloProvider,
components: {
IterationBreadcrumb,
},
render(createElement) {
// workaround pending https://gitlab.com/gitlab-org/gitlab/-/merge_requests/48115
const parentEl = breadCrumbEl.parentElement.parentElement;
if (parentEl) {
parentEl.classList.remove('breadcrumbs-container');
parentEl.classList.add('gl-display-flex');
parentEl.classList.add('w-100');
}
return createElement('iteration-breadcrumb');
},
});
}
export function initCadenceApp({ namespaceType }) {
const el = document.querySelector('.js-iteration-cadence-app');
......@@ -124,6 +151,8 @@ export function initCadenceApp({ namespaceType }) {
},
});
injectVueRouterIntoBreadcrumbs(router);
return new Vue({
el,
router,
......
import Vue from 'vue';
import VueRouter from 'vue-router';
import { __, s__ } from '~/locale';
import IterationCadenceForm from './components/iteration_cadence_form.vue';
import IterationCadenceList from './components/iteration_cadences_list.vue';
import IterationForm from './components/iteration_form.vue';
......@@ -12,36 +13,90 @@ function checkPermission(permission) {
if (permission) {
next();
} else {
next({ path: '/' });
next({ name: 'index' });
}
};
}
function renderChildren(children) {
return {
component: {
render(createElement) {
return createElement('router-view');
},
},
children: [
...children,
{
path: '*',
redirect: '/',
},
],
};
}
export default function createRouter({ base, permissions = {} }) {
const routes = [
{
name: 'index',
path: '/',
meta: {
breadcrumb: s__('Iterations|Iteration cadences'),
},
...renderChildren([
{
name: 'index',
path: '',
component: IterationCadenceList,
},
{
name: 'new',
path: '/new',
path: 'new',
component: IterationCadenceForm,
beforeEnter: checkPermission(permissions.canCreateCadence),
meta: {
breadcrumb: s__('Iterations|New iteration cadence'),
},
},
{
path: '/:cadenceId',
...renderChildren([
{
name: 'cadence',
path: '/:cadenceId',
redirect: '/',
},
{
name: 'edit',
path: '/:cadenceId/edit',
component: IterationCadenceForm,
beforeEnter: checkPermission(permissions.canEditCadence),
meta: {
breadcrumb: __('Edit'),
},
},
{
path: 'iterations',
meta: {
breadcrumb: __('Iterations'),
},
...renderChildren([
{
name: 'iterations',
path: '/:cadenceId/iterations',
redirect: '/',
},
{
name: 'newIteration',
path: '/:cadenceId/iterations/new',
component: IterationForm,
beforeEnter: checkPermission(permissions.canCreateIteration),
meta: {
breadcrumb: s__('Iterations|New iteration'),
},
},
{
path: ':iterationId',
...renderChildren([
{
name: 'iteration',
path: '/:cadenceId/iterations/:iterationId',
......@@ -49,13 +104,20 @@ export default function createRouter({ base, permissions = {} }) {
},
{
name: 'editIteration',
path: '/:cadenceId/iterations/:iterationId/edit',
path: 'edit',
component: IterationForm,
beforeEnter: checkPermission(permissions.canEditIteration),
meta: {
breadcrumb: __('Edit'),
},
{
path: '*',
redirect: '/',
},
]),
},
]),
},
]),
},
]),
},
];
......
- page_title _('Iteration cadences')
- page_title s_('Iterations|Iteration cadences')
.js-iteration-cadence-app{ data: { group_full_path: @group.full_path,
cadences_list_path: group_iteration_cadences_path(@group),
......
- add_to_breadcrumbs _("Iterations"), group_iterations_path(@group)
- breadcrumb_title params[:id]
- page_title _("Edit iteration")
- page_title s_("Iterations|Edit iteration")
- if Feature.enabled?(:group_iterations, @group, default_enabled: true)
.js-iteration{ data: { full_path: @group.full_path,
......
......@@ -55,14 +55,16 @@ RSpec.describe 'User edits iteration cadence', :js do
it 'redirects to list page when loading edit cadence page' do
visit edit_group_iteration_cadence_path(cadence.group, id: cadence.id)
# vue-router has trailing slash but _path helper doesn't
# vue-router has trailing slash which apparently cannot be removed
# until version 4 - https://github.com/vuejs/vue-router/issues/2945
expect(page).to have_current_path("#{group_iteration_cadences_path(cadence.group)}/")
end
it 'redirects to list page when loading new cadence page' do
visit new_group_iteration_cadence_path(cadence.group)
# vue-router has trailing slash but _path helper doesn't
# vue-router has trailing slash which apparently cannot be removed
# until version 4 - https://github.com/vuejs/vue-router/issues/2945
expect(page).to have_current_path("#{group_iteration_cadences_path(cadence.group)}/")
end
end
......
......@@ -73,7 +73,7 @@ RSpec.describe 'User edits iteration' do
it 'prefills fields and updates URL' do
find(dropdown_selector).click
click_link_or_button('Edit iteration')
click_link_or_button('Edit')
aggregate_failures do
expect(title_input.value).to eq(iteration.title)
......
import { mount } from '@vue/test-utils';
import component from 'ee/iterations/components/iteration_breadcrumb.vue';
import createRouter from 'ee/iterations/router';
describe('Iteration Breadcrumb', () => {
let router;
let wrapper;
const base = '/';
const permissions = {
canCreateCadence: true,
canEditCadence: true,
canCreateIteration: true,
canEditIteration: true,
};
const cadenceId = 1234;
const iterationId = 4567;
const mountComponent = () => {
router = createRouter({ base, permissions });
wrapper = mount(component, {
router,
});
};
beforeEach(() => {
mountComponent();
});
afterEach(() => {
wrapper.destroy();
router = null;
});
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);
});
it('links to new cadence form page', async () => {
await router.push({ name: 'new' });
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('links to edit cadence form page', async () => {
await router.push({ name: 'edit', params: { cadenceId } });
const links = wrapper.findAll('a');
expect(links).toHaveLength(3);
expect(links.at(2).attributes('href')).toBe(`/${cadenceId}/edit`);
});
it('links to iteration page', async () => {
await router.push({ name: 'iteration', params: { cadenceId, iterationId } });
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}`);
});
it('links to edit iteration page', async () => {
await router.push({ name: 'editIteration', params: { cadenceId, iterationId } });
const links = wrapper.findAll('a');
expect(links).toHaveLength(5);
expect(links.at(4).attributes('href')).toBe(`/${cadenceId}/iterations/${iterationId}/edit`);
});
it('links to new iteration page', async () => {
await router.push({ name: 'newIteration', params: { cadenceId } });
const links = wrapper.findAll('a');
expect(links).toHaveLength(4);
expect(links.at(3).attributes('href')).toBe(`/${cadenceId}/iterations/new`);
});
});
......@@ -11835,9 +11835,6 @@ msgstr ""
msgid "Edit issues"
msgstr ""
msgid "Edit iteration"
msgstr ""
msgid "Edit public deploy key"
msgstr ""
......@@ -18315,9 +18312,6 @@ msgstr ""
msgid "Iteration"
msgstr ""
msgid "Iteration cadences"
msgstr ""
msgid "Iteration changed to"
msgstr ""
......@@ -18360,6 +18354,9 @@ msgstr ""
msgid "Iterations|Edit cadence"
msgstr ""
msgid "Iterations|Edit iteration"
msgstr ""
msgid "Iterations|Edit iteration cadence"
msgstr ""
......@@ -18369,12 +18366,18 @@ msgstr ""
msgid "Iterations|Future iterations"
msgstr ""
msgid "Iterations|Iteration cadences"
msgstr ""
msgid "Iterations|Iteration scheduling will be handled automatically"
msgstr ""
msgid "Iterations|Move incomplete issues to the next iteration"
msgstr ""
msgid "Iterations|New iteration"
msgstr ""
msgid "Iterations|New iteration cadence"
msgstr ""
......@@ -21945,9 +21948,6 @@ msgstr ""
msgid "New issue title"
msgstr ""
msgid "New iteration"
msgstr ""
msgid "New iteration created"
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