Commit 61820f46 authored by Vitaly Slobodin's avatar Vitaly Slobodin

Merge branch 'psi-cadence-bread' into 'master'

Start adding cadence breadcrumbs

See merge request gitlab-org/gitlab!65239
parents f7a6ece5 7b37a573
<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 { ...@@ -182,7 +182,7 @@ export default {
<div> <div>
<div class="gl-display-flex"> <div class="gl-display-flex">
<h3 ref="pageTitle" class="page-title"> <h3 ref="pageTitle" class="page-title">
{{ isEditing ? __('Edit iteration') : __('New iteration') }} {{ isEditing ? s__('Iterations|Edit iteration') : s__('Iterations|New iteration') }}
</h3> </h3>
</div> </div>
<hr class="gl-mt-0" /> <hr class="gl-mt-0" />
......
...@@ -150,7 +150,7 @@ export default { ...@@ -150,7 +150,7 @@ export default {
<div> <div>
<div class="gl-display-flex"> <div class="gl-display-flex">
<h3 ref="pageTitle" class="page-title"> <h3 ref="pageTitle" class="page-title">
{{ isEditing ? __('Edit iteration') : __('New iteration') }} {{ isEditing ? s__('Iterations|Edit iteration') : s__('Iterations|New iteration') }}
</h3> </h3>
</div> </div>
<hr class="gl-mt-0" /> <hr class="gl-mt-0" />
......
...@@ -146,7 +146,7 @@ export default { ...@@ -146,7 +146,7 @@ export default {
<template #button-content> <template #button-content>
<gl-icon name="ellipsis_v" /><span class="gl-sr-only">{{ __('Actions') }}</span> <gl-icon name="ellipsis_v" /><span class="gl-sr-only">{{ __('Actions') }}</span>
</template> </template>
<gl-dropdown-item :to="editPage">{{ __('Edit iteration') }}</gl-dropdown-item> <gl-dropdown-item :to="editPage">{{ __('Edit') }}</gl-dropdown-item>
</gl-dropdown> </gl-dropdown>
</div> </div>
<h3 ref="title" class="page-title">{{ iteration.title }}</h3> <h3 ref="title" class="page-title">{{ iteration.title }}</h3>
......
...@@ -217,7 +217,7 @@ export default { ...@@ -217,7 +217,7 @@ export default {
<template #button-content> <template #button-content>
<gl-icon name="ellipsis_v" /><span class="gl-sr-only">{{ __('Actions') }}</span> <gl-icon name="ellipsis_v" /><span class="gl-sr-only">{{ __('Actions') }}</span>
</template> </template>
<gl-dropdown-item @click="loadEditPage">{{ __('Edit iteration') }}</gl-dropdown-item> <gl-dropdown-item @click="loadEditPage">{{ __('Edit') }}</gl-dropdown-item>
</gl-dropdown> </gl-dropdown>
</div> </div>
<h3 ref="title" class="page-title">{{ iteration.title }}</h3> <h3 ref="title" class="page-title">{{ iteration.title }}</h3>
......
...@@ -171,7 +171,7 @@ export default { ...@@ -171,7 +171,7 @@ export default {
data-qa-selector="new_iteration_button" data-qa-selector="new_iteration_button"
:href="newIterationPath" :href="newIterationPath"
> >
{{ __('New iteration') }} {{ s__('Iterations|New iteration') }}
</gl-button> </gl-button>
</li> </li>
</template> </template>
......
...@@ -3,6 +3,7 @@ import VueApollo from 'vue-apollo'; ...@@ -3,6 +3,7 @@ import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql'; import createDefaultClient from '~/lib/graphql';
import { parseBoolean } from '~/lib/utils/common_utils'; import { parseBoolean } from '~/lib/utils/common_utils';
import App from './components/app.vue'; import App from './components/app.vue';
import IterationBreadcrumb from './components/iteration_breadcrumb.vue';
import IterationForm from './components/iteration_form_without_vue_router.vue'; import IterationForm from './components/iteration_form_without_vue_router.vue';
import IterationReport from './components/iteration_report_without_vue_router.vue'; import IterationReport from './components/iteration_report_without_vue_router.vue';
import Iterations from './components/iterations.vue'; import Iterations from './components/iterations.vue';
...@@ -95,6 +96,32 @@ export function initIterationReport({ namespaceType, initiallyEditing } = {}) { ...@@ -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 }) { export function initCadenceApp({ namespaceType }) {
const el = document.querySelector('.js-iteration-cadence-app'); const el = document.querySelector('.js-iteration-cadence-app');
...@@ -124,6 +151,8 @@ export function initCadenceApp({ namespaceType }) { ...@@ -124,6 +151,8 @@ export function initCadenceApp({ namespaceType }) {
}, },
}); });
injectVueRouterIntoBreadcrumbs(router);
return new Vue({ return new Vue({
el, el,
router, router,
......
import Vue from 'vue'; import Vue from 'vue';
import VueRouter from 'vue-router'; import VueRouter from 'vue-router';
import { __, s__ } from '~/locale';
import IterationCadenceForm from './components/iteration_cadence_form.vue'; import IterationCadenceForm from './components/iteration_cadence_form.vue';
import IterationCadenceList from './components/iteration_cadences_list.vue'; import IterationCadenceList from './components/iteration_cadences_list.vue';
import IterationForm from './components/iteration_form.vue'; import IterationForm from './components/iteration_form.vue';
...@@ -12,50 +13,111 @@ function checkPermission(permission) { ...@@ -12,50 +13,111 @@ function checkPermission(permission) {
if (permission) { if (permission) {
next(); next();
} else { } 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 = {} }) { export default function createRouter({ base, permissions = {} }) {
const routes = [ const routes = [
{ {
name: 'index',
path: '/', path: '/',
component: IterationCadenceList, meta: {
}, breadcrumb: s__('Iterations|Iteration cadences'),
{ },
name: 'new', ...renderChildren([
path: '/new', {
component: IterationCadenceForm, name: 'index',
beforeEnter: checkPermission(permissions.canCreateCadence), path: '',
}, component: IterationCadenceList,
{ },
name: 'edit', {
path: '/:cadenceId/edit', name: 'new',
component: IterationCadenceForm, path: 'new',
beforeEnter: checkPermission(permissions.canEditCadence), component: IterationCadenceForm,
}, beforeEnter: checkPermission(permissions.canCreateCadence),
{ meta: {
name: 'newIteration', breadcrumb: s__('Iterations|New iteration cadence'),
path: '/:cadenceId/iterations/new', },
component: IterationForm, },
beforeEnter: checkPermission(permissions.canCreateIteration), {
}, path: '/:cadenceId',
{ ...renderChildren([
name: 'iteration', {
path: '/:cadenceId/iterations/:iterationId', name: 'cadence',
component: IterationReport, path: '/:cadenceId',
}, redirect: '/',
{ },
name: 'editIteration', {
path: '/:cadenceId/iterations/:iterationId/edit', name: 'edit',
component: IterationForm, path: '/:cadenceId/edit',
beforeEnter: checkPermission(permissions.canEditIteration), component: IterationCadenceForm,
}, beforeEnter: checkPermission(permissions.canEditCadence),
{ meta: {
path: '*', breadcrumb: __('Edit'),
redirect: '/', },
},
{
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',
component: IterationReport,
},
{
name: 'editIteration',
path: 'edit',
component: IterationForm,
beforeEnter: checkPermission(permissions.canEditIteration),
meta: {
breadcrumb: __('Edit'),
},
},
]),
},
]),
},
]),
},
]),
}, },
]; ];
......
- page_title _('Iteration cadences') - page_title s_('Iterations|Iteration cadences')
.js-iteration-cadence-app{ data: { group_full_path: @group.full_path, .js-iteration-cadence-app{ data: { group_full_path: @group.full_path,
cadences_list_path: group_iteration_cadences_path(@group), cadences_list_path: group_iteration_cadences_path(@group),
......
- add_to_breadcrumbs _("Iterations"), group_iterations_path(@group) - add_to_breadcrumbs _("Iterations"), group_iterations_path(@group)
- breadcrumb_title params[:id] - breadcrumb_title params[:id]
- page_title _("Edit iteration") - page_title s_("Iterations|Edit iteration")
- if Feature.enabled?(:group_iterations, @group, default_enabled: true) - if Feature.enabled?(:group_iterations, @group, default_enabled: true)
.js-iteration{ data: { full_path: @group.full_path, .js-iteration{ data: { full_path: @group.full_path,
......
...@@ -55,14 +55,16 @@ RSpec.describe 'User edits iteration cadence', :js do ...@@ -55,14 +55,16 @@ RSpec.describe 'User edits iteration cadence', :js do
it 'redirects to list page when loading edit cadence page' do it 'redirects to list page when loading edit cadence page' do
visit edit_group_iteration_cadence_path(cadence.group, id: cadence.id) 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)}/") expect(page).to have_current_path("#{group_iteration_cadences_path(cadence.group)}/")
end end
it 'redirects to list page when loading new cadence page' do it 'redirects to list page when loading new cadence page' do
visit new_group_iteration_cadence_path(cadence.group) 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)}/") expect(page).to have_current_path("#{group_iteration_cadences_path(cadence.group)}/")
end end
end end
......
...@@ -73,7 +73,7 @@ RSpec.describe 'User edits iteration' do ...@@ -73,7 +73,7 @@ RSpec.describe 'User edits iteration' do
it 'prefills fields and updates URL' do it 'prefills fields and updates URL' do
find(dropdown_selector).click find(dropdown_selector).click
click_link_or_button('Edit iteration') click_link_or_button('Edit')
aggregate_failures do aggregate_failures do
expect(title_input.value).to eq(iteration.title) 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`);
});
});
...@@ -11850,9 +11850,6 @@ msgstr "" ...@@ -11850,9 +11850,6 @@ msgstr ""
msgid "Edit issues" msgid "Edit issues"
msgstr "" msgstr ""
msgid "Edit iteration"
msgstr ""
msgid "Edit public deploy key" msgid "Edit public deploy key"
msgstr "" msgstr ""
...@@ -18330,9 +18327,6 @@ msgstr "" ...@@ -18330,9 +18327,6 @@ msgstr ""
msgid "Iteration" msgid "Iteration"
msgstr "" msgstr ""
msgid "Iteration cadences"
msgstr ""
msgid "Iteration changed to" msgid "Iteration changed to"
msgstr "" msgstr ""
...@@ -18375,6 +18369,9 @@ msgstr "" ...@@ -18375,6 +18369,9 @@ msgstr ""
msgid "Iterations|Edit cadence" msgid "Iterations|Edit cadence"
msgstr "" msgstr ""
msgid "Iterations|Edit iteration"
msgstr ""
msgid "Iterations|Edit iteration cadence" msgid "Iterations|Edit iteration cadence"
msgstr "" msgstr ""
...@@ -18384,12 +18381,18 @@ msgstr "" ...@@ -18384,12 +18381,18 @@ msgstr ""
msgid "Iterations|Future iterations" msgid "Iterations|Future iterations"
msgstr "" msgstr ""
msgid "Iterations|Iteration cadences"
msgstr ""
msgid "Iterations|Iteration scheduling will be handled automatically" msgid "Iterations|Iteration scheduling will be handled automatically"
msgstr "" msgstr ""
msgid "Iterations|Move incomplete issues to the next iteration" msgid "Iterations|Move incomplete issues to the next iteration"
msgstr "" msgstr ""
msgid "Iterations|New iteration"
msgstr ""
msgid "Iterations|New iteration cadence" msgid "Iterations|New iteration cadence"
msgstr "" msgstr ""
...@@ -21960,9 +21963,6 @@ msgstr "" ...@@ -21960,9 +21963,6 @@ msgstr ""
msgid "New issue title" msgid "New issue title"
msgstr "" msgstr ""
msgid "New iteration"
msgstr ""
msgid "New iteration created" msgid "New iteration created"
msgstr "" 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