Commit c61122f9 authored by Natalia Tepluhina's avatar Natalia Tepluhina

Merge branch '293925-iteration-dropdown-group-by-cadence' into 'master'

Group by cadence in iteration dropdown widgets [RUN ALL RSPEC] [RUN AS-IF-FOSS]

See merge request gitlab-org/gitlab!63643
parents c14b941c 8f72bce7
...@@ -11,6 +11,7 @@ import SidebarAssigneesWidget from '~/sidebar/components/assignees/sidebar_assig ...@@ -11,6 +11,7 @@ import SidebarAssigneesWidget from '~/sidebar/components/assignees/sidebar_assig
import SidebarConfidentialityWidget from '~/sidebar/components/confidential/sidebar_confidentiality_widget.vue'; import SidebarConfidentialityWidget from '~/sidebar/components/confidential/sidebar_confidentiality_widget.vue';
import SidebarDateWidget from '~/sidebar/components/date/sidebar_date_widget.vue'; import SidebarDateWidget from '~/sidebar/components/date/sidebar_date_widget.vue';
import SidebarSubscriptionsWidget from '~/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue'; import SidebarSubscriptionsWidget from '~/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default { export default {
headerHeight: `${contentTop()}px`, headerHeight: `${contentTop()}px`,
...@@ -26,7 +27,10 @@ export default { ...@@ -26,7 +27,10 @@ export default {
SidebarDropdownWidget, SidebarDropdownWidget,
BoardSidebarWeightInput: () => BoardSidebarWeightInput: () =>
import('ee_component/boards/components/sidebar/board_sidebar_weight_input.vue'), import('ee_component/boards/components/sidebar/board_sidebar_weight_input.vue'),
IterationSidebarDropdownWidget: () =>
import('ee_component/sidebar/components/iteration_sidebar_dropdown_widget.vue'),
}, },
mixins: [glFeatureFlagMixin()],
inject: { inject: {
multipleAssigneesFeatureAvailable: { multipleAssigneesFeatureAvailable: {
default: false, default: false,
...@@ -103,17 +107,31 @@ export default { ...@@ -103,17 +107,31 @@ export default {
:issuable-type="issuableType" :issuable-type="issuableType"
data-testid="sidebar-milestones" data-testid="sidebar-milestones"
/> />
<sidebar-dropdown-widget <template v-if="!glFeatures.iterationCadences">
v-if="iterationFeatureAvailable" <sidebar-dropdown-widget
:iid="activeBoardItem.iid" v-if="iterationFeatureAvailable"
issuable-attribute="iteration" :iid="activeBoardItem.iid"
:workspace-path="projectPathForActiveIssue" issuable-attribute="iteration"
:attr-workspace-path="groupPathForActiveIssue" :workspace-path="projectPathForActiveIssue"
:issuable-type="issuableType" :attr-workspace-path="groupPathForActiveIssue"
class="gl-mt-5" :issuable-type="issuableType"
data-testid="iteration-edit" class="gl-mt-5"
data-qa-selector="iteration_container" data-testid="iteration-edit"
/> data-qa-selector="iteration_container"
/>
</template>
<template v-else>
<iteration-sidebar-dropdown-widget
v-if="iterationFeatureAvailable"
:iid="activeBoardItem.iid"
:workspace-path="projectPathForActiveIssue"
:attr-workspace-path="groupPathForActiveIssue"
:issuable-type="issuableType"
class="gl-mt-5"
data-testid="iteration-edit"
data-qa-selector="iteration_container"
/>
</template>
</div> </div>
<board-sidebar-time-tracker class="swimlanes-sidebar-time-tracker" /> <board-sidebar-time-tracker class="swimlanes-sidebar-time-tracker" />
<sidebar-date-widget <sidebar-date-widget
......
...@@ -293,9 +293,17 @@ export default { ...@@ -293,9 +293,17 @@ export default {
<span v-else-if="!currentAttribute" class="gl-text-gray-500"> <span v-else-if="!currentAttribute" class="gl-text-gray-500">
{{ $options.i18n.none }} {{ $options.i18n.none }}
</span> </span>
<gl-link v-else class="gl-text-gray-900! gl-font-weight-bold" :href="attributeUrl"> <slot
{{ attributeTitle }} v-else
</gl-link> name="value"
:attributeTitle="attributeTitle"
:attributeUrl="attributeUrl"
:currentAttribute="currentAttribute"
>
<gl-link class="gl-text-gray-900! gl-font-weight-bold" :href="attributeUrl">
{{ attributeTitle }}
</gl-link>
</slot>
</div> </div>
</template> </template>
<template #default> <template #default>
...@@ -327,16 +335,24 @@ export default { ...@@ -327,16 +335,24 @@ export default {
<gl-dropdown-text v-if="emptyPropsList"> <gl-dropdown-text v-if="emptyPropsList">
{{ i18n.noAttributesFound }} {{ i18n.noAttributesFound }}
</gl-dropdown-text> </gl-dropdown-text>
<gl-dropdown-item <slot
v-for="attrItem in attributesList" v-else
:key="attrItem.id" name="list"
:is-check-item="true" :attributesList="attributesList"
:is-checked="isAttributeChecked(attrItem.id)" :isAttributeChecked="isAttributeChecked"
:data-testid="`${issuableAttribute}-items`" :updateAttribute="updateAttribute"
@click="updateAttribute(attrItem.id)"
> >
{{ attrItem.title }} <gl-dropdown-item
</gl-dropdown-item> v-for="attrItem in attributesList"
:key="attrItem.id"
:is-check-item="true"
:is-checked="isAttributeChecked(attrItem.id)"
:data-testid="`${issuableAttribute}-items`"
@click="updateAttribute(attrItem.id)"
>
{{ attrItem.title }}
</gl-dropdown-item>
</slot>
</template> </template>
</gl-dropdown> </gl-dropdown>
</template> </template>
......
...@@ -10,6 +10,7 @@ class Groups::BoardsController < Groups::ApplicationController ...@@ -10,6 +10,7 @@ class Groups::BoardsController < Groups::ApplicationController
push_frontend_feature_flag(:graphql_board_lists, group, default_enabled: false) push_frontend_feature_flag(:graphql_board_lists, group, default_enabled: false)
push_frontend_feature_flag(:board_multi_select, group, default_enabled: :yaml) push_frontend_feature_flag(:board_multi_select, group, default_enabled: :yaml)
push_frontend_feature_flag(:swimlanes_buffered_rendering, group, default_enabled: :yaml) push_frontend_feature_flag(:swimlanes_buffered_rendering, group, default_enabled: :yaml)
push_frontend_feature_flag(:iteration_cadences, group, default_enabled: :yaml)
end end
feature_category :boards feature_category :boards
......
...@@ -33,6 +33,7 @@ class GroupsController < Groups::ApplicationController ...@@ -33,6 +33,7 @@ class GroupsController < Groups::ApplicationController
before_action do before_action do
push_frontend_feature_flag(:vue_issuables_list, @group) push_frontend_feature_flag(:vue_issuables_list, @group)
push_frontend_feature_flag(:iteration_cadences, @group, default_enabled: :yaml)
end end
before_action :export_rate_limit, only: [:export, :download_export] before_action :export_rate_limit, only: [:export, :download_export]
......
...@@ -10,6 +10,7 @@ class Projects::BoardsController < Projects::ApplicationController ...@@ -10,6 +10,7 @@ class Projects::BoardsController < Projects::ApplicationController
push_frontend_feature_flag(:swimlanes_buffered_rendering, project, default_enabled: :yaml) push_frontend_feature_flag(:swimlanes_buffered_rendering, project, default_enabled: :yaml)
push_frontend_feature_flag(:graphql_board_lists, project, default_enabled: :yaml) push_frontend_feature_flag(:graphql_board_lists, project, default_enabled: :yaml)
push_frontend_feature_flag(:board_multi_select, project, default_enabled: :yaml) push_frontend_feature_flag(:board_multi_select, project, default_enabled: :yaml)
push_frontend_feature_flag(:iteration_cadences, project&.group, default_enabled: :yaml)
end end
feature_category :boards feature_category :boards
......
...@@ -46,6 +46,7 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -46,6 +46,7 @@ class Projects::IssuesController < Projects::ApplicationController
push_frontend_feature_flag(:usage_data_design_action, project, default_enabled: true) push_frontend_feature_flag(:usage_data_design_action, project, default_enabled: true)
push_frontend_feature_flag(:improved_emoji_picker, project, default_enabled: :yaml) push_frontend_feature_flag(:improved_emoji_picker, project, default_enabled: :yaml)
push_frontend_feature_flag(:vue_issues_list, project) push_frontend_feature_flag(:vue_issues_list, project)
push_frontend_feature_flag(:iteration_cadences, project&.group, default_enabled: :yaml)
end end
before_action only: :show do before_action only: :show do
......
<script> <script>
import { import {
GlDropdown, GlDropdown,
GlDropdownDivider,
GlDropdownItem, GlDropdownItem,
GlSearchBoxByType, GlSearchBoxByType,
GlDropdownSectionHeader, GlDropdownSectionHeader,
...@@ -8,20 +9,24 @@ import { ...@@ -8,20 +9,24 @@ import {
GlLoadingIcon, GlLoadingIcon,
} from '@gitlab/ui'; } from '@gitlab/ui';
import { __ } from '~/locale'; import { __ } from '~/locale';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { iterationSelectTextMap, iterationDisplayState } from '../constants'; import { iterationSelectTextMap, iterationDisplayState } from '../constants';
import groupIterationsQuery from '../queries/iterations.query.graphql'; import groupIterationsQuery from '../queries/iterations.query.graphql';
export default { export default {
noIteration: { title: iterationSelectTextMap.noIteration, id: null },
directives: { directives: {
GlTooltip: GlTooltipDirective, GlTooltip: GlTooltipDirective,
}, },
components: { components: {
GlDropdown, GlDropdown,
GlDropdownDivider,
GlDropdownItem, GlDropdownItem,
GlSearchBoxByType, GlSearchBoxByType,
GlDropdownSectionHeader, GlDropdownSectionHeader,
GlLoadingIcon, GlLoadingIcon,
}, },
mixins: [glFeatureFlagMixin()],
apollo: { apollo: {
iterations: { iterations: {
query: groupIterationsQuery, query: groupIterationsQuery,
...@@ -36,14 +41,8 @@ export default { ...@@ -36,14 +41,8 @@ export default {
}; };
}, },
update(data) { update(data) {
// TODO: https://gitlab.com/gitlab-org/gitlab/-/issues/220379
return data.group?.iterations?.nodes || []; return data.group?.iterations?.nodes || [];
}, },
result({ data }) {
const nodes = data.group?.iterations?.nodes || [];
this.iterations = iterationSelectTextMap.noIterationItem.concat(nodes);
},
skip() { skip() {
return !this.shouldFetch; return !this.shouldFetch;
}, },
...@@ -64,6 +63,26 @@ export default { ...@@ -64,6 +63,26 @@ export default {
}; };
}, },
computed: { computed: {
cadenceTitle() {
return this.currentIteration?.iterationCadence?.title;
},
iterationCadences() {
const cadences = [];
this.iterations.forEach((iteration) => {
if (!iteration.iterationCadence) {
return;
}
const { title } = iteration.iterationCadence;
const cadenceIteration = { id: iteration.id, title: iteration.title };
const cadence = cadences.find((cad) => cad.title === title);
if (cadence) {
cadence.iterations.push(cadenceIteration);
} else {
cadences.push({ title, iterations: [cadenceIteration] });
}
});
return cadences;
},
title() { title() {
return this.currentIteration?.title || __('Select iteration'); return this.currentIteration?.title || __('Select iteration');
}, },
...@@ -95,16 +114,41 @@ export default { ...@@ -95,16 +114,41 @@ export default {
__('Assign Iteration') __('Assign Iteration')
}}</gl-dropdown-section-header> }}</gl-dropdown-section-header>
<gl-search-box-by-type v-model="searchTerm" /> <gl-search-box-by-type v-model="searchTerm" />
<gl-loading-icon v-if="$apollo.loading" />
<gl-dropdown-item <gl-dropdown-item
v-for="iterationItem in iterations"
v-else
:key="iterationItem.id"
:is-check-item="true" :is-check-item="true"
:is-checked="isIterationChecked(iterationItem.id)" :is-checked="isIterationChecked($options.noIteration.id)"
@click="onClick(iterationItem)" @click="onClick($options.noIteration)"
>{{ iterationItem.title }}</gl-dropdown-item
> >
{{ $options.noIteration.title }}
</gl-dropdown-item>
<gl-dropdown-divider />
<gl-loading-icon v-if="$apollo.queries.iterations.loading" />
<template v-else-if="!glFeatures.iterationCadences">
<gl-dropdown-item
v-for="iterationItem in iterations"
:key="iterationItem.id"
:is-check-item="true"
:is-checked="isIterationChecked(iterationItem.id)"
@click="onClick(iterationItem)"
>{{ iterationItem.title }}</gl-dropdown-item
>
</template>
<template v-else>
<template v-for="(cadence, index) in iterationCadences">
<gl-dropdown-divider v-if="index !== 0" :key="index" />
<gl-dropdown-section-header :key="cadence.title">
{{ cadence.title }}
</gl-dropdown-section-header>
<gl-dropdown-item
v-for="iterationItem in cadence.iterations"
:key="iterationItem.id"
:is-check-item="true"
:is-checked="isIterationChecked(iterationItem.id)"
@click="onClick(iterationItem)"
>{{ iterationItem.title }}</gl-dropdown-item
>
</template>
</template>
</gl-dropdown> </gl-dropdown>
</div> </div>
</template> </template>
<script>
import {
GlDropdownDivider,
GlDropdownItem,
GlDropdownSectionHeader,
GlIcon,
GlLink,
} from '@gitlab/ui';
import SidebarDropdownWidget from 'ee/sidebar/components/sidebar_dropdown_widget.vue';
import { IssuableType } from '~/issue_show/constants';
import { IssuableAttributeType } from '../constants';
export default {
issuableAttribute: IssuableAttributeType.Iteration,
components: {
GlDropdownDivider,
GlDropdownItem,
GlDropdownSectionHeader,
GlIcon,
GlLink,
SidebarDropdownWidget,
},
props: {
attrWorkspacePath: {
required: true,
type: String,
},
iid: {
required: true,
type: String,
},
issuableType: {
type: String,
required: true,
validator(value) {
return value === IssuableType.Issue;
},
},
workspacePath: {
required: true,
type: String,
},
},
methods: {
getCadenceTitle(currentIteration) {
return currentIteration?.iterationCadence?.title;
},
getIterationCadences(iterations) {
const cadences = [];
iterations.forEach((iteration) => {
if (!iteration.iterationCadence) {
return;
}
const { title } = iteration.iterationCadence;
const cadenceIteration = { id: iteration.id, title: iteration.title };
const cadence = cadences.find((cad) => cad.title === title);
if (cadence) {
cadence.iterations.push(cadenceIteration);
} else {
cadences.push({ title, iterations: [cadenceIteration] });
}
});
return cadences;
},
},
};
</script>
<template>
<sidebar-dropdown-widget
:attr-workspace-path="attrWorkspacePath"
:iid="iid"
:issuable-attribute="$options.issuableAttribute"
:issuable-type="issuableType"
:workspace-path="workspacePath"
>
<template #value="{ attributeTitle, attributeUrl, currentAttribute }">
<p class="gl-font-weight-bold gl-line-height-20 gl-m-0">
{{ getCadenceTitle(currentAttribute) }}
</p>
<gl-link class="gl-text-gray-900! gl-line-height-20" :href="attributeUrl">
<gl-icon name="iteration" class="gl-mr-1" />
{{ attributeTitle }}
</gl-link>
</template>
<template #list="{ attributesList = [], isAttributeChecked, updateAttribute }">
<template v-for="(cadence, index) in getIterationCadences(attributesList)">
<gl-dropdown-divider v-if="index !== 0" :key="index" />
<gl-dropdown-section-header :key="cadence.title">
{{ cadence.title }}
</gl-dropdown-section-header>
<gl-dropdown-item
v-for="iteration in cadence.iterations"
:key="iteration.id"
:is-check-item="true"
:is-checked="isAttributeChecked(iteration.id)"
:data-testid="`${$options.issuableAttribute}-items`"
@click="updateAttribute(iteration.id)"
>
{{ iteration.title }}
</gl-dropdown-item>
</template>
</template>
</sidebar-dropdown-widget>
</template>
...@@ -31,7 +31,6 @@ export const healthStatusTextMap = { ...@@ -31,7 +31,6 @@ export const healthStatusTextMap = {
export const iterationSelectTextMap = { export const iterationSelectTextMap = {
iteration: __('Iteration'), iteration: __('Iteration'),
noIteration: __('No iteration'), noIteration: __('No iteration'),
noIterationItem: [{ title: __('No iteration'), id: null }],
assignIteration: __('Assign Iteration'), assignIteration: __('Assign Iteration'),
iterationSelectFail: __('Failed to set iteration on this issue. Please try again.'), iterationSelectFail: __('Failed to set iteration on this issue. Please try again.'),
currentIterationFetchError: __('Failed to fetch the iteration for this issue. Please try again.'), currentIterationFetchError: __('Failed to fetch the iteration for this issue. Please try again.'),
......
...@@ -6,6 +6,7 @@ import { store } from '~/notes/stores'; ...@@ -6,6 +6,7 @@ import { store } from '~/notes/stores';
import { apolloProvider } from '~/sidebar/graphql'; import { apolloProvider } from '~/sidebar/graphql';
import * as CEMountSidebar from '~/sidebar/mount_sidebar'; import * as CEMountSidebar from '~/sidebar/mount_sidebar';
import CveIdRequest from './components/cve_id_request/cve_id_request_sidebar.vue'; import CveIdRequest from './components/cve_id_request/cve_id_request_sidebar.vue';
import IterationSidebarDropdownWidget from './components/iteration_sidebar_dropdown_widget.vue';
import SidebarDropdownWidget from './components/sidebar_dropdown_widget.vue'; import SidebarDropdownWidget from './components/sidebar_dropdown_widget.vue';
import SidebarStatus from './components/status/sidebar_status.vue'; import SidebarStatus from './components/status/sidebar_status.vue';
import SidebarWeight from './components/weight/sidebar_weight.vue'; import SidebarWeight from './components/weight/sidebar_weight.vue';
...@@ -113,18 +114,19 @@ function mountIterationSelect() { ...@@ -113,18 +114,19 @@ function mountIterationSelect() {
const { groupPath, canEdit, projectPath, issueIid } = el.dataset; const { groupPath, canEdit, projectPath, issueIid } = el.dataset;
const IterationDropdown = gon.features.iterationCadences
? IterationSidebarDropdownWidget
: SidebarDropdownWidget;
return new Vue({ return new Vue({
el, el,
apolloProvider, apolloProvider,
components: {
SidebarDropdownWidget,
},
provide: { provide: {
canUpdate: parseBoolean(canEdit), canUpdate: parseBoolean(canEdit),
isClassicSidebar: true, isClassicSidebar: true,
}, },
render: (createElement) => render: (createElement) =>
createElement('sidebar-dropdown-widget', { createElement(IterationDropdown, {
props: { props: {
attrWorkspacePath: groupPath, attrWorkspacePath: groupPath,
workspacePath: projectPath, workspacePath: projectPath,
......
...@@ -2,4 +2,8 @@ fragment IterationFragment on Iteration { ...@@ -2,4 +2,8 @@ fragment IterationFragment on Iteration {
id id
title title
webUrl webUrl
iterationCadence {
id
title
}
} }
...@@ -135,39 +135,93 @@ RSpec.describe 'Issue Sidebar' do ...@@ -135,39 +135,93 @@ RSpec.describe 'Issue Sidebar' do
context 'Iterations', :js do context 'Iterations', :js do
context 'when iterations feature available' do context 'when iterations feature available' do
let_it_be(:iteration) { create(:iteration, group: group, start_date: 1.day.from_now, due_date: 2.days.from_now, title: 'Iteration 1') } let_it_be(:iteration_cadence) { create(:iterations_cadence, group: group, active: true) }
let_it_be(:iteration2) { create(:iteration, group: group, start_date: 2.days.ago, due_date: 1.day.ago, title: 'Iteration 2', state: 'closed', skip_future_date_validation: true) } let_it_be(:iteration) { create(:iteration, iterations_cadence: iteration_cadence, group: group, start_date: 1.day.from_now, due_date: 2.days.from_now) }
let_it_be(:iteration2) { create(:iteration, iterations_cadence: iteration_cadence, group: group, start_date: 2.days.ago, due_date: 1.day.ago, state: 'closed', skip_future_date_validation: true) }
before do before do
iteration
stub_licensed_features(iterations: true) stub_licensed_features(iterations: true)
project.add_developer(user) project.add_developer(user)
end
visit_issue(project, issue) context 'when `iteration_cadences` feature flag is off' do
before do
stub_feature_flags(iteration_cadences: false)
wait_for_all_requests visit_issue(project, issue)
end
it 'selects and updates the right iteration' do wait_for_all_requests
find_and_click_edit_iteration end
it 'selects and updates the right iteration', :aggregate_failures do
find_and_click_edit_iteration
select_iteration(iteration.title) within '[data-testid="iteration-edit"]' do
expect(page).not_to have_text(iteration_cadence.title)
expect(page).to have_text(iteration.title)
end
expect(page.find('[data-testid="select-iteration"]')).to have_content('Iteration 1') select_iteration(iteration.title)
find_and_click_edit_iteration within '[data-testid="select-iteration"]' do
expect(page).not_to have_text(iteration_cadence.title)
expect(page).to have_text(iteration.title)
end
select_iteration('No iteration') find_and_click_edit_iteration
expect(page.find('[data-testid="select-iteration"]')).to have_content('None') select_iteration('No iteration')
expect(page.find('[data-testid="select-iteration"]')).to have_content('None')
end
it 'does not show closed iterations' do
find_and_click_edit_iteration
page.within '[data-testid="iteration-edit"]' do
expect(page).not_to have_content iteration2.title
end
end
end end
it 'does not show closed iterations' do context 'when `iteration_cadences` feature flag is on' do
find_and_click_edit_iteration before do
stub_feature_flags(iteration_cadences: true)
visit_issue(project, issue)
page.within '.milestone' do wait_for_all_requests
expect(page).not_to have_content iteration2.title end
it 'selects and updates the right iteration', :aggregate_failures do
find_and_click_edit_iteration
within '[data-testid="iteration-edit"]' do
expect(page).to have_text(iteration_cadence.title)
expect(page).to have_text(iteration.title)
end
select_iteration(iteration.title)
within '[data-testid="select-iteration"]' do
expect(page).to have_text(iteration_cadence.title)
expect(page).to have_text(iteration.title)
end
find_and_click_edit_iteration
select_iteration('No iteration')
expect(page.find('[data-testid="select-iteration"]')).to have_content('None')
end
it 'does not show closed iterations' do
find_and_click_edit_iteration
page.within '[data-testid="iteration-edit"]' do
expect(page).not_to have_content iteration2.title
end
end end
end end
end end
......
...@@ -21,5 +21,21 @@ exports[`IterationDropdown default shows gl-dropdown 1`] = ` ...@@ -21,5 +21,21 @@ exports[`IterationDropdown default shows gl-dropdown 1`] = `
value="" value=""
/> />
<gl-dropdown-item-stub
avatarurl=""
iconcolor=""
iconname=""
iconrightarialabel=""
iconrightname=""
ischeckitem="true"
secondarytext=""
>
No iteration
</gl-dropdown-item-stub>
<gl-dropdown-divider-stub />
</gl-dropdown-stub> </gl-dropdown-stub>
`; `;
import { GlDropdownItem, GlLoadingIcon, GlDropdown, GlSearchBoxByType } from '@gitlab/ui'; import {
import { shallowMount, createLocalVue } from '@vue/test-utils'; GlDropdownDivider,
GlDropdownItem,
GlLoadingIcon,
GlDropdown,
GlDropdownSectionHeader,
GlSearchBoxByType,
} from '@gitlab/ui';
import { mount, shallowMount, createLocalVue } from '@vue/test-utils';
import VueApollo from 'vue-apollo'; import VueApollo from 'vue-apollo';
import IterationDropdown from 'ee/sidebar/components/iteration_dropdown.vue'; import IterationDropdown from 'ee/sidebar/components/iteration_dropdown.vue';
import { iterationSelectTextMap } from 'ee/sidebar/constants';
import groupIterationsQuery from 'ee/sidebar/queries/iterations.query.graphql'; import groupIterationsQuery from 'ee/sidebar/queries/iterations.query.graphql';
import createMockApollo from 'helpers/mock_apollo_helper'; import createMockApollo from 'helpers/mock_apollo_helper';
...@@ -14,16 +20,34 @@ const TEST_SEARCH = 'search'; ...@@ -14,16 +20,34 @@ const TEST_SEARCH = 'search';
const TEST_FULL_PATH = 'gitlab-test/test'; const TEST_FULL_PATH = 'gitlab-test/test';
const TEST_ITERATIONS = [ const TEST_ITERATIONS = [
{ {
id: '1', id: '11',
title: 'Test Title', title: 'Test Title',
webUrl: '', webUrl: '',
state: '', state: '',
iterationCadence: {
id: '111',
title: 'My Cadence',
},
}, },
{ {
id: '2', id: '22',
title: 'Another Test Title', title: 'Another Test Title',
webUrl: '', webUrl: '',
state: '', state: '',
iterationCadence: {
id: '222',
title: 'My Second Cadence',
},
},
{
id: '33',
title: 'Yet Another Test Title',
webUrl: '',
state: '',
iterationCadence: {
id: '333',
title: 'My Cadence',
},
}, },
]; ];
...@@ -53,7 +77,7 @@ describe('IterationDropdown', () => { ...@@ -53,7 +77,7 @@ describe('IterationDropdown', () => {
await wrapper.vm.$nextTick(); await wrapper.vm.$nextTick();
jest.runOnlyPendingTimers(); jest.runOnlyPendingTimers();
}; };
const findDropdownItems = () => wrapper.findAll(GlDropdownItem); const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
const findDropdownItemWithText = (text) => const findDropdownItemWithText = (text) =>
findDropdownItems().wrappers.find((x) => x.text() === text); findDropdownItems().wrappers.find((x) => x.text() === text);
const findDropdownItemsData = () => const findDropdownItemsData = () =>
...@@ -69,15 +93,15 @@ describe('IterationDropdown', () => { ...@@ -69,15 +93,15 @@ describe('IterationDropdown', () => {
await wrapper.vm.$nextTick(); await wrapper.vm.$nextTick();
}; };
const findDropdown = () => wrapper.find(GlDropdown); const findDropdown = () => wrapper.findComponent(GlDropdown);
const showDropdownAndWait = async () => { const showDropdownAndWait = async () => {
findDropdown().vm.$emit('show'); findDropdown().vm.$emit('show');
await waitForDebounce(); await waitForDebounce();
}; };
const isLoading = () => wrapper.find(GlLoadingIcon).exists(); const isLoading = () => wrapper.findComponent(GlLoadingIcon).exists();
const createComponent = ({ mountFn = shallowMount } = {}) => { const createComponent = ({ mountFn = shallowMount, iterationCadences = false } = {}) => {
fakeApollo = createMockApollo([[groupIterationsQuery, groupIterationsSpy]]); fakeApollo = createMockApollo([[groupIterationsQuery, groupIterationsSpy]]);
wrapper = mountFn(IterationDropdown, { wrapper = mountFn(IterationDropdown, {
...@@ -86,6 +110,11 @@ describe('IterationDropdown', () => { ...@@ -86,6 +110,11 @@ describe('IterationDropdown', () => {
propsData: { propsData: {
fullPath: TEST_FULL_PATH, fullPath: TEST_FULL_PATH,
}, },
provide: {
glFeatures: {
iterationCadences,
},
},
}); });
}; };
...@@ -99,8 +128,8 @@ describe('IterationDropdown', () => { ...@@ -99,8 +128,8 @@ describe('IterationDropdown', () => {
}); });
it('shows gl-dropdown', () => { it('shows gl-dropdown', () => {
expect(wrapper.find(GlDropdown).exists()).toBe(true); expect(findDropdown().exists()).toBe(true);
expect(wrapper.find(GlDropdown).element).toMatchSnapshot(); expect(findDropdown().element).toMatchSnapshot();
}); });
}); });
...@@ -140,7 +169,7 @@ describe('IterationDropdown', () => { ...@@ -140,7 +169,7 @@ describe('IterationDropdown', () => {
}); });
it('shows dropdown items', () => { it('shows dropdown items', () => {
const result = iterationSelectTextMap.noIterationItem.concat(TEST_ITERATIONS); const result = [IterationDropdown.noIteration].concat(TEST_ITERATIONS);
expect(findDropdownItemsData()).toEqual( expect(findDropdownItemsData()).toEqual(
result.map((x) => ({ result.map((x) => ({
...@@ -159,7 +188,7 @@ describe('IterationDropdown', () => { ...@@ -159,7 +188,7 @@ describe('IterationDropdown', () => {
}); });
describe.each([0, 1, 2])('when item %s is selected', (index) => { describe.each([0, 1, 2])('when item %s is selected', (index) => {
const allIterations = iterationSelectTextMap.noIterationItem.concat(TEST_ITERATIONS); const allIterations = [IterationDropdown.noIteration].concat(TEST_ITERATIONS);
const selected = allIterations[index]; const selected = allIterations[index];
const asNotChecked = ({ title }) => ({ isCheckItem: true, isChecked: false, text: title }); const asNotChecked = ({ title }) => ({ isCheckItem: true, isChecked: false, text: title });
...@@ -211,7 +240,7 @@ describe('IterationDropdown', () => { ...@@ -211,7 +240,7 @@ describe('IterationDropdown', () => {
groupIterationsSpy.mockClear(); groupIterationsSpy.mockClear();
wrapper.find(GlSearchBoxByType).vm.$emit('input', TEST_SEARCH); wrapper.findComponent(GlSearchBoxByType).vm.$emit('input', TEST_SEARCH);
await waitForDebounce(); await waitForDebounce();
}); });
...@@ -224,4 +253,28 @@ describe('IterationDropdown', () => { ...@@ -224,4 +253,28 @@ describe('IterationDropdown', () => {
}); });
}); });
}); });
describe('when iteration_cadences feature flag is on', () => {
beforeEach(async () => {
createComponent({ iterationCadences: true, mountFn: mount });
await showDropdownAndWait();
});
it('shows dropdown items grouped by iteration cadence', () => {
const dropdownItems = wrapper.findAll('li');
expect(dropdownItems.at(0).text()).toBe('Assign Iteration');
expect(dropdownItems.at(1).text()).toBe('No iteration');
expect(dropdownItems.at(2).findComponent(GlDropdownDivider).exists()).toBe(true);
expect(dropdownItems.at(3).findComponent(GlDropdownSectionHeader).text()).toBe('My Cadence');
expect(dropdownItems.at(4).text()).toBe('Test Title');
expect(dropdownItems.at(5).text()).toBe('Yet Another Test Title');
expect(dropdownItems.at(6).findComponent(GlDropdownDivider).exists()).toBe(true);
expect(dropdownItems.at(7).findComponent(GlDropdownSectionHeader).text()).toBe(
'My Second Cadence',
);
expect(dropdownItems.at(8).text()).toBe('Another Test Title');
});
});
}); });
import { shallowMount } from '@vue/test-utils';
import IterationSidebarDropdownWidget from 'ee/sidebar/components/iteration_sidebar_dropdown_widget.vue';
import SidebarDropdownWidget from 'ee/sidebar/components/sidebar_dropdown_widget.vue';
import { IssuableType } from '~/issue_show/constants';
describe('IterationSidebarDropdownWidget', () => {
let wrapper;
const defaultProps = {
attrWorkspacePath: 'attr/workspace/path',
iid: 'iid',
issuableType: IssuableType.Issue,
workspacePath: 'workspace/path',
};
const createComponent = () =>
shallowMount(IterationSidebarDropdownWidget, {
propsData: defaultProps,
});
afterEach(() => {
wrapper.destroy();
});
describe('SidebarDropdownWidget component', () => {
it('renders', () => {
wrapper = createComponent();
expect(wrapper.findComponent(SidebarDropdownWidget).props()).toEqual({
...defaultProps,
issuableAttribute: 'iteration',
});
});
});
});
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