Commit ed7049fb authored by Florie Guibert's avatar Florie Guibert Committed by Simon Knox

Add iteration selector to board scope

Changelog: changed
EE: true
parent b17b71f0
......@@ -3,6 +3,7 @@ import { GlModal, GlAlert } from '@gitlab/ui';
import { mapGetters, mapActions, mapState } from 'vuex';
import { getParameterByName, visitUrl } from '~/lib/utils/url_utility';
import { __, s__ } from '~/locale';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { formType } from '../constants';
import createBoardMutation from '../graphql/board_create.mutation.graphql';
......@@ -15,6 +16,7 @@ const boardDefaults = {
name: '',
labels: [],
milestone: {},
iterationCadence: {},
iteration: {},
assignee: {},
weight: null,
......@@ -41,6 +43,7 @@ export default {
BoardConfigurationOptions,
GlAlert,
},
mixins: [glFeatureFlagMixin()],
inject: {
fullPath: {
default: '',
......@@ -231,9 +234,12 @@ export default {
this.board = { ...boardDefaults, ...this.currentBoard };
}
},
setIteration(iterationId) {
setIteration(iteration) {
if (this.glFeatures.iterationCadences) {
this.board.iterationCadenceId = iteration.iterationCadenceId;
}
this.$set(this.board, 'iteration', {
id: iterationId,
id: iteration.id,
});
},
setBoardLabels(labels) {
......
<script>
import {
GlIcon,
GlLoadingIcon,
GlDropdown,
GlDropdownForm,
GlDropdownDivider,
GlDropdownItem,
GlDropdownSectionHeader,
GlSearchBoxByType,
} from '@gitlab/ui';
import { __ } from '~/locale';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue';
export default {
components: {
GlIcon,
GlLoadingIcon,
GlDropdown,
GlDropdownForm,
GlDropdownDivider,
GlDropdownItem,
GlDropdownSectionHeader,
GlSearchBoxByType,
TooltipOnTruncate,
},
props: {
selectText: {
......@@ -39,6 +45,11 @@ export default {
required: false,
default: () => [],
},
groupedOptions: {
type: Array,
required: false,
default: () => [],
},
isLoading: {
type: Boolean,
required: false,
......@@ -79,11 +90,7 @@ export default {
if (Array.isArray(this.selected)) {
return this.selected.some((label) => label.title === option.title);
}
return (
this.selected &&
((option.name && this.selected.name === option.name) ||
(option.title && this.selected.title === option.title))
);
return this.selected && option.id && this.selected.id === option.id;
},
showDropdown() {
this.$refs.dropdown.show();
......@@ -101,6 +108,9 @@ export default {
// TODO: this has some knowledge of the context where the component is used. We could later rework it.
return option.username || null;
},
optionKey(option) {
return option.key ? option.key : option.id;
},
},
i18n: {
noMatchingResults: __('No matching results'),
......@@ -154,10 +164,10 @@ export default {
</template>
<gl-dropdown-item
v-for="option in options"
:key="option.id"
:key="optionKey(option)"
:is-checked="isSelected(option)"
:is-check-centered="true"
:is-check-item="true"
is-check-centered
is-check-item
:avatar-url="avatarUrl(option)"
:secondary-text="secondaryText(option)"
data-testid="unselected-option"
......@@ -167,6 +177,36 @@ export default {
{{ option.title }}
</slot>
</gl-dropdown-item>
<template v-for="(optionGroup, index) in groupedOptions">
<gl-dropdown-divider v-if="index !== 0" :key="index" />
<gl-dropdown-section-header :key="optionGroup.id">
<div class="gl-display-flex gl-max-w-full">
<tooltip-on-truncate
:title="optionGroup.title"
class="gl-text-truncate gl-flex-grow-1"
>
{{ optionGroup.title }}
</tooltip-on-truncate>
<span v-if="optionGroup.secondaryText" class="gl-float-right gl-font-weight-normal">
<gl-icon name="clock" class="gl-mr-2" />
{{ optionGroup.secondaryText }}
</span>
</div>
</gl-dropdown-section-header>
<gl-dropdown-item
v-for="option in optionGroup.options"
:key="optionKey(option)"
:is-checked="isSelected(option)"
is-check-centered
is-check-item
data-testid="unselected-option"
@click="selectOption(option)"
>
<slot name="item" :item="option">
{{ option.title }}
</slot>
</gl-dropdown-item>
</template>
<gl-dropdown-item v-if="noOptionsFound" class="gl-pl-6!">
{{ $options.i18n.noMatchingResults }}
</gl-dropdown-item>
......
......@@ -189,13 +189,18 @@ export function transformBoardConfig(boardConfig) {
updateScopeObject('milestone_title', milestoneTitle);
}
let { iterationTitle } = boardConfig;
if (boardConfig.iterationId === IterationIDs.NONE) {
iterationTitle = IterationFilterType.none;
const { iterationId } = boardConfig;
if (iterationId === IterationIDs.NONE) {
updateScopeObject('iteration_id', IterationFilterType.none);
} else if (iterationId === IterationIDs.CURRENT) {
updateScopeObject('iteration_id', IterationFilterType.current);
} else if (iterationId) {
updateScopeObject('iteration_id', getIdFromGraphQLId(iterationId));
}
if (iterationTitle) {
updateScopeObject('iteration_id', iterationTitle);
const { iterationCadenceId } = boardConfig;
if (iterationCadenceId) {
updateScopeObject('iteration_cadence_id', getIdFromGraphQLId(iterationCadenceId));
}
let { weight } = boardConfig;
......@@ -259,6 +264,9 @@ export const FiltersInfo = {
return valList[valList.length - 1].toUpperCase();
},
},
iterationCadenceId: {
negatedSupport: false,
},
weight: {
negatedSupport: true,
},
......
......@@ -27,6 +27,7 @@ export default {
: null,
milestoneId: this.board.milestone?.id || null,
iterationId: this.board.iteration?.id || null,
iterationCadenceId: this.board.iterationCadenceId || null,
};
},
boardScopeMutationVariables() {
......
<script>
import { mapGetters } from 'vuex';
import { __ } from '~/locale';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import AssigneeSelect from './assignee_select.vue';
import BoardScopeCurrentIteration from './board_scope_current_iteration.vue';
import BoardLabelsSelect from './labels_select.vue';
import BoardIterationSelect from './iteration_select.vue';
import BoardMilestoneSelect from './milestone_select.vue';
import BoardWeightSelect from './weight_select.vue';
......@@ -11,10 +13,12 @@ export default {
components: {
AssigneeSelect,
BoardLabelsSelect,
BoardIterationSelect,
BoardMilestoneSelect,
BoardScopeCurrentIteration,
BoardWeightSelect,
},
mixins: [glFeatureFlagMixin()],
props: {
collapseScope: {
type: Boolean,
......@@ -87,8 +91,15 @@ export default {
@set-milestone="$emit('set-milestone', $event)"
/>
<board-iteration-select
v-if="isIssueBoard && glFeatures.iterationCadences"
:board="board"
:can-edit="canAdminBoard"
@set-iteration="$emit('set-iteration', $event)"
/>
<board-scope-current-iteration
v-if="isIssueBoard"
v-if="isIssueBoard && !glFeatures.iterationCadences"
:can-admin-board="canAdminBoard"
:iteration-id="iterationId"
@set-iteration="$emit('set-iteration', $event)"
......
<script>
import { GlFormCheckbox } from '@gitlab/ui';
import { __ } from '~/locale';
import { IterationIDs } from '../constants';
import { IterationIDs, CURRENT_ITERATION } from '../constants';
export default {
i18n: {
......@@ -30,8 +30,8 @@ export default {
methods: {
handleToggle() {
this.checked = !this.checked;
const iterationId = this.checked ? IterationIDs.CURRENT : null;
this.$emit('set-iteration', iterationId);
const iteration = this.checked ? CURRENT_ITERATION : { id: null };
this.$emit('set-iteration', iteration);
},
},
};
......
<script>
import { GlButton } from '@gitlab/ui';
import { isEmpty } from 'lodash';
import { mapActions, mapGetters } from 'vuex';
import searchIterationQuery from 'ee/issues/list/queries/search_iterations.query.graphql';
import { getIterationPeriod } from 'ee/iterations/utils';
import { n__, s__, __ } from '~/locale';
import { TYPE_ITERATION } from '~/graphql_shared/constants';
import { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils';
import DropdownWidget from '~/vue_shared/components/dropdown/dropdown_widget/dropdown_widget.vue';
import { IterationsPreset, ANY_ITERATION } from '../constants';
export default {
IterationsPreset,
components: {
GlButton,
DropdownWidget,
},
inject: ['fullPath'],
props: {
board: {
type: Object,
required: true,
},
canEdit: {
type: Boolean,
required: false,
default: false,
},
},
data() {
return {
search: '',
iterations: [],
selected: this.board.iteration
? {
...this.board.iteration,
id: convertToGraphQLId(TYPE_ITERATION, getIdFromGraphQLId(this.board.iteration?.id)),
}
: null,
isEditing: false,
isDropdownShowing: false,
};
},
apollo: {
iterations: {
query: searchIterationQuery,
variables() {
return {
fullPath: this.fullPath,
search: this.search,
isProject: this.isProjectBoard,
};
},
skip() {
return !this.isEditing;
},
update(data) {
const boardType = this.isProjectBoard ? 'project' : 'group';
return data[boardType]?.iterations?.nodes || [];
},
error() {
this.setError({ message: this.$options.i18n.errorSearchingIterations });
},
},
},
computed: {
...mapGetters(['isProjectBoard']),
anyIteration() {
return this.selected.id === ANY_ITERATION.id;
},
iterationTitle() {
return this.anyIteration ? ANY_ITERATION.title : this.selected.title;
},
iterationTitleClass() {
return this.anyIteration ? 'gl-text-secondary' : 'gl-font-weight-bold';
},
isLoading() {
return this.$apollo.queries.iterations.loading;
},
iterationsByCadence() {
const cadences = [];
this.iterations.forEach((iteration) => {
if (!iteration.iterationCadence) {
return;
}
const { title, durationInWeeks, id } = iteration.iterationCadence;
const cadenceIteration = {
key: `${iteration.iterationCadence.id}-${iteration.id}`,
id: iteration.id,
title: this.iterationOptionText(iteration),
iterationCadenceId: id,
};
const cadence = cadences.find((cad) => cad.title === title);
if (cadence) {
cadence.options.push(cadenceIteration);
} else {
const durationText = durationInWeeks
? n__('Every week', 'Every %d weeks', durationInWeeks)
: null;
cadences.push({
id,
title,
secondaryText: durationText,
options: [cadenceIteration],
});
}
});
return cadences;
},
},
created() {
if (isEmpty(this.board.iteration)) {
this.selected = ANY_ITERATION;
}
},
methods: {
...mapActions(['setError']),
selectIteration(iteration) {
this.selected = iteration;
this.toggleEdit();
this.$emit(
'set-iteration',
iteration?.id !== ANY_ITERATION.id ? iteration : { id: null, iterationCadenceId: null },
);
},
toggleEdit() {
if (!this.isEditing && !this.isDropdownShowing) {
this.isEditing = true;
this.showDropdown();
} else {
this.hideDropdown();
}
},
showDropdown() {
this.$refs.editDropdown.showDropdown();
this.isDropdownShowing = true;
},
hideDropdown() {
this.isEditing = false;
this.isDropdownShowing = false;
},
setSearch(search) {
this.search = search;
},
iterationOptionText(iteration) {
return iteration.title
? `${iteration.title}: ${getIterationPeriod(iteration)}`
: getIterationPeriod(iteration);
},
},
i18n: {
label: s__('BoardScope|Iteration'),
errorSearchingIterations: s__(
'BoardScope|An error occurred while getting iterations. Please try again.',
),
searchIterations: s__('BoardScope|Search iterations'),
selectIteration: s__('BoardScope|Select iteration'),
edit: __('Edit'),
},
};
</script>
<template>
<div class="block iteration">
<div class="title gl-mb-3">
{{ $options.i18n.label }}
<gl-button
v-if="canEdit"
category="tertiary"
size="small"
class="edit-link float-right"
@click="toggleEdit"
>
{{ $options.i18n.edit }}
</gl-button>
</div>
<div v-if="!isEditing" :class="iterationTitleClass" data-testid="selected-iteration">
{{ iterationTitle }}
</div>
<dropdown-widget
v-show="isEditing"
ref="editDropdown"
:select-text="$options.i18n.selectIteration"
:search-text="$options.i18n.searchIterations"
:preset-options="$options.IterationsPreset"
:grouped-options="iterationsByCadence"
:is-loading="isLoading"
:selected="selected"
:search-term="search"
@hide="hideDropdown"
@set-option="selectIteration"
@set-search="setSearch"
/>
</div>
</template>
......@@ -31,6 +31,7 @@ export const FilterFields = {
'iterationId',
'iterationTitle',
'iterationWildcardId',
'iterationCadenceId',
],
[issuableTypes.epic]: ['authorUsername', 'labelName', 'search', 'myReactionEmoji'],
};
......@@ -46,6 +47,26 @@ export const IterationIDs = {
CURRENT: 'gid://gitlab/Iteration/-4',
};
export const ANY_ITERATION = {
id: 'gid://gitlab/Iteration/-1',
title: s__('BoardScope|Any iteration'),
iterationCadenceId: null,
};
export const NO_ITERATION = {
id: 'gid://gitlab/Iteration/0',
title: s__('BoardScope|No iteration'),
iterationCadenceId: null,
};
export const CURRENT_ITERATION = {
id: 'gid://gitlab/Iteration/-4',
title: s__('BoardScope|Current iteration'),
iterationCadenceId: null,
};
export const IterationsPreset = [ANY_ITERATION, NO_ITERATION, CURRENT_ITERATION];
export const MilestoneFilterType = {
any: 'Any',
none: 'None',
......
......@@ -22,5 +22,10 @@ fragment BoardScopeFragment on Board {
iteration {
...Iteration
}
iterationCadence {
id
title
durationInWeeks
}
weight
}
......@@ -6,5 +6,6 @@ fragment Iteration on Iteration {
iterationCadence {
id
title
durationInWeeks
}
}
......@@ -278,7 +278,62 @@ RSpec.describe 'Scoped issue boards', :js do
end
end
context 'iteration' do
context 'iteration - iteration_cadences FF on' do
let_it_be(:cadence) { create(:iterations_cadence, group: group, active: true, duration_in_weeks: 1, title: 'one week iterations') }
let_it_be(:iteration) { create(:current_iteration, :skip_future_date_validation, iterations_cadence: cadence, title: 'one test', group: group, start_date: 1.day.ago, due_date: Date.today) }
before do
stub_feature_flags(iteration_cadences: true)
visit project_boards_path(project)
wait_for_requests
end
it 'sets board iteration' do
update_board_iteration(iteration.title)
expect(find('.gl-filtered-search-scrollable')).to have_content(cadence.title)
expect(page).to have_selector('.board-card', count: 0)
end
it 'sets board to any iteration' do
update_board_iteration('Any iteration')
expect(find('.gl-filtered-search-scrollable')).not_to have_content(iteration.title)
expect(page).to have_selector('.board', count: 2)
expect(all('.board').first).to have_selector('.board-card', count: 2)
expect(all('.board').last).to have_selector('.board-card', count: 1)
end
it 'sets board to current iteration' do
update_board_iteration('Current')
expect(find('.gl-filtered-search-scrollable')).not_to have_content(iteration.title)
expect(find('.gl-filtered-search-scrollable')).to have_content('Current')
expect(all('.board')[1]).to have_selector('.board-card', count: 0)
end
it 'does not display iteration in search hint' do
update_board_iteration(iteration.title)
filtered_search.click
page.within('.gl-filtered-search-suggestion-list') do
expect(page).to have_content(_('Label'))
expect(page).not_to have_content(_('Iteration'))
end
end
end
context 'iteration - iteration_cadences FF off' do
before do
stub_feature_flags(iteration_cadences: false)
visit project_boards_path(project)
wait_for_requests
end
context 'group with iterations' do
let_it_be(:cadence) { create(:iterations_cadence, group: group) }
let_it_be(:iteration) { create(:current_iteration, :skip_future_date_validation, iterations_cadence: cadence, group: group, start_date: 1.day.ago, due_date: Date.today) }
......@@ -296,10 +351,6 @@ RSpec.describe 'Scoped issue boards', :js do
end
context 'board scoped to current iteration' do
before do
stub_feature_flags(iteration_cadences: false)
end
it 'adds current iteration to new issues' do
update_board_scope('current_iteration', true)
......@@ -583,6 +634,10 @@ RSpec.describe 'Scoped issue boards', :js do
update_board_scope('weight', weight.to_s)
end
def update_board_iteration(iteration_title)
update_board_scope('iteration', iteration_title)
end
def create_board_scope(filter, value)
click_on_create_new_board
find('#board-new-name').set 'test'
......
......@@ -181,13 +181,14 @@ describe('transformBoardConfig', () => {
{ id: 6, title: 'On hold', color: '#34ebec', type: 'GroupLabel', textColor: '#333333' },
],
weight: 0,
iterationId: 'gid://gitlab/Iteration/1',
};
it('formats url parameters from boardConfig object', () => {
const result = transformBoardConfig(boardConfig);
expect(result).toBe(
'milestone_title=milestone&weight=0&assignee_username=username&label_name[]=Deliverable&label_name[]=On%20hold',
'milestone_title=milestone&iteration_id=1&weight=0&assignee_username=username&label_name[]=Deliverable&label_name[]=On%20hold',
);
});
......@@ -195,6 +196,8 @@ describe('transformBoardConfig', () => {
setWindowLocation('?label_name[]=Deliverable&label_name[]=On%20hold');
const result = transformBoardConfig(boardConfig);
expect(result).toBe('milestone_title=milestone&weight=0&assignee_username=username');
expect(result).toBe(
'milestone_title=milestone&iteration_id=1&weight=0&assignee_username=username',
);
});
});
import { GlModal } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import Vuex from 'vuex';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import BoardForm from 'ee/boards/components/board_form.vue';
import createEpicBoardMutation from 'ee/boards/graphql/epic_board_create.mutation.graphql';
import destroyEpicBoardMutation from 'ee/boards/graphql/epic_board_destroy.mutation.graphql';
......@@ -28,6 +28,8 @@ const currentBoard = {
labels: [],
milestone: {},
assignee: {},
iteration: {},
iterationCadence: {},
weight: null,
hideBacklogList: false,
hideClosedList: false,
......@@ -51,8 +53,8 @@ describe('BoardForm', () => {
const findModal = () => wrapper.findComponent(GlModal);
const findModalActionPrimary = () => findModal().props('actionPrimary');
const findFormWrapper = () => wrapper.find('[data-testid="board-form-wrapper"]');
const findDeleteConfirmation = () => wrapper.find('[data-testid="delete-confirmation-message"]');
const findFormWrapper = () => wrapper.findByTestId('board-form-wrapper');
const findDeleteConfirmation = () => wrapper.findByTestId('delete-confirmation-message');
const findInput = () => wrapper.find('#board-new-name');
const createStore = ({ getters = {} } = {}) => {
......@@ -67,11 +69,12 @@ describe('BoardForm', () => {
});
};
const createComponent = (props) => {
wrapper = shallowMount(BoardForm, {
const createComponent = ({ props, iterationCadences = false } = {}) => {
wrapper = shallowMountExtended(BoardForm, {
propsData: { ...defaultProps, ...props },
provide: {
rootPath: 'root',
glFeatures: { iterationCadences },
},
mocks: {
$apollo: {
......@@ -97,7 +100,7 @@ describe('BoardForm', () => {
describe('on non-scoped-board', () => {
beforeEach(() => {
createComponent({ canAdminBoard: true, currentPage: formType.new });
createComponent({ props: { canAdminBoard: true, currentPage: formType.new } });
});
it('clears the form', () => {
......@@ -140,7 +143,7 @@ describe('BoardForm', () => {
});
it('does not call API if board name is empty', async () => {
createComponent({ canAdminBoard: true, currentPage: formType.new });
createComponent({ props: { canAdminBoard: true, currentPage: formType.new } });
findInput().trigger('keyup.enter', { metaKey: true });
await waitForPromises();
......@@ -149,7 +152,7 @@ describe('BoardForm', () => {
});
it('calls a correct GraphQL mutation and redirects to correct page from existing board', async () => {
createComponent({ canAdminBoard: true, currentPage: formType.new });
createComponent({ props: { canAdminBoard: true, currentPage: formType.new } });
fillForm();
await waitForPromises();
......@@ -169,7 +172,7 @@ describe('BoardForm', () => {
it('shows a GlAlert if GraphQL mutation fails', async () => {
mutate = jest.fn().mockRejectedValue('Houston, we have a problem');
createComponent({ canAdminBoard: true, currentPage: formType.new });
createComponent({ props: { canAdminBoard: true, currentPage: formType.new } });
jest.spyOn(wrapper.vm, 'setError').mockImplementation(() => {});
fillForm();
......@@ -202,21 +205,69 @@ describe('BoardForm', () => {
});
createComponent({
currentBoard: {
...currentBoard,
assignee: {
id: 1,
},
milestone: {
id: 'gid://gitlab/Milestone/2',
props: {
currentBoard: {
...currentBoard,
assignee: {
id: 1,
},
milestone: {
id: 'gid://gitlab/Milestone/2',
},
iteration: {
id: 'gid://gitlab/Iteration/3',
},
},
iteration: {
id: 'gid://gitlab/Iteration/3',
canAdminBoard: true,
currentPage: formType.edit,
scopedIssueBoardFeatureEnabled: true,
},
});
findInput().trigger('keyup.enter', { metaKey: true });
await waitForPromises();
expect(mutate).toHaveBeenCalledWith({
mutation: updateBoardMutation,
variables: {
input: expect.objectContaining({
id: currentBoard.id,
assigneeId: 'gid://gitlab/User/1',
milestoneId: 'gid://gitlab/Milestone/2',
iterationId: 'gid://gitlab/Iteration/3',
}),
},
});
});
it('should send iterationCadenceId when feature flag is on', async () => {
mutate = jest.fn().mockResolvedValue({
data: {
updateBoard: { board: { id: 'gid://gitlab/Board/321' } },
},
});
createComponent({
props: {
currentBoard: {
...currentBoard,
assignee: {
id: 1,
},
milestone: {
id: 'gid://gitlab/Milestone/2',
},
iteration: {
id: 'gid://gitlab/Iteration/3',
},
iterationCadenceId: 'gid://gitlab/Iterations::Cadence/4',
},
canAdminBoard: true,
currentPage: formType.edit,
scopedIssueBoardFeatureEnabled: true,
},
canAdminBoard: true,
currentPage: formType.edit,
scopedIssueBoardFeatureEnabled: true,
iterationCadences: true,
});
findInput().trigger('keyup.enter', { metaKey: true });
......@@ -231,6 +282,7 @@ describe('BoardForm', () => {
assigneeId: 'gid://gitlab/User/1',
milestoneId: 'gid://gitlab/Milestone/2',
iterationId: 'gid://gitlab/Iteration/3',
iterationCadenceId: 'gid://gitlab/Iterations::Cadence/4',
}),
},
});
......@@ -251,9 +303,11 @@ describe('BoardForm', () => {
},
});
createComponent({
canAdminBoard: true,
currentPage: formType.edit,
currentBoard: currentEpicBoard,
props: {
canAdminBoard: true,
currentPage: formType.edit,
currentBoard: currentEpicBoard,
},
});
findInput().trigger('keyup.enter', { metaKey: true });
......@@ -276,9 +330,11 @@ describe('BoardForm', () => {
it('shows a GlAlert if GraphQL mutation fails', async () => {
mutate = jest.fn().mockRejectedValue('Houston, we have a problem');
createComponent({
canAdminBoard: true,
currentPage: formType.edit,
currentBoard: currentEpicBoard,
props: {
canAdminBoard: true,
currentPage: formType.edit,
currentBoard: currentEpicBoard,
},
});
jest.spyOn(wrapper.vm, 'setError').mockImplementation(() => {});
findInput().trigger('keyup.enter', { metaKey: true });
......@@ -300,9 +356,11 @@ describe('BoardForm', () => {
it('passes correct primary action text and variant', () => {
createComponent({
canAdminBoard: true,
currentPage: formType.delete,
currentBoard: currentEpicBoard,
props: {
canAdminBoard: true,
currentPage: formType.delete,
currentBoard: currentEpicBoard,
},
});
expect(findModalActionPrimary().text).toBe('Delete');
expect(findModalActionPrimary().attributes[0].variant).toBe('danger');
......@@ -310,9 +368,11 @@ describe('BoardForm', () => {
it('renders delete confirmation message', () => {
createComponent({
canAdminBoard: true,
currentPage: formType.delete,
currentBoard: currentEpicBoard,
props: {
canAdminBoard: true,
currentPage: formType.delete,
currentBoard: currentEpicBoard,
},
});
expect(findDeleteConfirmation().exists()).toBe(true);
});
......@@ -320,9 +380,11 @@ describe('BoardForm', () => {
it('calls a correct GraphQL mutation and redirects to correct page after deleting board', async () => {
mutate = jest.fn().mockResolvedValue({});
createComponent({
canAdminBoard: true,
currentPage: formType.delete,
currentBoard: currentEpicBoard,
props: {
canAdminBoard: true,
currentPage: formType.delete,
currentBoard: currentEpicBoard,
},
});
findModal().vm.$emit('primary');
......@@ -342,9 +404,11 @@ describe('BoardForm', () => {
it('shows a GlAlert if GraphQL mutation fails', async () => {
mutate = jest.fn().mockRejectedValue('Houston, we have a problem');
createComponent({
canAdminBoard: true,
currentPage: formType.delete,
currentBoard: currentEpicBoard,
props: {
canAdminBoard: true,
currentPage: formType.delete,
currentBoard: currentEpicBoard,
},
});
jest.spyOn(wrapper.vm, 'setError').mockImplementation(() => {});
findModal().vm.$emit('primary');
......
import { GlButton, GlDropdown, GlDropdownItem } from '@gitlab/ui';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import Vuex from 'vuex';
import IterationSelect from 'ee/boards/components/iteration_select.vue';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { boardObj } from 'jest/boards/mock_data';
import defaultStore from '~/boards/stores';
import searchIterationQuery from 'ee/issues/list/queries/search_iterations.query.graphql';
import DropdownWidget from '~/vue_shared/components/dropdown/dropdown_widget/dropdown_widget.vue';
import { mockIterationsResponse, mockIterations } from './mock_data';
Vue.use(VueApollo);
describe('Iteration select component', () => {
let wrapper;
let fakeApollo;
const selectedText = () => wrapper.findByTestId('selected-iteration').text();
const findEditButton = () => wrapper.findComponent(GlButton);
const findDropdown = () => wrapper.findComponent(DropdownWidget);
const iterationsQueryHandlerSuccess = jest.fn().mockResolvedValue(mockIterationsResponse);
const createStore = () => {
return new Vuex.Store({
...defaultStore,
getters: {
isGroupBoard: () => true,
isProjectBoard: () => false,
},
actions: {
setError: jest.fn(),
},
});
};
const createComponent = ({ props = {} } = {}) => {
const store = createStore();
fakeApollo = createMockApollo([[searchIterationQuery, iterationsQueryHandlerSuccess]]);
wrapper = shallowMountExtended(IterationSelect, {
store,
apolloProvider: fakeApollo,
propsData: {
board: boardObj,
canEdit: true,
...props,
},
provide: {
fullPath: 'gitlab-org',
},
stubs: {
GlDropdown,
GlDropdownItem,
},
});
// We need to mock out `showDropdown` which
// invokes `show` method of BDropdown used inside GlDropdown.
jest.spyOn(wrapper.vm, 'showDropdown').mockImplementation();
};
afterEach(() => {
wrapper.destroy();
fakeApollo = null;
});
describe('when not editing', () => {
beforeEach(() => {
createComponent();
});
it('defaults to Any iteration', () => {
expect(selectedText()).toContain('Any iteration');
});
it('skips the queries and does not render dropdown', () => {
expect(iterationsQueryHandlerSuccess).not.toHaveBeenCalled();
expect(findDropdown().isVisible()).toBe(false);
});
it('renders selected iteration', async () => {
findEditButton().vm.$emit('click');
findDropdown().vm.$emit('set-option', mockIterations[1]);
await nextTick();
expect(selectedText()).toContain(mockIterations[1].title);
});
it('shows Edit button if canEdit is true', () => {
expect(findEditButton().exists()).toBe(true);
});
});
describe('when editing', () => {
beforeEach(() => {
createComponent();
});
it('trigger query and renders dropdown with passed iterations', async () => {
findEditButton().vm.$emit('click');
await waitForPromises();
expect(iterationsQueryHandlerSuccess).toHaveBeenCalled();
expect(findDropdown().isVisible()).toBe(true);
expect(findDropdown().props('groupedOptions')).toHaveLength(2);
});
});
describe('canEdit', () => {
beforeEach(() => {
createComponent({ props: { canEdit: false } });
});
it('hides Edit button if false', () => {
expect(findEditButton().exists()).toBe(false);
});
});
});
......@@ -146,9 +146,12 @@ export const mockIterations = [
iterationCadence: {
id: 'gid://gitlab/Iterations::Cadence/1',
title: 'GitLab.org Iterations',
durationInWeeks: 1,
__typename: 'IterationCadence',
},
startDate: '2021-10-05',
dueDate: '2021-10-10',
__typename: 'Iteration',
},
{
id: 'gid://gitlab/Iteration/2',
......@@ -156,12 +159,27 @@ export const mockIterations = [
iterationCadence: {
id: 'gid://gitlab/Iterations::Cadence/2',
title: 'GitLab.org Iterations: Volume II',
durationInWeeks: 2,
__typename: 'IterationCadence',
},
startDate: '2021-10-12',
dueDate: '2021-10-17',
__typename: 'Iteration',
},
];
export const mockIterationsResponse = {
data: {
group: {
id: 'gid://gitlab/Group/1',
iterations: {
nodes: mockIterations,
},
__typename: 'Group',
},
},
};
export const labels = [
{
id: 'gid://gitlab/GroupLabel/5',
......
......@@ -31,7 +31,7 @@ import {
mockIssue,
mockIssues,
mockEpic,
mockMilestones,
mockIterations,
mockAssignees,
mockSubGroups,
mockGroup0,
......@@ -1108,7 +1108,7 @@ describe('fetchIterations', () => {
data: {
group: {
iterations: {
nodes: mockMilestones,
nodes: mockIterations,
},
},
},
......@@ -1156,7 +1156,7 @@ describe('fetchIterations', () => {
await actions.fetchIterations(store);
expect(store.state.iterationsLoading).toBe(false);
expect(store.state.iterations).toBe(mockMilestones);
expect(store.state.iterations).toBe(mockIterations);
});
});
......
......@@ -5852,6 +5852,9 @@ msgstr ""
msgid "BoardNewIssue|Select a project"
msgstr ""
msgid "BoardScope|An error occurred while getting iterations. Please try again."
msgstr ""
msgid "BoardScope|An error occurred while getting milestones, please try again."
msgstr ""
......@@ -5867,6 +5870,9 @@ msgstr ""
msgid "BoardScope|Any assignee"
msgstr ""
msgid "BoardScope|Any iteration"
msgstr ""
msgid "BoardScope|Any label"
msgstr ""
......@@ -5876,24 +5882,39 @@ msgstr ""
msgid "BoardScope|Choose labels"
msgstr ""
msgid "BoardScope|Current iteration"
msgstr ""
msgid "BoardScope|Edit"
msgstr ""
msgid "BoardScope|Iteration"
msgstr ""
msgid "BoardScope|Labels"
msgstr ""
msgid "BoardScope|Milestone"
msgstr ""
msgid "BoardScope|No iteration"
msgstr ""
msgid "BoardScope|No milestone"
msgstr ""
msgid "BoardScope|Search iterations"
msgstr ""
msgid "BoardScope|Search milestones"
msgstr ""
msgid "BoardScope|Select assignee"
msgstr ""
msgid "BoardScope|Select iteration"
msgstr ""
msgid "BoardScope|Select labels"
msgstr ""
......
import { GlModal } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import setWindowLocation from 'helpers/set_window_location_helper';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import BoardForm from '~/boards/components/board_form.vue';
......@@ -22,6 +22,8 @@ const currentBoard = {
labels: [],
milestone: {},
assignee: {},
iteration: {},
iterationCadence: {},
weight: null,
hideBacklogList: false,
hideClosedList: false,
......@@ -37,11 +39,11 @@ describe('BoardForm', () => {
let wrapper;
let mutate;
const findModal = () => wrapper.find(GlModal);
const findModal = () => wrapper.findComponent(GlModal);
const findModalActionPrimary = () => findModal().props('actionPrimary');
const findForm = () => wrapper.find('[data-testid="board-form"]');
const findFormWrapper = () => wrapper.find('[data-testid="board-form-wrapper"]');
const findDeleteConfirmation = () => wrapper.find('[data-testid="delete-confirmation-message"]');
const findForm = () => wrapper.findByTestId('board-form');
const findFormWrapper = () => wrapper.findByTestId('board-form-wrapper');
const findDeleteConfirmation = () => wrapper.findByTestId('delete-confirmation-message');
const findInput = () => wrapper.find('#board-new-name');
const store = createStore({
......@@ -52,7 +54,7 @@ describe('BoardForm', () => {
});
const createComponent = (props, data) => {
wrapper = shallowMount(BoardForm, {
wrapper = shallowMountExtended(BoardForm, {
propsData: { ...defaultProps, ...props },
data() {
return {
......
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