Commit 2351da48 authored by Axel García's avatar Axel García

Replace "New epic" button popover with link

This also removes the code for the popover
component as it's not being used anymore
parent 1252d865
<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';
import createStore from './store';
import EpicApp from './components/epic_app.vue';
import EpicCreateApp from './components/epic_create.vue';
export default (epicCreate = false) => {
const el = document.getElementById(epicCreate ? 'epic-create-root' : 'epic-app-root');
export default () => {
const el = document.getElementById('epic-app-root');
if (!el) {
return false;
......@@ -21,28 +20,6 @@ export default (epicCreate = false) => {
const store = createStore();
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 epicData = parseIssuableData(el);
......
......@@ -7,13 +7,12 @@ import { dateInWords } from '~/lib/utils/datetime_utility';
import CommonMixin from '../mixins/common_mixin';
import { emptyStateDefault, emptyStateWithFilters } from '../constants';
import initEpicCreate from '../../epic/epic_bundle';
export default {
components: {
GlButton,
},
mixins: [CommonMixin],
inject: ['newEpicPath', 'listEpicsPath'],
props: {
presetType: {
type: String,
......@@ -31,10 +30,6 @@ export default {
type: Boolean,
required: true,
},
newEpicEndpoint: {
type: String,
required: true,
},
emptyStateIllustrationPath: {
type: String,
required: true,
......@@ -116,15 +111,6 @@ 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>
......@@ -135,17 +121,23 @@ export default {
</div>
<div class="col-12">
<div class="text-content">
<h4>{{ message }}</h4>
<h4 data-testid="default-message">{{ message }}</h4>
<p v-html="subMessage"></p>
<div class="text-center">
<div
v-if="!hasFiltersApplied"
id="epic-create-root"
:data-endpoint="newEpicEndpoint"
></div>
<gl-button :title="__('List')" :href="newEpicEndpoint">{{
s__('View epics list')
}}</gl-button>
<div class="gl-text-center">
<gl-button
:href="newEpicPath"
variant="success"
class="gl-mt-3 gl-sm-mt-0! gl-w-full gl-sm-w-auto!"
>
{{ __('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!"
>
{{ __('View epics list') }}
</gl-button>
</div>
</div>
</div>
......
......@@ -37,10 +37,6 @@ export default {
type: String,
required: true,
},
newEpicEndpoint: {
type: String,
required: true,
},
emptyStateIllustrationPath: {
type: String,
required: true,
......@@ -179,7 +175,6 @@ export default {
:timeframe-start="timeframeStart"
:timeframe-end="timeframeEnd"
:has-filters-applied="hasFiltersApplied"
:new-epic-endpoint="newEpicEndpoint"
:empty-state-illustration-path="emptyStateIllustrationPath"
:is-child-epics="isChildEpics"
/>
......
......@@ -50,6 +50,14 @@ export default () => {
components: {
roadmapApp,
},
provide() {
const { dataset } = this.$options.el;
return {
newEpicPath: dataset.newEpicPath,
listEpicsPath: dataset.listEpicsPath,
};
},
data() {
const supportedPresetTypes = Object.keys(PRESET_TYPES);
const { dataset } = this.$options.el;
......@@ -83,7 +91,6 @@ export default () => {
basePath: dataset.epicsPath,
fullPath: dataset.fullPath,
epicIid: dataset.iid,
newEpicEndpoint: dataset.newEpicEndpoint,
groupLabelsEndpoint: dataset.groupLabelsEndpoint,
groupMilestonesEndpoint: dataset.groupMilestonesEndpoint,
epicsState: dataset.epicsState,
......@@ -119,7 +126,6 @@ export default () => {
return createElement('roadmap-app', {
props: {
presetType: this.presetType,
newEpicEndpoint: this.newEpicEndpoint,
emptyStateIllustrationPath: this.emptyStateIllustrationPath,
},
});
......
......@@ -66,7 +66,8 @@
full_path: @group.full_path,
empty_state_illustration: image_path('illustrations/epics/roadmap.svg'),
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),
preset_type: roadmap_layout,
epics_state: 'all',
sorted_by: roadmap_sort_order,
......
......@@ -20,7 +20,8 @@
full_path: @group.full_path,
empty_state_illustration: image_path('illustrations/epics/roadmap.svg'),
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),
group_labels_endpoint: group_labels_path(@group, format: :json),
group_milestones_endpoint: group_milestones_path(@group, format: :json),
preset_type: roadmap_layout,
......
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 epicsListEmptyComponent from 'ee/roadmap/components/epics_list_empty.vue';
import { shallowMount } from '@vue/test-utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import EpicsListEmpty from 'ee/roadmap/components/epics_list_empty.vue';
import { mockTimeframeInitialDate, mockSvgPath } from 'ee_jest/roadmap/mock_data';
import { TEST_HOST } from 'helpers/test_constants';
import { PRESET_TYPES } from 'ee/roadmap/constants';
import {
getTimeframeForQuartersView,
getTimeframeForWeeksView,
getTimeframeForMonthsView,
} from 'ee/roadmap/utils/roadmap_utils';
import {
mockTimeframeInitialDate,
mockSvgPath,
mockNewEpicEndpoint,
} from 'ee_jest/roadmap/mock_data';
import mountComponent from 'helpers/vue_mount_component_helper';
import { getTimeframeForMonthsView } from 'ee/roadmap/utils/roadmap_utils';
const mockTimeframeQuarters = getTimeframeForQuartersView(mockTimeframeInitialDate);
const mockTimeframeMonths = getTimeframeForMonthsView(mockTimeframeInitialDate);
const mockTimeframeWeeks = getTimeframeForWeeksView(mockTimeframeInitialDate);
const createComponent = ({
hasFiltersApplied = false,
presetType = PRESET_TYPES.MONTHS,
timeframeStart = mockTimeframeMonths[0],
timeframeEnd = mockTimeframeMonths[mockTimeframeMonths.length - 1],
}) => {
const Component = Vue.extend(epicsListEmptyComponent);
return mountComponent(Component, {
presetType,
timeframeStart,
timeframeEnd,
emptyStateIllustrationPath: mockSvgPath,
newEpicEndpoint: mockNewEpicEndpoint,
hasFiltersApplied,
});
};
describe('EpicsListEmptyComponent', () => {
let vm;
describe('~/todos/app.vue', () => {
let wrapper;
const createWrapper = ({
hasFiltersApplied = false,
presetType = PRESET_TYPES.MONTHS,
timeframeStart = mockTimeframeMonths[0],
timeframeEnd = mockTimeframeMonths[mockTimeframeMonths.length - 1],
}) => {
wrapper = extendedWrapper(
shallowMount(EpicsListEmpty, {
propsData: {
presetType,
timeframeStart,
timeframeEnd,
emptyStateIllustrationPath: mockSvgPath,
hasFiltersApplied,
},
provide: {
newEpicPath: TEST_HOST,
listEpicsPath: TEST_HOST,
},
}),
);
};
beforeEach(() => {
vm = createComponent({});
createWrapper({});
});
afterEach(() => {
vm.$destroy();
});
describe('computed', () => {
describe('message', () => {
it('returns default empty state message', () => {
expect(vm.message).toBe('The roadmap shows the progress of your epics along a timeline');
});
it('returns empty state message when `hasFiltersApplied` prop is true', (done) => {
vm.hasFiltersApplied = true;
Vue.nextTick()
.then(() => {
expect(vm.message).toBe('Sorry, no epics matched your search');
})
.then(done)
.catch(done.fail);
});
});
describe('subMessage', () => {
describe('with presetType `QUARTERS`', () => {
beforeEach(() => {
vm.presetType = PRESET_TYPES.QUARTERS;
[vm.timeframeStart] = mockTimeframeQuarters;
vm.timeframeEnd = mockTimeframeQuarters[mockTimeframeQuarters.length - 1];
});
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`', () => {
beforeEach(() => {
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`', () => {
beforeEach(() => {
const timeframeEnd = mockTimeframeWeeks[mockTimeframeWeeks.length - 1];
timeframeEnd.setDate(timeframeEnd.getDate() + 6);
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', () => {
it('returns empty state sub-message when `isChildEpics` is set to `true`', (done) => {
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('returns correct timeframe startDate and endDate in words', () => {
expect(vm.timeframeRange.startDate).toBe('Nov 1, 2017');
expect(vm.timeframeRange.endDate).toBe('Jun 30, 2018');
});
});
wrapper.destroy();
wrapper = null;
});
describe('template', () => {
it('renders empty state illustration in image element with provided `emptyStateIllustrationPath`', () => {
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', () => {
expect(vm.$el.querySelector('#epic-create-root')).not.toBeNull();
});
it('does not render new epic button element when `hasFiltersApplied` prop is true', (done) => {
vm.hasFiltersApplied = true;
Vue.nextTick()
.then(() => {
expect(vm.$el.querySelector('.epic-create-dropdown')).toBeNull();
})
.then(done)
.catch(done.fail);
});
it('renders view epics list link element', () => {
const viewEpicsListEl = vm.$el.querySelector('a.btn');
const findDefaultMessage = () => wrapper.findByTestId('default-message');
expect(viewEpicsListEl).not.toBeNull();
expect(viewEpicsListEl.getAttribute('href')).toBe(mockNewEpicEndpoint);
expect(viewEpicsListEl.querySelector('span').innerText.trim()).toBe('View epics list');
});
it('renders default message', () => {
expect(findDefaultMessage().text()).toBe(wrapper.vm.message);
});
});
......@@ -17,7 +17,6 @@ import {
mockFormattedEpic,
mockFormattedChildEpic2,
mockGroupId,
mockNewEpicEndpoint,
mockSortedBy,
mockSvgPath,
mockTimeframeInitialDate,
......@@ -35,7 +34,6 @@ describe('RoadmapApp', () => {
const emptyStateIllustrationPath = mockSvgPath;
const epics = [mockFormattedEpic];
const hasFiltersApplied = true;
const newEpicEndpoint = mockNewEpicEndpoint;
const presetType = PRESET_TYPES.MONTHS;
const timeframe = getTimeframeForMonthsView(mockTimeframeInitialDate);
......@@ -44,7 +42,6 @@ describe('RoadmapApp', () => {
localVue,
propsData: {
emptyStateIllustrationPath,
newEpicEndpoint,
presetType,
},
provide: {
......@@ -122,10 +119,6 @@ describe('RoadmapApp', () => {
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', () => {
expect(wrapper.find(EpicsListEmpty).props('presetType')).toBe(presetType);
});
......
......@@ -17232,9 +17232,6 @@ msgstr ""
msgid "Make sure you save it - you won't be able to access it again."
msgstr ""
msgid "Make this epic confidential"
msgstr ""
msgid "Makes this issue confidential."
msgstr ""
......@@ -29033,9 +29030,6 @@ msgstr ""
msgid "This epic already has the maximum number of child epics."
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."
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