Commit d88a3e5a authored by Florie Guibert's avatar Florie Guibert Committed by Natalia Tepluhina

Refactor board scope milestone dropdown

parent 61bce846
...@@ -18,7 +18,7 @@ const boardDefaults = { ...@@ -18,7 +18,7 @@ const boardDefaults = {
id: false, id: false,
name: '', name: '',
labels: [], labels: [],
milestone_id: undefined, milestone: {},
iteration_id: undefined, iteration_id: undefined,
assignee: {}, assignee: {},
weight: null, weight: null,
...@@ -190,10 +190,9 @@ export default { ...@@ -190,10 +190,9 @@ export default {
return { return {
weight: this.board.weight, weight: this.board.weight,
assigneeId: this.board.assignee?.id || null, assigneeId: this.board.assignee?.id || null,
milestoneId: milestoneId: this.board.milestone?.id
this.board.milestone?.id || this.board.milestone?.id === 0 ? convertToGraphQLId(TYPE_MILESTONE, this.board.milestone.id)
? convertToGraphQLId(TYPE_MILESTONE, this.board.milestone.id) : null,
: null,
iterationId: this.board.iteration_id iterationId: this.board.iteration_id
? convertToGraphQLId(TYPE_ITERATION, this.board.iteration_id) ? convertToGraphQLId(TYPE_ITERATION, this.board.iteration_id)
: null, : null,
...@@ -304,9 +303,14 @@ export default { ...@@ -304,9 +303,14 @@ export default {
}); });
}, },
setAssignee(assigneeId) { setAssignee(assigneeId) {
this.board.assignee = { this.$set(this.board, 'assignee', {
id: assigneeId, id: assigneeId,
}; });
},
setMilestone(milestoneId) {
this.$set(this.board, 'milestone', {
id: milestoneId,
});
}, },
}, },
}; };
...@@ -376,6 +380,7 @@ export default { ...@@ -376,6 +380,7 @@ export default {
@set-iteration="setIteration" @set-iteration="setIteration"
@set-board-labels="setBoardLabels" @set-board-labels="setBoardLabels"
@set-assignee="setAssignee" @set-assignee="setAssignee"
@set-milestone="setMilestone"
/> />
</form> </form>
</gl-modal> </gl-modal>
......
import { isArray } from 'lodash'; import { isArray } from 'lodash';
/**
* Ids generated by GraphQL endpoints are usually in the format
* gid://gitlab/Environments/123. This method checks if the passed id follows that format
*
* @param {String|Number} id The id value
* @returns {Boolean}
*/
export const isGid = (id) => {
if (typeof id === 'string' && id.startsWith('gid://gitlab/')) {
return true;
}
return false;
};
/** /**
* Ids generated by GraphQL endpoints are usually in the format * Ids generated by GraphQL endpoints are usually in the format
* gid://gitlab/Environments/123. This method extracts Id number * gid://gitlab/Environments/123. This method extracts Id number
...@@ -35,6 +50,10 @@ export const convertToGraphQLId = (type, id) => { ...@@ -35,6 +50,10 @@ export const convertToGraphQLId = (type, id) => {
throw new TypeError(`id must be a number or string; got ${typeof id}`); throw new TypeError(`id must be a number or string; got ${typeof id}`);
} }
if (isGid(id)) {
return id;
}
return `gid://gitlab/${type}/${id}`; return `gid://gitlab/${type}/${id}`;
}; };
......
#import "./milestone.fragment.graphql"
query groupMilestones($fullPath: ID!, $title: String, $state: MilestoneStateEnum) {
workspace: group(fullPath: $fullPath) {
__typename
id
attributes: milestones(
searchTitle: $title
state: $state
sort: EXPIRED_LAST_DUE_DATE_ASC
first: 20
includeAncestors: true
) {
nodes {
...MilestoneFragment
state
}
}
}
}
/* eslint-disable @gitlab/require-i18n-strings */
import { __ } from '~/locale';
import DropdownWidget from './dropdown_widget.vue';
export default {
component: DropdownWidget,
title: 'vue_shared/components/dropdown/dropdown_widget/dropdown_widget',
};
const Template = (args, { argTypes }) => ({
components: { DropdownWidget },
props: Object.keys(argTypes),
template: '<dropdown-widget v-bind="$props" v-on="$props" />',
});
export const Default = Template.bind({});
Default.args = {
options: [
{ id: 'gid://gitlab/Milestone/-1', title: __('Any Milestone') },
{ id: 'gid://gitlab/Milestone/0', title: __('No Milestone') },
{ id: 'gid://gitlab/Milestone/-2', title: __('Upcoming') },
{ id: 'gid://gitlab/Milestone/-3', title: __('Started') },
],
selectText: 'Select',
searchText: 'Search',
};
<script>
import {
GlLoadingIcon,
GlDropdown,
GlDropdownForm,
GlDropdownDivider,
GlDropdownItem,
GlSearchBoxByType,
} from '@gitlab/ui';
import { __ } from '~/locale';
export default {
components: {
GlLoadingIcon,
GlDropdown,
GlDropdownForm,
GlDropdownDivider,
GlDropdownItem,
GlSearchBoxByType,
},
props: {
selectText: {
type: String,
required: false,
default: __('Select'),
},
searchText: {
type: String,
required: false,
default: __('Search'),
},
presetOptions: {
type: Array,
required: false,
default: () => [],
},
options: {
type: Array,
required: false,
default: () => [],
},
isLoading: {
type: Boolean,
required: false,
default: false,
},
selected: {
type: Object,
required: false,
default: () => {},
},
searchTerm: {
type: String,
required: false,
default: '',
},
},
computed: {
isSearchEmpty() {
return this.searchTerm === '' && !this.isLoading;
},
noOptionsFound() {
return !this.isSearchEmpty && this.options.length === 0;
},
},
methods: {
selectOption(option) {
this.$emit('set-option', option || null);
},
isSelected(option) {
return this.selected && this.selected.title === option.title;
},
showDropdown() {
this.$refs.dropdown.show();
},
setFocus() {
this.$refs.search.focusInput();
},
setSearchTerm(search) {
this.$emit('set-search', search);
},
},
i18n: {
noMatchingResults: __('No matching results'),
},
};
</script>
<template>
<gl-dropdown
ref="dropdown"
:text="selectText"
lazy
menu-class="gl-w-full!"
class="gl-w-full"
v-on="$listeners"
@shown="setFocus"
>
<template #header>
<gl-search-box-by-type
ref="search"
:value="searchTerm"
:placeholder="searchText"
class="js-dropdown-input-field"
@input="setSearchTerm"
/>
</template>
<gl-dropdown-form class="gl-relative gl-min-h-7">
<gl-loading-icon
v-if="isLoading"
size="md"
class="gl-absolute gl-left-0 gl-top-0 gl-right-0"
/>
<template v-else>
<template v-if="isSearchEmpty && presetOptions.length > 0">
<gl-dropdown-item
v-for="option in presetOptions"
:key="option.id"
:is-checked="isSelected(option)"
:is-check-centered="true"
:is-check-item="true"
@click="selectOption(option)"
>
{{ option.title }}
</gl-dropdown-item>
<gl-dropdown-divider />
</template>
<gl-dropdown-item
v-for="option in options"
:key="option.id"
:is-checked="isSelected(option)"
:is-check-centered="true"
:is-check-item="true"
data-testid="unselected-option"
@click="selectOption(option)"
>
{{ option.title }}
</gl-dropdown-item>
<gl-dropdown-item v-if="noOptionsFound" class="gl-pl-6!">
{{ $options.i18n.noMatchingResults }}
</gl-dropdown-item>
</template>
</gl-dropdown-form>
<template #footer>
<slot name="footer"></slot>
</template>
</gl-dropdown>
</template>
...@@ -101,6 +101,7 @@ export default { ...@@ -101,6 +101,7 @@ export default {
:group-id="groupId" :group-id="groupId"
:project-id="projectId" :project-id="projectId"
:can-edit="canAdminBoard" :can-edit="canAdminBoard"
@set-milestone="$emit('set-milestone', $event)"
/> />
<board-scope-current-iteration <board-scope-current-iteration
......
<script> <script>
import { GlLoadingIcon, GlIcon } from '@gitlab/ui'; import { GlButton } from '@gitlab/ui';
import MilestoneSelect from '~/milestone_select'; import { isEmpty } from 'lodash';
import { mapActions, mapGetters } from 'vuex';
import { s__ } from '~/locale';
const ANY_MILESTONE = 'Any milestone'; import groupMilestonesQuery from '~/sidebar/queries/group_milestones.query.graphql';
const NO_MILESTONE = 'No milestone'; import projectMilestonesQuery from '~/sidebar/queries/project_milestones.query.graphql';
import DropdownWidget from '~/vue_shared/components/dropdown/dropdown_widget/dropdown_widget.vue';
import { MilestonesPreset, ANY_MILESTONE } from '../constants';
export default { export default {
MilestonesPreset,
components: { components: {
GlLoadingIcon, GlButton,
GlIcon, DropdownWidget,
}, },
inject: ['fullPath'],
props: { props: {
board: { board: {
type: Object, type: Object,
...@@ -31,48 +39,93 @@ export default { ...@@ -31,48 +39,93 @@ export default {
default: false, default: false,
}, },
}, },
data() {
computed: { return {
milestoneTitle() { search: '',
if (this.noMilestone) return NO_MILESTONE; milestones: [],
return this.board.milestone ? this.board.milestone.title : ANY_MILESTONE; selected: this.board.milestone,
isEditing: false,
isDropdownShowing: false,
};
},
apollo: {
milestones: {
query() {
return this.isProjectBoard ? projectMilestonesQuery : groupMilestonesQuery;
},
variables() {
return {
fullPath: this.fullPath,
title: this.search,
first: 20,
};
},
skip() {
return !this.isEditing;
},
update(data) {
return data?.workspace?.attributes?.nodes || [];
},
error() {
this.setError({ message: this.$options.i18n.errorSearchingMilestones });
},
}, },
noMilestone() { },
return this.milestoneId === 0; computed: {
...mapGetters(['isProjectBoard']),
anyMilestone() {
return this.selected.title === ANY_MILESTONE.title;
}, },
milestoneId() { milestoneTitle() {
return this.board.milestone_id; return this.selected.title;
}, },
milestoneTitleClass() { milestoneTitleClass() {
return this.milestoneTitle === ANY_MILESTONE ? 'text-secondary' : 'bold'; return this.anyMilestone ? 'gl-text-gray-500' : 'gl-font-weight-bold';
}, },
selected() { isLoading() {
if (this.noMilestone) return NO_MILESTONE; return this.$apollo.queries.milestones.loading;
return this.board.milestone ? this.board.milestone.name : '';
}, },
}, },
mounted() { created() {
this.milestoneDropdown = new MilestoneSelect(null, this.$refs.dropdownButton, { if (isEmpty(this.board.milestone)) {
handleClick: this.selectMilestone, this.selected = ANY_MILESTONE;
}); }
}, },
methods: { methods: {
...mapActions(['setError']),
selectMilestone(milestone) { selectMilestone(milestone) {
let { id } = milestone; this.selected = milestone;
// swap the IDs of 'Any' and 'No' milestone to what backend requires this.toggleEdit();
if (milestone.title === ANY_MILESTONE) { this.$emit('set-milestone', milestone?.id || null);
id = -1; },
} else if (milestone.title === NO_MILESTONE) { toggleEdit() {
id = 0; if (!this.isEditing && !this.isDropdownShowing) {
this.isEditing = true;
this.showDropdown();
} else {
this.isEditing = false;
this.isDropdownShowing = false;
} }
// eslint-disable-next-line vue/no-mutating-props
this.board.milestone_id = id;
// eslint-disable-next-line vue/no-mutating-props
this.board.milestone = {
...milestone,
id,
};
}, },
showDropdown() {
this.$refs.editDropdown.showDropdown();
this.isDropdownShowing = true;
},
hideDropdown() {
this.isEditing = false;
},
setSearch(search) {
this.search = search;
},
},
i18n: {
label: s__('BoardScope|Milestone'),
errorSearchingMilestones: s__(
'BoardScope|An error occurred while getting milestones, please try again.',
),
searchMilestones: s__('BoardScope|Search milestones'),
selectMilestone: s__('BoardScope|Select milestone'),
edit: s__('BoardScope|Edit'),
}, },
}; };
</script> </script>
...@@ -80,60 +133,33 @@ export default { ...@@ -80,60 +133,33 @@ export default {
<template> <template>
<div class="block milestone"> <div class="block milestone">
<div class="title gl-mb-3"> <div class="title gl-mb-3">
{{ __('Milestone') }} {{ $options.i18n.label }}
<button v-if="canEdit" type="button" class="edit-link btn btn-blank float-right"> <gl-button
{{ __('Edit') }} v-if="canEdit"
</button> variant="link"
class="edit-link float-right gl-text-gray-900!"
@click="toggleEdit"
>
{{ $options.i18n.edit }}
</gl-button>
</div> </div>
<div :class="milestoneTitleClass" class="value">{{ milestoneTitle }}</div> <div v-if="!isEditing" :class="milestoneTitleClass" data-testid="selected-milestone">
<div class="selectbox" style="display: none"> {{ milestoneTitle }}
<input :value="milestoneId" name="milestone_id" type="hidden" />
<div class="dropdown">
<!-- eslint-disable @gitlab/vue-no-data-toggle -->
<button
ref="dropdownButton"
:data-selected="selected"
:data-project-id="projectId"
:data-group-id="groupId"
:data-show-no="true"
:data-show-any="true"
:data-show-started="true"
:data-show-upcoming="true"
:data-use-id="true"
class="dropdown-menu-toggle wide"
data-toggle="dropdown"
type="button"
>
{{ __('Milestone') }}
<gl-icon
name="chevron-down"
class="gl-absolute gl-top-3 gl-right-3 gl-text-gray-500"
:size="16"
/>
</button>
<!-- eslint-enable @gitlab/vue-no-data-toggle -->
<div class="dropdown-menu dropdown-select dropdown-menu-selectable">
<div class="dropdown-input">
<input
type="search"
class="dropdown-input-field"
:placeholder="__('Search milestones')"
autocomplete="off"
/>
<gl-icon
name="search"
class="dropdown-input-search gl-absolute gl-top-3 gl-right-5 gl-text-gray-300 gl-pointer-events-none"
/>
<gl-icon
name="close"
class="dropdown-input-clear js-dropdown-input-clear gl-right-5 gl-absolute gl-top-3 gl-text-gray-500"
/>
</div>
<div class="dropdown-content"></div>
<div class="dropdown-loading"><gl-loading-icon size="sm" /></div>
</div>
</div>
</div> </div>
<dropdown-widget
v-show="isEditing"
ref="editDropdown"
:select-text="$options.i18n.selectMilestone"
:search-text="$options.i18n.searchMilestones"
:preset-options="$options.MilestonesPreset"
:options="milestones"
:is-loading="isLoading"
:selected="selected"
:search-term="search"
@hide="hideDropdown"
@set-option="selectMilestone"
@set-search="setSearch"
/>
</div> </div>
</template> </template>
...@@ -55,6 +55,30 @@ export const MilestoneIDs = { ...@@ -55,6 +55,30 @@ export const MilestoneIDs = {
NONE: 0, NONE: 0,
}; };
export const ANY_MILESTONE = {
id: 'gid://gitlab/Milestone/-1',
title: s__('BoardScope|Any Milestone'),
};
export const NO_MILESTONE = {
id: 'gid://gitlab/Milestone/0',
title: s__('BoardScope|No milestone'),
};
export const UPCOMING_MILESTONE = {
id: 'gid://gitlab/Milestone/-2',
title: s__('BoardScope|Upcoming'),
};
export const STARTED_MILESTONE = {
id: 'gid://gitlab/Milestone/-3',
title: s__('BoardScope|Started'),
};
export const MilestonesPreset = [
ANY_MILESTONE,
NO_MILESTONE,
UPCOMING_MILESTONE,
STARTED_MILESTONE,
];
export const WeightFilterType = { export const WeightFilterType = {
none: 'None', none: 'None',
}; };
......
...@@ -65,8 +65,8 @@ RSpec.describe 'Scoped issue boards', :js do ...@@ -65,8 +65,8 @@ RSpec.describe 'Scoped issue boards', :js do
expect(page).to have_selector('.board-card', count: 2) expect(page).to have_selector('.board-card', count: 2)
end end
it 'creates board filtering by Any milestone' do it 'creates board filtering by Any Milestone' do
create_board_milestone('Any milestone') create_board_milestone('Any Milestone')
expect(find('.tokens-container')).to have_content("") expect(find('.tokens-container')).to have_content("")
expect(page).to have_selector('.board-card', count: 3) expect(page).to have_selector('.board-card', count: 3)
...@@ -228,7 +228,7 @@ RSpec.describe 'Scoped issue boards', :js do ...@@ -228,7 +228,7 @@ RSpec.describe 'Scoped issue boards', :js do
edit_board.click edit_board.click
expect(find('.milestone .value')).to have_content(milestone.title) expect(find('[data-testid="selected-milestone"]')).to have_content(milestone.title)
expect(find('[data-testid="selected-assignee"]')).to have_content(user.name) expect(find('[data-testid="selected-assignee"]')).to have_content(user.name)
expect(find('.weight .value')).to have_content(2) expect(find('.weight .value')).to have_content(2)
end end
...@@ -242,7 +242,7 @@ RSpec.describe 'Scoped issue boards', :js do ...@@ -242,7 +242,7 @@ RSpec.describe 'Scoped issue boards', :js do
end end
it 'sets board to any milestone' do it 'sets board to any milestone' do
update_board_milestone('Any milestone') update_board_milestone('Any Milestone')
expect(find('.tokens-container')).not_to have_content(milestone.title) expect(find('.tokens-container')).not_to have_content(milestone.title)
......
...@@ -37,6 +37,7 @@ describe('BoardScope', () => { ...@@ -37,6 +37,7 @@ describe('BoardScope', () => {
}, },
stubs: { stubs: {
AssigneeSelect: true, AssigneeSelect: true,
BoardMilestoneSelect: true,
}, },
}); });
} }
......
import Vue from 'vue'; import { GlButton, GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import Vuex from 'vuex';
import MilestoneSelect from 'ee/boards/components/milestone_select.vue'; import MilestoneSelect from 'ee/boards/components/milestone_select.vue';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { boardObj } from 'jest/boards/mock_data'; import { boardObj } from 'jest/boards/mock_data';
import Api from '~/api'; import { mockProjectMilestonesResponse, mockGroupMilestonesResponse } from 'jest/sidebar/mock_data';
import IssuableContext from '~/issuable_context';
let vm;
function selectedText() {
return vm.$el.querySelector('.value').innerText.trim();
}
function activeDropdownItem(index) {
const items = vm.$el.querySelectorAll('.is-active');
if (!items[index]) return '';
return items[index].innerText.trim();
}
const milestone = {
id: 1,
title: 'first milestone',
name: 'first milestone',
due_date: '2015-05-05',
expired: true,
};
const milestone2 = {
id: 2,
title: 'second milestone',
name: 'second milestone',
due_date: null,
expired: false,
};
describe('Milestone select component', () => { import defaultStore from '~/boards/stores';
beforeEach((done) => { import groupMilestonesQuery from '~/sidebar/queries/group_milestones.query.graphql';
setFixtures('<div class="test-container"></div>'); import projectMilestonesQuery from '~/sidebar/queries/project_milestones.query.graphql';
import DropdownWidget from '~/vue_shared/components/dropdown/dropdown_widget/dropdown_widget.vue';
// eslint-disable-next-line no-new const localVue = createLocalVue();
new IssuableContext(); localVue.use(VueApollo);
const Component = Vue.extend(MilestoneSelect); describe('Milestone select component', () => {
vm = new Component({ let wrapper;
let fakeApollo;
let store;
const selectedText = () => wrapper.find('[data-testid="selected-milestone"]').text();
const findEditButton = () => wrapper.findComponent(GlButton);
const findDropdown = () => wrapper.findComponent(DropdownWidget);
const milestonesQueryHandlerSuccess = jest.fn().mockResolvedValue(mockProjectMilestonesResponse);
const groupUsersQueryHandlerSuccess = jest.fn().mockResolvedValue(mockGroupMilestonesResponse);
const createStore = ({ isGroupBoard = false, isProjectBoard = false } = {}) => {
store = new Vuex.Store({
...defaultStore,
getters: {
isGroupBoard: () => isGroupBoard,
isProjectBoard: () => isProjectBoard,
},
});
};
const createComponent = ({
props = {},
milestonesQueryHandler = milestonesQueryHandlerSuccess,
} = {}) => {
fakeApollo = createMockApollo([
[projectMilestonesQuery, milestonesQueryHandler],
[groupMilestonesQuery, groupUsersQueryHandlerSuccess],
]);
wrapper = shallowMount(MilestoneSelect, {
localVue,
store,
apolloProvider: fakeApollo,
propsData: { propsData: {
board: boardObj, board: boardObj,
groupId: 2,
projectId: 2,
canEdit: true, canEdit: true,
...props,
},
provide: {
fullPath: 'gitlab-org',
},
stubs: {
GlDropdown,
GlDropdownItem,
}, },
}).$mount('.test-container'); });
setImmediate(done); // We need to mock out `showDropdown` which
}); // invokes `show` method of BDropdown used inside GlDropdown.
jest.spyOn(wrapper.vm, 'showDropdown').mockImplementation();
};
describe('canEdit', () => { beforeEach(() => {
it('hides Edit button', (done) => { createStore({ isProjectBoard: true });
vm.canEdit = false; createComponent();
Vue.nextTick(() => { });
expect(vm.$el.querySelector('.edit-link')).toBeFalsy();
done();
});
});
it('shows Edit button if true', (done) => { afterEach(() => {
vm.canEdit = true; wrapper.destroy();
Vue.nextTick(() => { fakeApollo = null;
expect(vm.$el.querySelector('.edit-link')).toBeTruthy(); store = null;
done();
});
});
}); });
describe('selected value', () => { describe('when not editing', () => {
it('defaults to Any milestone', () => { it('defaults to Any milestone', () => {
expect(selectedText()).toContain('Any milestone'); expect(selectedText()).toContain('Any Milestone');
}); });
it('shows No milestone', (done) => { it('skips the queries and does not render dropdown', () => {
vm.board.milestone_id = 0; expect(milestonesQueryHandlerSuccess).not.toHaveBeenCalled();
Vue.nextTick(() => { expect(findDropdown().isVisible()).toBe(false);
expect(selectedText()).toContain('No milestone');
done();
});
}); });
});
it('shows selected milestone title', (done) => { describe('when editing', () => {
vm.board.milestone_id = 20; it('trigger query and renders dropdown with passed milestones', async () => {
vm.board.milestone = { findEditButton().vm.$emit('click');
id: 20, await waitForPromises();
title: 'Selected milestone', await nextTick();
}; expect(milestonesQueryHandlerSuccess).toHaveBeenCalled();
Vue.nextTick(() => {
expect(selectedText()).toContain('Selected milestone');
done();
});
});
describe('clicking dropdown items', () => {
beforeEach(() => {
jest.spyOn(Api, 'projectMilestones').mockResolvedValue({ data: [milestone, milestone2] });
});
it('sets Any milestone', async (done) => {
vm.board.milestone_id = 0;
vm.$el.querySelector('.edit-link').click();
await vm.$nextTick();
jest.runOnlyPendingTimers();
setImmediate(() => {
vm.$el.querySelectorAll('li a')[0].click();
});
setImmediate(() => { expect(findDropdown().isVisible()).toBe(true);
expect(activeDropdownItem(0)).toEqual('Any milestone'); expect(findDropdown().props('options')).toHaveLength(2);
expect(selectedText()).toEqual('Any milestone'); });
done(); });
});
});
it('sets No milestone', (done) => { describe('canEdit', () => {
vm.$el.querySelector('.edit-link').click(); it('hides Edit button', async () => {
wrapper.setProps({ canEdit: false });
await nextTick();
jest.runOnlyPendingTimers(); expect(findEditButton().exists()).toBe(false);
});
setImmediate(() => { it('shows Edit button if true', () => {
vm.$el.querySelectorAll('li a')[1].click(); expect(findEditButton().exists()).toBe(true);
}); });
});
setImmediate(() => { it.each`
expect(activeDropdownItem(0)).toEqual('No milestone'); boardType | mockedResponse | queryHandler | notCalledHandler
expect(selectedText()).toEqual('No milestone'); ${'group'} | ${mockGroupMilestonesResponse} | ${groupUsersQueryHandlerSuccess} | ${milestonesQueryHandlerSuccess}
done(); ${'project'} | ${mockProjectMilestonesResponse} | ${milestonesQueryHandlerSuccess} | ${groupUsersQueryHandlerSuccess}
}); `(
'fetches $boardType milestones',
async ({ boardType, mockedResponse, queryHandler, notCalledHandler }) => {
createStore({ isProjectBoard: boardType === 'project', isGroupBoard: boardType === 'group' });
createComponent({
[queryHandler]: jest.fn().mockResolvedValue(mockedResponse),
}); });
it('sets milestone', (done) => { findEditButton().vm.$emit('click');
vm.$el.querySelector('.edit-link').click(); await waitForPromises();
await nextTick();
jest.runOnlyPendingTimers();
setImmediate(() => { expect(queryHandler).toHaveBeenCalled();
vm.$el.querySelectorAll('li a')[4].click(); expect(notCalledHandler).not.toHaveBeenCalled();
}); },
);
setImmediate(() => {
// "second milestone" is not expired, hence it shows up to the top.
expect(activeDropdownItem(0)).toBe('second milestone');
expect(selectedText()).toBe('second milestone');
expect(vm.board.milestone).toEqual(milestone2);
done();
});
});
});
});
}); });
...@@ -3891,6 +3891,9 @@ msgstr "" ...@@ -3891,6 +3891,9 @@ msgstr ""
msgid "Any Author" msgid "Any Author"
msgstr "" msgstr ""
msgid "Any Milestone"
msgstr ""
msgid "Any branch" msgid "Any branch"
msgstr "" msgstr ""
...@@ -5309,9 +5312,15 @@ msgstr "" ...@@ -5309,9 +5312,15 @@ msgstr ""
msgid "BoardNewIssue|Select a project" msgid "BoardNewIssue|Select a project"
msgstr "" msgstr ""
msgid "BoardScope|An error occurred while getting milestones, please try again."
msgstr ""
msgid "BoardScope|An error occurred while searching for users, please try again." msgid "BoardScope|An error occurred while searching for users, please try again."
msgstr "" msgstr ""
msgid "BoardScope|Any Milestone"
msgstr ""
msgid "BoardScope|Any assignee" msgid "BoardScope|Any assignee"
msgstr "" msgstr ""
...@@ -5321,12 +5330,30 @@ msgstr "" ...@@ -5321,12 +5330,30 @@ msgstr ""
msgid "BoardScope|Edit" msgid "BoardScope|Edit"
msgstr "" msgstr ""
msgid "BoardScope|Milestone"
msgstr ""
msgid "BoardScope|No matching results" msgid "BoardScope|No matching results"
msgstr "" msgstr ""
msgid "BoardScope|No milestone"
msgstr ""
msgid "BoardScope|Search milestones"
msgstr ""
msgid "BoardScope|Select assignee" msgid "BoardScope|Select assignee"
msgstr "" msgstr ""
msgid "BoardScope|Select milestone"
msgstr ""
msgid "BoardScope|Started"
msgstr ""
msgid "BoardScope|Upcoming"
msgstr ""
msgid "Boards" msgid "Boards"
msgstr "" msgstr ""
...@@ -22181,6 +22208,9 @@ msgstr "" ...@@ -22181,6 +22208,9 @@ msgstr ""
msgid "No Matching Results" msgid "No Matching Results"
msgstr "" msgstr ""
msgid "No Milestone"
msgstr ""
msgid "No Scopes" msgid "No Scopes"
msgstr "" msgstr ""
......
import { import {
isGid,
getIdFromGraphQLId, getIdFromGraphQLId,
convertToGraphQLId, convertToGraphQLId,
convertToGraphQLIds, convertToGraphQLIds,
...@@ -10,6 +11,16 @@ const mockType = 'Group'; ...@@ -10,6 +11,16 @@ const mockType = 'Group';
const mockId = 12; const mockId = 12;
const mockGid = `gid://gitlab/Group/12`; const mockGid = `gid://gitlab/Group/12`;
describe('isGid', () => {
it('returns true if passed id is gid', () => {
expect(isGid(mockGid)).toBe(true);
});
it('returns false if passed id is not gid', () => {
expect(isGid(mockId)).toBe(false);
});
});
describe('getIdFromGraphQLId', () => { describe('getIdFromGraphQLId', () => {
[ [
{ {
...@@ -67,6 +78,10 @@ describe('convertToGraphQLId', () => { ...@@ -67,6 +78,10 @@ describe('convertToGraphQLId', () => {
`('throws TypeError with "$message" if a param is missing', ({ type, id, message }) => { `('throws TypeError with "$message" if a param is missing', ({ type, id, message }) => {
expect(() => convertToGraphQLId(type, id)).toThrow(new TypeError(message)); expect(() => convertToGraphQLId(type, id)).toThrow(new TypeError(message));
}); });
it('returns id as is if it follows the gid format', () => {
expect(convertToGraphQLId(mockType, mockGid)).toStrictEqual(mockGid);
});
}); });
describe('convertToGraphQLIds', () => { describe('convertToGraphQLIds', () => {
......
...@@ -585,6 +585,19 @@ export const mockProjectMilestonesResponse = { ...@@ -585,6 +585,19 @@ export const mockProjectMilestonesResponse = {
}, },
}; };
export const mockGroupMilestonesResponse = {
data: {
workspace: {
id: 'gid://gitlab/Group/1',
attributes: {
nodes: [mockMilestone1, mockMilestone2],
},
__typename: 'MilestoneConnection',
},
__typename: 'Group',
},
};
export const noCurrentMilestoneResponse = { export const noCurrentMilestoneResponse = {
data: { data: {
workspace: { workspace: {
......
import { GlDropdown, GlSearchBoxByType, GlDropdownItem } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import DropdownWidget from '~/vue_shared/components/dropdown/dropdown_widget/dropdown_widget.vue';
describe('DropdownWidget component', () => {
let wrapper;
const findDropdown = () => wrapper.findComponent(GlDropdown);
const findDropdownItems = () => wrapper.findAll(GlDropdownItem);
const findSearch = () => wrapper.findComponent(GlSearchBoxByType);
const createComponent = ({ props = {} } = {}) => {
wrapper = shallowMount(DropdownWidget, {
propsData: {
...props,
options: [
{
id: '1',
title: 'Option 1',
},
{
id: '2',
title: 'Option 2',
},
],
},
stubs: {
GlDropdown,
},
});
// We need to mock out `showDropdown` which
// invokes `show` method of BDropdown used inside GlDropdown.
// Context: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/54895#note_524281679
jest.spyOn(wrapper.vm, 'showDropdown').mockImplementation();
};
beforeEach(() => {
createComponent();
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
it('passes default selectText prop to dropdown', () => {
expect(findDropdown().props('text')).toBe('Select');
});
describe('when dropdown is open', () => {
beforeEach(async () => {
findDropdown().vm.$emit('show');
await wrapper.vm.$nextTick();
});
it('emits search event when typing in search box', () => {
const searchTerm = 'searchTerm';
findSearch().vm.$emit('input', searchTerm);
expect(wrapper.emitted('set-search')).toEqual([[searchTerm]]);
});
it('renders one selectable item per passed option', async () => {
expect(findDropdownItems()).toHaveLength(2);
});
it('emits set-option event when clicking on an option', async () => {
wrapper
.findAll('[data-testid="unselected-option"]')
.at(1)
.vm.$emit('click', new Event('click'));
await wrapper.vm.$nextTick();
expect(wrapper.emitted('set-option')).toEqual([[wrapper.props().options[1]]]);
});
});
});
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