Commit 7bf305dc authored by Andrew Fontaine's avatar Andrew Fontaine

Merge branch...

Merge branch '229621-new-epic-button-in-epic-list-should-direct-the-user-to-a-full-epic-creation-page-2' into 'master'

New epic button in Epic Roadmap empty state should direct the user to a full epic creation page

See merge request gitlab-org/gitlab!45948
parents 3082c033 4bb08b91
...@@ -16,13 +16,15 @@ to them. ...@@ -16,13 +16,15 @@ to them.
> - The New Epic form [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/211533) in [GitLab Premium](https://about.gitlab.com/pricing/) 13.2. > - The New Epic form [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/211533) in [GitLab Premium](https://about.gitlab.com/pricing/) 13.2.
> - In [GitLab 13.7](https://gitlab.com/gitlab-org/gitlab/-/issues/229621) and later, the New Epic button on the Epics list opens the New Epic form. > - In [GitLab 13.7](https://gitlab.com/gitlab-org/gitlab/-/issues/229621) and later, the New Epic button on the Epics list opens the New Epic form.
> - In [GitLab 13.9](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/45948) and later, you can create a new epic from an empty Roadmap.
To create an epic in the group you're in: To create an epic in the group you're in:
1. Get to the New Epic form: 1. Get to the New Epic form:
- From the **Epics** list in your group, select the **New Epic** button. - From the **Epics** list in your group, select **New epic**.
- From an epic in your group, select the **New Epic** button. - From an epic in your group, select **New epic**.
- From anywhere, in the top menu, select **New...** (**{plus-square}**) **> New epic**. - From anywhere, in the top menu, select **New...** (**{plus-square}**) **> New epic**.
- In an empty [roadmap](../roadmap/index.md), select **New epic**.
![New epic from an open epic](img/new_epic_from_groups_v13.7.png) ![New epic from an open epic](img/new_epic_from_groups_v13.7.png)
...@@ -39,7 +41,7 @@ To create an epic in the group you're in: ...@@ -39,7 +41,7 @@ To create an epic in the group you're in:
## Edit an epic ## Edit an epic
After you create an epic, you can edit change the following details: After you create an epic, you can edit the following details:
- Title - Title
- Description - Description
...@@ -152,6 +154,9 @@ To make an epic confidential: ...@@ -152,6 +154,9 @@ To make an epic confidential:
## Manage issues assigned to an epic ## Manage issues assigned to an epic
This section collects instructions for all the things you can do with [issues](../../project/issues/index.md)
in relation to epics.
### Add a new issue to an epic ### Add a new issue to an epic
You can add an existing issue to an epic, or create a new issue that's You can add an existing issue to an epic, or create a new issue that's
......
<script>
import { mapState, mapActions } from 'vuex';
import {
GlForm,
GlFormInput,
GlFormCheckbox,
GlIcon,
GlButton,
GlTooltipDirective,
} from '@gitlab/ui';
import { __ } from '~/locale';
import autofocusonshow from '~/vue_shared/directives/autofocusonshow';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default {
components: {
GlFormCheckbox,
GlIcon,
GlButton,
GlForm,
GlFormInput,
},
directives: {
autofocusonshow,
GlTooltip: GlTooltipDirective,
},
mixins: [glFeatureFlagsMixin()],
props: {
alignRight: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
...mapState(['newEpicTitle', 'newEpicConfidential', 'epicCreateInProgress']),
buttonLabel() {
return this.epicCreateInProgress ? __('Creating epic') : __('Create epic');
},
isEpicCreateDisabled() {
return !this.newEpicTitle.length;
},
epicTitle: {
set(value) {
this.setEpicCreateTitle({
newEpicTitle: value,
});
},
get() {
return this.newEpicTitle;
},
},
epicConfidential: {
set(value) {
this.setEpicCreateConfidential({
newEpicConfidential: value,
});
},
get() {
return this.newEpicConfidential;
},
},
},
methods: {
...mapActions(['setEpicCreateTitle', 'createEpic', 'setEpicCreateConfidential']),
},
};
</script>
<template>
<div class="dropdown epic-create-dropdown">
<gl-button
category="primary"
variant="success"
data-qa-selector="new_epic_button"
data-toggle="dropdown"
>
{{ __('New epic') }}
</gl-button>
<div :class="{ 'dropdown-menu-right': alignRight }" class="dropdown-menu">
<gl-form>
<gl-form-input
ref="epicTitleInput"
v-model="epicTitle"
v-autofocusonshow
:disabled="epicCreateInProgress"
:placeholder="__('Title')"
type="text"
class="form-control"
data-qa-selector="epic_title_field"
@keyup.enter.exact="createEpic"
/>
<gl-form-checkbox
v-model="epicConfidential"
class="mt-3 mb-3 mr-0"
data-qa-selector="confidential_epic_checkbox"
><span> {{ __('Make this epic confidential') }} </span>
<span
v-gl-tooltip.viewport.top.hover
:title="
__(
'This epic and its child elements will only be visible to team members with at minimum Reporter access.',
)
"
:aria-label="
__(
'This epic and its child elements will only be visible to team members with at minimum Reporter access.',
)
"
>
<gl-icon name="question" :size="12"
/></span>
</gl-form-checkbox>
<gl-button
:disabled="isEpicCreateDisabled"
:loading="epicCreateInProgress"
category="primary"
variant="success"
class="gl-mt-3"
data-qa-selector="create_epic_button"
@click.stop="createEpic"
>{{ buttonLabel }}</gl-button
>
</gl-form>
</div>
</div>
</template>
...@@ -9,10 +9,9 @@ import { parseIssuableData } from '~/issue_show/utils/parse_data'; ...@@ -9,10 +9,9 @@ import { parseIssuableData } from '~/issue_show/utils/parse_data';
import createStore from './store'; import createStore from './store';
import EpicApp from './components/epic_app.vue'; import EpicApp from './components/epic_app.vue';
import EpicCreateApp from './components/epic_create.vue';
export default (epicCreate = false) => { export default () => {
const el = document.getElementById(epicCreate ? 'epic-create-root' : 'epic-app-root'); const el = document.getElementById('epic-app-root');
if (!el) { if (!el) {
return false; return false;
...@@ -21,28 +20,6 @@ export default (epicCreate = false) => { ...@@ -21,28 +20,6 @@ export default (epicCreate = false) => {
const store = createStore(); const store = createStore();
store.registerModule('labelsSelect', labelsSelectModule()); store.registerModule('labelsSelect', labelsSelectModule());
if (epicCreate) {
return new Vue({
el,
store,
components: { EpicCreateApp },
created() {
this.setEpicMeta({
endpoint: el.dataset.endpoint,
});
},
methods: {
...mapActions(['setEpicMeta']),
},
render: (createElement) =>
createElement('epic-create-app', {
props: {
alignRight: el.dataset.alignRight,
},
}),
});
}
const epicMeta = convertObjectPropsToCamelCase(JSON.parse(el.dataset.meta), { deep: true }); const epicMeta = convertObjectPropsToCamelCase(JSON.parse(el.dataset.meta), { deep: true });
const epicData = parseIssuableData(el); const epicData = parseIssuableData(el);
......
<script> <script>
/* eslint-disable vue/no-v-html */ import { GlButton, GlSafeHtmlDirective } from '@gitlab/ui';
import { GlButton } from '@gitlab/ui';
import { s__, sprintf } from '~/locale'; import { s__, sprintf } from '~/locale';
import { dateInWords } from '~/lib/utils/datetime_utility'; import { dateInWords } from '~/lib/utils/datetime_utility';
import CommonMixin from '../mixins/common_mixin'; import CommonMixin from '../mixins/common_mixin';
import { emptyStateDefault, emptyStateWithFilters } from '../constants'; import { emptyStateDefault, emptyStateWithFilters } from '../constants';
import initEpicCreate from '../../epic/epic_bundle';
export default { export default {
components: { components: {
GlButton, GlButton,
}, },
directives: {
SafeHtml: GlSafeHtmlDirective,
},
mixins: [CommonMixin], mixins: [CommonMixin],
inject: ['newEpicPath', 'listEpicsPath', 'epicsDocsPath'],
props: { props: {
presetType: { presetType: {
type: String, type: String,
...@@ -31,10 +32,6 @@ export default { ...@@ -31,10 +32,6 @@ export default {
type: Boolean, type: Boolean,
required: true, required: true,
}, },
newEpicEndpoint: {
type: String,
required: true,
},
emptyStateIllustrationPath: { emptyStateIllustrationPath: {
type: String, type: String,
required: true, required: true,
...@@ -96,8 +93,7 @@ export default { ...@@ -96,8 +93,7 @@ export default {
'GroupRoadmap|To view the roadmap, add a start or due date to one of the %{linkStart}child epics%{linkEnd}.', 'GroupRoadmap|To view the roadmap, add a start or due date to one of the %{linkStart}child epics%{linkEnd}.',
), ),
{ {
linkStart: linkStart: `<a href="${this.epicsDocsPath}#multi-level-child-epics" target="_blank" rel="noopener noreferrer nofollow">`,
'<a href="https://docs.gitlab.com/ee/user/group/epics/#multi-level-child-epics" target="_blank" rel="noopener noreferrer nofollow">',
linkEnd: '</a>', linkEnd: '</a>',
}, },
false, false,
...@@ -116,36 +112,38 @@ export default { ...@@ -116,36 +112,38 @@ export default {
}); });
}, },
}, },
mounted() {
// If filters are not applied and yet user
// is seeing empty state, we need to show
// `New epic` button, so boot-up Epic app
// in create mode.
if (!this.hasFiltersApplied) {
initEpicCreate(true);
}
},
}; };
</script> </script>
<template> <template>
<div class="row empty-state"> <div class="row empty-state">
<div class="col-12"> <div class="col-12">
<div class="svg-content"><img :src="emptyStateIllustrationPath" /></div> <div class="svg-content">
<img :src="emptyStateIllustrationPath" data-testid="illustration" />
</div>
</div> </div>
<div class="col-12"> <div class="col-12">
<div class="text-content"> <div class="text-content">
<h4>{{ message }}</h4> <h4 data-testid="title">{{ message }}</h4>
<p v-html="subMessage"></p> <p v-safe-html="subMessage" data-testid="sub-title"></p>
<div class="text-center">
<div <div class="gl-text-center">
<gl-button
v-if="!hasFiltersApplied" v-if="!hasFiltersApplied"
id="epic-create-root" :href="newEpicPath"
:data-endpoint="newEpicEndpoint" variant="success"
></div> class="gl-mt-3 gl-sm-mt-0! gl-w-full gl-sm-w-auto!"
<gl-button :title="__('List')" :href="newEpicEndpoint">{{ data-testid="new-epic-button"
s__('View epics list') >
}}</gl-button> {{ __('New epic') }}
</gl-button>
<gl-button
:href="listEpicsPath"
class="gl-mt-3 gl-sm-mt-0! gl-sm-ml-3 gl-w-full gl-sm-w-auto!"
data-testid="list-epics-button"
>
{{ __('View epics list') }}
</gl-button>
</div> </div>
</div> </div>
</div> </div>
......
...@@ -37,10 +37,6 @@ export default { ...@@ -37,10 +37,6 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
newEpicEndpoint: {
type: String,
required: true,
},
emptyStateIllustrationPath: { emptyStateIllustrationPath: {
type: String, type: String,
required: true, required: true,
...@@ -179,7 +175,6 @@ export default { ...@@ -179,7 +175,6 @@ export default {
:timeframe-start="timeframeStart" :timeframe-start="timeframeStart"
:timeframe-end="timeframeEnd" :timeframe-end="timeframeEnd"
:has-filters-applied="hasFiltersApplied" :has-filters-applied="hasFiltersApplied"
:new-epic-endpoint="newEpicEndpoint"
:empty-state-illustration-path="emptyStateIllustrationPath" :empty-state-illustration-path="emptyStateIllustrationPath"
:is-child-epics="isChildEpics" :is-child-epics="isChildEpics"
/> />
......
...@@ -50,6 +50,15 @@ export default () => { ...@@ -50,6 +50,15 @@ export default () => {
components: { components: {
roadmapApp, roadmapApp,
}, },
provide() {
const { dataset } = this.$options.el;
return {
newEpicPath: dataset.newEpicPath,
listEpicsPath: dataset.listEpicsPath,
epicsDocsPath: dataset.epicsDocsPath,
};
},
data() { data() {
const supportedPresetTypes = Object.keys(PRESET_TYPES); const supportedPresetTypes = Object.keys(PRESET_TYPES);
const { dataset } = this.$options.el; const { dataset } = this.$options.el;
...@@ -83,7 +92,6 @@ export default () => { ...@@ -83,7 +92,6 @@ export default () => {
basePath: dataset.epicsPath, basePath: dataset.epicsPath,
fullPath: dataset.fullPath, fullPath: dataset.fullPath,
epicIid: dataset.iid, epicIid: dataset.iid,
newEpicEndpoint: dataset.newEpicEndpoint,
groupLabelsEndpoint: dataset.groupLabelsEndpoint, groupLabelsEndpoint: dataset.groupLabelsEndpoint,
groupMilestonesEndpoint: dataset.groupMilestonesEndpoint, groupMilestonesEndpoint: dataset.groupMilestonesEndpoint,
epicsState: dataset.epicsState, epicsState: dataset.epicsState,
...@@ -119,7 +127,6 @@ export default () => { ...@@ -119,7 +127,6 @@ export default () => {
return createElement('roadmap-app', { return createElement('roadmap-app', {
props: { props: {
presetType: this.presetType, presetType: this.presetType,
newEpicEndpoint: this.newEpicEndpoint,
emptyStateIllustrationPath: this.emptyStateIllustrationPath, emptyStateIllustrationPath: this.emptyStateIllustrationPath,
}, },
}); });
......
...@@ -66,7 +66,9 @@ ...@@ -66,7 +66,9 @@
full_path: @group.full_path, full_path: @group.full_path,
empty_state_illustration: image_path('illustrations/epics/roadmap.svg'), empty_state_illustration: image_path('illustrations/epics/roadmap.svg'),
has_filters_applied: 'false', has_filters_applied: 'false',
new_epic_endpoint: group_epics_path(@group), new_epic_path: new_group_epic_path(@group),
list_epics_path: group_epics_path(@group),
epics_docs_path: help_page_path('user/group/epics/index'),
preset_type: roadmap_layout, preset_type: roadmap_layout,
epics_state: 'all', epics_state: 'all',
sorted_by: roadmap_sort_order, sorted_by: roadmap_sort_order,
......
...@@ -20,7 +20,9 @@ ...@@ -20,7 +20,9 @@
full_path: @group.full_path, full_path: @group.full_path,
empty_state_illustration: image_path('illustrations/epics/roadmap.svg'), empty_state_illustration: image_path('illustrations/epics/roadmap.svg'),
has_filters_applied: "#{has_filters_applied}", has_filters_applied: "#{has_filters_applied}",
new_epic_endpoint: group_epics_path(@group), new_epic_path: new_group_epic_path(@group),
list_epics_path: group_epics_path(@group),
epics_docs_path: help_page_path('user/group/epics/index'),
group_labels_endpoint: group_labels_path(@group, format: :json), group_labels_endpoint: group_labels_path(@group, format: :json),
group_milestones_endpoint: group_milestones_path(@group, format: :json), group_milestones_endpoint: group_milestones_path(@group, format: :json),
preset_type: roadmap_layout, preset_type: roadmap_layout,
......
---
title: New epic button in Epic Roadmap empty state should direct the user to a full
epic creation page
merge_request: 45948
author:
type: changed
import Vue from 'vue';
import EpicCreate from 'ee/epic/components/epic_create.vue';
import createStore from 'ee/epic/store';
import { mountComponentWithStore } from 'helpers/vue_mount_component_helper';
import { mockEpicMeta } from '../mock_data';
describe('EpicCreateComponent', () => {
let vm;
let store;
beforeEach(() => {
const Component = Vue.extend(EpicCreate);
store = createStore();
store.dispatch('setEpicMeta', mockEpicMeta);
vm = mountComponentWithStore(Component, {
store,
});
});
afterEach(() => {
vm.$destroy();
});
describe('computed', () => {
describe('buttonLabel', () => {
it('returns string `Create epic` when `epicCreateInProgress` is false', () => {
vm.$store.state.epicCreateInProgress = false;
expect(vm.buttonLabel).toBe('Create epic');
});
it('returns string `Creating epic` when `epicCreateInProgress` is true', () => {
vm.$store.state.epicCreateInProgress = true;
expect(vm.buttonLabel).toBe('Creating epic');
});
});
describe('isEpicCreateDisabled', () => {
it('returns `true` when `newEpicTitle` is an empty string', () => {
vm.$store.state.newEpicTitle = '';
expect(vm.isEpicCreateDisabled).toBe(true);
});
it('returns `false` when `newEpicTitle` is not empty', () => {
vm.$store.state.newEpicTitle = 'foobar';
expect(vm.isEpicCreateDisabled).toBe(false);
});
});
describe('epicTitle', () => {
describe('set', () => {
it('calls `setEpicCreateTitle` with param `value`', () => {
jest.spyOn(vm, 'setEpicCreateTitle');
const newEpicTitle = 'foobar';
vm.epicTitle = newEpicTitle;
expect(vm.setEpicCreateTitle).toHaveBeenCalledWith(
expect.objectContaining({
newEpicTitle,
}),
);
});
});
describe('get', () => {
it('returns value of `newEpicTitle` from state', () => {
const newEpicTitle = 'foobar';
vm.$store.state.newEpicTitle = newEpicTitle;
expect(vm.epicTitle).toBe(newEpicTitle);
});
});
});
describe('epicConfidential', () => {
describe('set', () => {
it('calls `setEpicCreateConfidential` with param `value`', () => {
jest.spyOn(vm, 'setEpicCreateConfidential');
const newEpicConfidential = true;
vm.epicConfidential = newEpicConfidential;
expect(vm.setEpicCreateConfidential).toHaveBeenCalledWith(
expect.objectContaining({
newEpicConfidential,
}),
);
});
});
describe('get', () => {
it('returns value of `newEpicConfidential` from state', () => {
const newEpicConfidential = true;
vm.$store.state.newEpicConfidential = newEpicConfidential;
expect(vm.epicConfidential).toBe(newEpicConfidential);
});
});
});
});
describe('template', () => {
it('renders component container element with classes `dropdown` & `epic-create-dropdown`', () => {
expect(vm.$el.classList.contains('dropdown')).toBe(true);
expect(vm.$el.classList.contains('epic-create-dropdown')).toBe(true);
});
it('renders new epic button element', () => {
const newEpicButtonEl = vm.$el.querySelector('button.btn-success');
expect(newEpicButtonEl).not.toBeNull();
expect(newEpicButtonEl.innerText.trim()).toBe('New epic');
});
it('renders new epic dropdown menu element', () => {
const dropdownMenuEl = vm.$el.querySelector('.dropdown-menu');
expect(dropdownMenuEl).not.toBeNull();
});
it('renders epic input textbox element', () => {
const inputEl = vm.$el.querySelector('.dropdown-menu input.form-control');
expect(inputEl).not.toBeNull();
expect(inputEl.placeholder).toBe('Title');
});
it('renders create epic button element', () => {
const createEpicButtonEl = vm.$el.querySelector('.dropdown-menu button.btn-success');
expect(createEpicButtonEl).not.toBeNull();
expect(createEpicButtonEl.innerText.trim()).toBe('Create epic');
});
});
});
import Vue from 'vue'; import { shallowMount } from '@vue/test-utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import epicsListEmptyComponent from 'ee/roadmap/components/epics_list_empty.vue'; import EpicsListEmpty from 'ee/roadmap/components/epics_list_empty.vue';
import { mockTimeframeInitialDate, mockSvgPath } from 'ee_jest/roadmap/mock_data';
import { PRESET_TYPES } from 'ee/roadmap/constants'; import { PRESET_TYPES } from 'ee/roadmap/constants';
import { TEST_HOST } from 'helpers/test_constants';
import { import {
getTimeframeForQuartersView, getTimeframeForQuartersView,
getTimeframeForWeeksView, getTimeframeForWeeksView,
getTimeframeForMonthsView, getTimeframeForMonthsView,
} from 'ee/roadmap/utils/roadmap_utils'; } from 'ee/roadmap/utils/roadmap_utils';
import { const TEST_EPICS_PATH = '/epics';
mockTimeframeInitialDate, const TEST_NEW_EPIC_PATH = '/epics/new';
mockSvgPath,
mockNewEpicEndpoint,
} from 'ee_jest/roadmap/mock_data';
import mountComponent from 'helpers/vue_mount_component_helper';
const mockTimeframeQuarters = getTimeframeForQuartersView(mockTimeframeInitialDate); const mockTimeframeQuarters = getTimeframeForQuartersView(mockTimeframeInitialDate);
const mockTimeframeMonths = getTimeframeForMonthsView(mockTimeframeInitialDate); const mockTimeframeMonths = getTimeframeForMonthsView(mockTimeframeInitialDate);
const mockTimeframeWeeks = getTimeframeForWeeksView(mockTimeframeInitialDate); const mockTimeframeWeeks = getTimeframeForWeeksView(mockTimeframeInitialDate);
const createComponent = ({ describe('ee/roadmap/components/epics_list_empty.vue', () => {
hasFiltersApplied = false, let wrapper;
presetType = PRESET_TYPES.MONTHS,
timeframeStart = mockTimeframeMonths[0], const createWrapper = ({
timeframeEnd = mockTimeframeMonths[mockTimeframeMonths.length - 1], isChildEpics = false,
}) => { hasFiltersApplied = false,
const Component = Vue.extend(epicsListEmptyComponent); presetType = PRESET_TYPES.MONTHS,
timeframeStart = mockTimeframeMonths[0],
return mountComponent(Component, { timeframeEnd = mockTimeframeMonths[mockTimeframeMonths.length - 1],
presetType, } = {}) => {
timeframeStart, wrapper = extendedWrapper(
timeframeEnd, shallowMount(EpicsListEmpty, {
emptyStateIllustrationPath: mockSvgPath, propsData: {
newEpicEndpoint: mockNewEpicEndpoint, presetType,
hasFiltersApplied, timeframeStart,
timeframeEnd,
emptyStateIllustrationPath: mockSvgPath,
hasFiltersApplied,
isChildEpics,
},
provide: {
newEpicPath: TEST_NEW_EPIC_PATH,
listEpicsPath: TEST_EPICS_PATH,
epicsDocsPath: TEST_HOST,
},
}),
);
};
afterEach(() => {
wrapper.destroy();
wrapper = null;
}); });
};
describe('EpicsListEmptyComponent', () => { const findTitle = () => wrapper.findByTestId('title');
let vm; const findSubTitle = () => wrapper.findByTestId('sub-title');
const findNewEpicButton = () => wrapper.findByTestId('new-epic-button');
const findListEpicsButton = () => wrapper.findByTestId('list-epics-button');
const findIllustration = () => wrapper.findByTestId('illustration');
beforeEach(() => { it('renders default message', () => {
vm = createComponent({}); createWrapper({});
expect(findTitle().text()).toBe(wrapper.vm.message);
}); });
afterEach(() => { it('renders empty state message when `hasFiltersApplied` prop is true', () => {
vm.$destroy(); createWrapper({ hasFiltersApplied: true });
expect(findTitle().text()).toBe('Sorry, no epics matched your search');
}); });
describe('computed', () => { describe('with presetType `QUARTERS`', () => {
describe('message', () => { it('renders default empty state sub-title when `hasFiltersApplied` props is false', () => {
it('returns default empty state message', () => { createWrapper({
expect(vm.message).toBe('The roadmap shows the progress of your epics along a timeline'); presetType: PRESET_TYPES.QUARTERS,
timeframeStart: mockTimeframeQuarters[0],
timeframeEnd: mockTimeframeQuarters[mockTimeframeQuarters.length - 1],
}); });
it('returns empty state message when `hasFiltersApplied` prop is true', (done) => { expect(findSubTitle().text()).toBe(
vm.hasFiltersApplied = true; 'To view the roadmap, add a start or due date to one of your epics in this group or its subgroups; from Jul 1, 2017 to Mar 31, 2019.',
Vue.nextTick() );
.then(() => {
expect(vm.message).toBe('Sorry, no epics matched your search');
})
.then(done)
.catch(done.fail);
});
}); });
describe('subMessage', () => { it('renders empty state sub-title when `hasFiltersApplied` prop is true', () => {
describe('with presetType `QUARTERS`', () => { createWrapper({
beforeEach(() => { presetType: PRESET_TYPES.QUARTERS,
vm.presetType = PRESET_TYPES.QUARTERS; timeframeStart: mockTimeframeQuarters[0],
[vm.timeframeStart] = mockTimeframeQuarters; timeframeEnd: mockTimeframeQuarters[mockTimeframeQuarters.length - 1],
vm.timeframeEnd = mockTimeframeQuarters[mockTimeframeQuarters.length - 1]; hasFiltersApplied: true,
});
it('returns default empty state sub-message when `hasFiltersApplied` props is false', (done) => {
Vue.nextTick()
.then(() => {
expect(vm.subMessage).toBe(
'To view the roadmap, add a start or due date to one of your epics in this group or its subgroups; from Jul 1, 2017 to Mar 31, 2019.',
);
})
.then(done)
.catch(done.fail);
});
it('returns empty state sub-message when `hasFiltersApplied` prop is true', (done) => {
vm.hasFiltersApplied = true;
Vue.nextTick()
.then(() => {
expect(vm.subMessage).toBe(
'To widen your search, change or remove filters; from Jul 1, 2017 to Mar 31, 2019.',
);
})
.then(done)
.catch(done.fail);
});
}); });
describe('with presetType `MONTHS`', () => { expect(findSubTitle().text()).toBe(
beforeEach(() => { 'To widen your search, change or remove filters; from Jul 1, 2017 to Mar 31, 2019.',
vm.presetType = PRESET_TYPES.MONTHS; );
}); });
});
it('returns default empty state sub-message when `hasFiltersApplied` props is false', (done) => {
Vue.nextTick()
.then(() => {
expect(vm.subMessage).toBe(
'To view the roadmap, add a start or due date to one of your epics in this group or its subgroups; from Nov 1, 2017 to Jun 30, 2018.',
);
})
.then(done)
.catch(done.fail);
});
it('returns empty state sub-message when `hasFiltersApplied` prop is true', (done) => {
vm.hasFiltersApplied = true;
Vue.nextTick()
.then(() => {
expect(vm.subMessage).toBe(
'To widen your search, change or remove filters; from Nov 1, 2017 to Jun 30, 2018.',
);
})
.then(done)
.catch(done.fail);
});
});
describe('with presetType `WEEKS`', () => { describe('with presetType `MONTHS`', () => {
beforeEach(() => { it('renders default empty state sub-title when `hasFiltersApplied` props is false', () => {
const timeframeEnd = mockTimeframeWeeks[mockTimeframeWeeks.length - 1]; createWrapper({
timeframeEnd.setDate(timeframeEnd.getDate() + 6); presetType: PRESET_TYPES.MONTHS,
vm.presetType = PRESET_TYPES.WEEKS;
[vm.timeframeStart] = mockTimeframeWeeks;
vm.timeframeEnd = timeframeEnd;
});
it('returns default empty state sub-message when `hasFiltersApplied` props is false', (done) => {
Vue.nextTick()
.then(() => {
expect(vm.subMessage).toBe(
'To view the roadmap, add a start or due date to one of your epics in this group or its subgroups; from Dec 17, 2017 to Feb 9, 2018.',
);
})
.then(done)
.catch(done.fail);
});
it('returns empty state sub-message when `hasFiltersApplied` prop is true', (done) => {
vm.hasFiltersApplied = true;
Vue.nextTick()
.then(() => {
expect(vm.subMessage).toBe(
'To widen your search, change or remove filters; from Dec 17, 2017 to Feb 15, 2018.',
);
})
.then(done)
.catch(done.fail);
});
}); });
describe('with child epics context', () => { expect(findSubTitle().text()).toBe(
it('returns empty state sub-message when `isChildEpics` is set to `true`', (done) => { 'To view the roadmap, add a start or due date to one of your epics in this group or its subgroups; from Nov 1, 2017 to Jun 30, 2018.',
vm.isChildEpics = true; );
Vue.nextTick()
.then(() => {
expect(vm.subMessage).toBe(
'To view the roadmap, add a start or due date to one of the <a href="https://docs.gitlab.com/ee/user/group/epics/#multi-level-child-epics" target="_blank" rel="noopener noreferrer nofollow">child epics</a>.',
);
})
.then(done)
.catch(done.fail);
});
});
}); });
describe('timeframeRange', () => { it('renders empty state sub-title when `hasFiltersApplied` prop is true', () => {
it('returns correct timeframe startDate and endDate in words', () => { createWrapper({
expect(vm.timeframeRange.startDate).toBe('Nov 1, 2017'); presetType: PRESET_TYPES.MONTHS,
expect(vm.timeframeRange.endDate).toBe('Jun 30, 2018'); hasFiltersApplied: true,
}); });
});
});
describe('template', () => { expect(findSubTitle().text()).toBe(
it('renders empty state illustration in image element with provided `emptyStateIllustrationPath`', () => { 'To widen your search, change or remove filters; from Nov 1, 2017 to Jun 30, 2018.',
expect(vm.$el.querySelector('.svg-content img').getAttribute('src')).toBe(
vm.emptyStateIllustrationPath,
); );
}); });
});
it('renders mount point for new epic button to boot via Epic app', () => { describe('with presetType `WEEKS`', () => {
expect(vm.$el.querySelector('#epic-create-root')).not.toBeNull(); let timeframeEnd;
beforeEach(() => {
timeframeEnd = mockTimeframeWeeks[mockTimeframeWeeks.length - 1];
timeframeEnd.setDate(timeframeEnd.getDate() + 6);
}); });
it('does not render new epic button element when `hasFiltersApplied` prop is true', (done) => { it('renders default empty state sub-title when `hasFiltersApplied` props is false', () => {
vm.hasFiltersApplied = true; createWrapper({
Vue.nextTick() presetType: PRESET_TYPES.WEEKS,
.then(() => { timeframeStart: mockTimeframeWeeks[0],
expect(vm.$el.querySelector('.epic-create-dropdown')).toBeNull(); timeframeEnd,
}) });
.then(done)
.catch(done.fail); expect(findSubTitle().text()).toBe(
'To view the roadmap, add a start or due date to one of your epics in this group or its subgroups; from Dec 17, 2017 to Feb 9, 2018.',
);
}); });
it('renders view epics list link element', () => { it('renders empty state sub-title when `hasFiltersApplied` prop is true', () => {
const viewEpicsListEl = vm.$el.querySelector('a.btn'); createWrapper({
presetType: PRESET_TYPES.WEEKS,
timeframeStart: mockTimeframeWeeks[0],
timeframeEnd,
hasFiltersApplied: true,
});
expect(viewEpicsListEl).not.toBeNull(); expect(findSubTitle().text()).toBe(
expect(viewEpicsListEl.getAttribute('href')).toBe(mockNewEpicEndpoint); 'To widen your search, change or remove filters; from Dec 17, 2017 to Feb 15, 2018.',
expect(viewEpicsListEl.querySelector('span').innerText.trim()).toBe('View epics list'); );
}); });
}); });
it('renders empty state sub-title when `isChildEpics` is set to `true`', () => {
createWrapper({ isChildEpics: true });
expect(findSubTitle().text()).toBe(
'To view the roadmap, add a start or due date to one of the child epics.',
);
});
it('renders empty state illustration in image element with provided `emptyStateIllustrationPath`', () => {
createWrapper({});
expect(findIllustration().attributes('src')).toBe(mockSvgPath);
});
it('renders buttons for create and list epics', () => {
createWrapper({});
expect(findNewEpicButton().attributes('href')).toBe(TEST_NEW_EPIC_PATH);
expect(findListEpicsButton().attributes('href')).toBe(TEST_EPICS_PATH);
});
it('does not render new epic button element when `hasFiltersApplied` prop is true', () => {
createWrapper({ hasFiltersApplied: true });
expect(findNewEpicButton().exists()).toBe(false);
});
}); });
...@@ -17,7 +17,6 @@ import { ...@@ -17,7 +17,6 @@ import {
mockFormattedEpic, mockFormattedEpic,
mockFormattedChildEpic2, mockFormattedChildEpic2,
mockGroupId, mockGroupId,
mockNewEpicEndpoint,
mockSortedBy, mockSortedBy,
mockSvgPath, mockSvgPath,
mockTimeframeInitialDate, mockTimeframeInitialDate,
...@@ -35,7 +34,6 @@ describe('RoadmapApp', () => { ...@@ -35,7 +34,6 @@ describe('RoadmapApp', () => {
const emptyStateIllustrationPath = mockSvgPath; const emptyStateIllustrationPath = mockSvgPath;
const epics = [mockFormattedEpic]; const epics = [mockFormattedEpic];
const hasFiltersApplied = true; const hasFiltersApplied = true;
const newEpicEndpoint = mockNewEpicEndpoint;
const presetType = PRESET_TYPES.MONTHS; const presetType = PRESET_TYPES.MONTHS;
const timeframe = getTimeframeForMonthsView(mockTimeframeInitialDate); const timeframe = getTimeframeForMonthsView(mockTimeframeInitialDate);
...@@ -44,7 +42,6 @@ describe('RoadmapApp', () => { ...@@ -44,7 +42,6 @@ describe('RoadmapApp', () => {
localVue, localVue,
propsData: { propsData: {
emptyStateIllustrationPath, emptyStateIllustrationPath,
newEpicEndpoint,
presetType, presetType,
}, },
provide: { provide: {
...@@ -122,10 +119,6 @@ describe('RoadmapApp', () => { ...@@ -122,10 +119,6 @@ describe('RoadmapApp', () => {
expect(wrapper.find(EpicsListEmpty).props('isChildEpics')).toBe(false); expect(wrapper.find(EpicsListEmpty).props('isChildEpics')).toBe(false);
}); });
it('contains endpoint to create a new epic', () => {
expect(wrapper.find(EpicsListEmpty).props('newEpicEndpoint')).toBe(mockNewEpicEndpoint);
});
it('contains the preset type', () => { it('contains the preset type', () => {
expect(wrapper.find(EpicsListEmpty).props('presetType')).toBe(presetType); expect(wrapper.find(EpicsListEmpty).props('presetType')).toBe(presetType);
}); });
......
...@@ -17223,9 +17223,6 @@ msgstr "" ...@@ -17223,9 +17223,6 @@ msgstr ""
msgid "Make sure you save it - you won't be able to access it again." msgid "Make sure you save it - you won't be able to access it again."
msgstr "" msgstr ""
msgid "Make this epic confidential"
msgstr ""
msgid "Makes this issue confidential." msgid "Makes this issue confidential."
msgstr "" msgstr ""
...@@ -29039,9 +29036,6 @@ msgstr "" ...@@ -29039,9 +29036,6 @@ msgstr ""
msgid "This epic already has the maximum number of child epics." msgid "This epic already has the maximum number of child epics."
msgstr "" msgstr ""
msgid "This epic and its child elements will only be visible to team members with at minimum Reporter access."
msgstr ""
msgid "This epic does not exist or you don't have sufficient permission." msgid "This epic does not exist or you don't have sufficient permission."
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