Commit 6ce2b93c authored by Ezekiel Kigbo's avatar Ezekiel Kigbo

Display a warning when maximum labels reached

Indicate how many labels are selected

When the user selects more than 10 labels
we should display how many labels are currently
selected and what the maximum is

Cleaned up tests

Fix alignment of label checkmarks

Display selected number of labels at all times

Disable selection of new labels
after max labels selected
parent d8f8ecb8
...@@ -524,6 +524,8 @@ img.emoji { ...@@ -524,6 +524,8 @@ img.emoji {
cursor: pointer; cursor: pointer;
} }
.cursor-not-allowed { cursor: not-allowed; }
// this needs to use "!important" due to some very specific styles // this needs to use "!important" due to some very specific styles
// around buttons // around buttons
.cursor-default { .cursor-default {
......
...@@ -5,8 +5,11 @@ import { ...@@ -5,8 +5,11 @@ import {
GlNewDropdown, GlNewDropdown,
GlNewDropdownItem, GlNewDropdownItem,
GlSearchBoxByType, GlSearchBoxByType,
GlIcon,
} from '@gitlab/ui'; } from '@gitlab/ui';
import { s__, sprintf } from '~/locale'; import { s__, sprintf } from '~/locale';
import createFlash from '~/flash';
import { removeFlash } from '../utils';
import { import {
TASKS_BY_TYPE_FILTERS, TASKS_BY_TYPE_FILTERS,
TASKS_BY_TYPE_SUBJECT_ISSUE, TASKS_BY_TYPE_SUBJECT_ISSUE,
...@@ -19,6 +22,7 @@ export default { ...@@ -19,6 +22,7 @@ export default {
components: { components: {
GlSegmentedControl, GlSegmentedControl,
GlDropdownDivider, GlDropdownDivider,
GlIcon,
GlNewDropdown, GlNewDropdown,
GlNewDropdownItem, GlNewDropdownItem,
GlSearchBoxByType, GlSearchBoxByType,
...@@ -27,8 +31,7 @@ export default { ...@@ -27,8 +31,7 @@ export default {
maxLabels: { maxLabels: {
type: Number, type: Number,
required: false, required: false,
// default: TASKS_BY_TYPE_MAX_LABELS, default: TASKS_BY_TYPE_MAX_LABELS,
default: 2,
}, },
labels: { labels: {
type: Array, type: Array,
...@@ -87,6 +90,9 @@ export default { ...@@ -87,6 +90,9 @@ export default {
maxLabelsSelected() { maxLabelsSelected() {
return this.selectedLabelIds.length >= this.maxLabels; return this.selectedLabelIds.length >= this.maxLabels;
}, },
hasMatchingLabels() {
return this.availableLabels.length;
},
}, },
methods: { methods: {
canUpdateLabelFilters(value) { canUpdateLabelFilters(value) {
...@@ -96,11 +102,20 @@ export default { ...@@ -96,11 +102,20 @@ export default {
isLabelSelected(id) { isLabelSelected(id) {
return this.selectedLabelIds.includes(id); return this.selectedLabelIds.includes(id);
}, },
isLabelDisabled(id) {
return this.maxLabelsSelected && !this.isLabelSelected(id);
},
handleLabelSelected(value) { handleLabelSelected(value) {
console.log('handleLabelSelected', value); removeFlash('notice');
// e.preventDefault();
if (this.canUpdateLabelFilters(value)) { if (this.canUpdateLabelFilters(value)) {
this.$emit('updateFilter', { filter: TASKS_BY_TYPE_FILTERS.LABEL, value }); this.$emit('updateFilter', { filter: TASKS_BY_TYPE_FILTERS.LABEL, value });
} else {
const { maxLabels } = this;
const message = sprintf(
s__('CycleAnalytics|Only %{maxLabels} labels can be selected at this time'),
{ maxLabels },
);
createFlash(message, 'notice');
} }
}, },
}, },
...@@ -138,27 +153,34 @@ export default { ...@@ -138,27 +153,34 @@ export default {
<div ref="labelsFilter" class="js-tasks-by-type-chart-filters-labels mb-3 px-3"> <div ref="labelsFilter" class="js-tasks-by-type-chart-filters-labels mb-3 px-3">
<p class="font-weight-bold text-left my-2"> <p class="font-weight-bold text-left my-2">
{{ s__('CycleAnalytics|Select labels') }} {{ s__('CycleAnalytics|Select labels') }}
<br />
<small>{{ selectedLabelLimitText }}</small>
</p> </p>
<small>{{ selectedLabelLimitText }}</small>
<gl-search-box-by-type <gl-search-box-by-type
v-model.trim="labelsSearchTerm" v-model.trim="labelsSearchTerm"
class="js-tasks-by-type-chart-filters-subject mb-2" class="js-tasks-by-type-chart-filters-subject mb-2"
/> />
<!-- TODO: make label dropdown item? -->
<gl-new-dropdown-item <gl-new-dropdown-item
v-for="label in availableLabels" v-for="label in availableLabels"
:key="label.id" :key="label.id"
:is-checked="isLabelSelected(label.id)" :disabled="isLabelDisabled(label.id)"
:class="{
'pl-4': !isLabelSelected(label.id),
'cursor-not-allowed': isLabelDisabled(label.id),
}"
@click="() => handleLabelSelected(label.id)" @click="() => handleLabelSelected(label.id)"
> >
<gl-icon
v-if="isLabelSelected(label.id)"
class="text-gray-700 mr-1 vertical-align-middle"
name="mobile-issue-close"
/>
<span <span
:style="{ 'background-color': label.color }" :style="{ 'background-color': label.color }"
class="d-inline-block dropdown-label-box" class="d-inline-block dropdown-label-box"
></span> ></span>
{{ label.name }} {{ label.name }}
</gl-new-dropdown-item> </gl-new-dropdown-item>
<div v-show="availableLabels.length < 1" class="text-secondary"> <div v-show="!hasMatchingLabels" class="text-secondary">
{{ __('No matching labels') }} {{ __('No matching labels') }}
</div> </div>
</div> </div>
......
...@@ -3,19 +3,13 @@ import Api from 'ee/api'; ...@@ -3,19 +3,13 @@ import Api from 'ee/api';
import { getDayDifference, getDateInPast } from '~/lib/utils/datetime_utility'; import { getDayDifference, getDateInPast } from '~/lib/utils/datetime_utility';
import { historyPushState } from '~/lib/utils/common_utils'; import { historyPushState } from '~/lib/utils/common_utils';
import { setUrlParams } from '~/lib/utils/url_utility'; import { setUrlParams } from '~/lib/utils/url_utility';
import createFlash, { hideFlash } from '~/flash'; import createFlash from '~/flash';
import { __, sprintf } from '~/locale'; import { __, sprintf } from '~/locale';
import httpStatus from '~/lib/utils/http_status'; import httpStatus from '~/lib/utils/http_status';
import * as types from './mutation_types'; import * as types from './mutation_types';
import { dateFormats } from '../../shared/constants'; import { dateFormats } from '../../shared/constants';
import { toYmd } from '../../shared/utils'; import { toYmd } from '../../shared/utils';
import { removeFlash } from '../utils';
const removeError = () => {
const flashEl = document.querySelector('.flash-alert');
if (flashEl) {
hideFlash(flashEl);
}
};
const handleErrorOrRethrow = ({ action, error }) => { const handleErrorOrRethrow = ({ action, error }) => {
if (error?.response?.status === httpStatus.FORBIDDEN) { if (error?.response?.status === httpStatus.FORBIDDEN) {
...@@ -167,7 +161,7 @@ export const receiveCycleAnalyticsDataError = ({ commit }, { response }) => { ...@@ -167,7 +161,7 @@ export const receiveCycleAnalyticsDataError = ({ commit }, { response }) => {
}; };
export const fetchCycleAnalyticsData = ({ dispatch }) => { export const fetchCycleAnalyticsData = ({ dispatch }) => {
removeError(); removeFlash();
dispatch('requestCycleAnalyticsData'); dispatch('requestCycleAnalyticsData');
return Promise.resolve() return Promise.resolve()
...@@ -182,12 +176,12 @@ export const fetchCycleAnalyticsData = ({ dispatch }) => { ...@@ -182,12 +176,12 @@ export const fetchCycleAnalyticsData = ({ dispatch }) => {
export const hideCustomStageForm = ({ commit }) => { export const hideCustomStageForm = ({ commit }) => {
commit(types.HIDE_CUSTOM_STAGE_FORM); commit(types.HIDE_CUSTOM_STAGE_FORM);
removeError(); removeFlash();
}; };
export const showCustomStageForm = ({ commit }) => { export const showCustomStageForm = ({ commit }) => {
commit(types.SHOW_CUSTOM_STAGE_FORM); commit(types.SHOW_CUSTOM_STAGE_FORM);
removeError(); removeFlash();
}; };
export const showEditCustomStageForm = ({ commit, dispatch }, selectedStage = {}) => { export const showEditCustomStageForm = ({ commit, dispatch }, selectedStage = {}) => {
...@@ -209,7 +203,7 @@ export const showEditCustomStageForm = ({ commit, dispatch }, selectedStage = {} ...@@ -209,7 +203,7 @@ export const showEditCustomStageForm = ({ commit, dispatch }, selectedStage = {}
endEventLabelId, endEventLabelId,
}); });
dispatch('setSelectedStage', selectedStage); dispatch('setSelectedStage', selectedStage);
removeError(); removeFlash();
}; };
export const requestSummaryData = ({ commit }) => commit(types.REQUEST_SUMMARY_DATA); export const requestSummaryData = ({ commit }) => commit(types.REQUEST_SUMMARY_DATA);
...@@ -346,7 +340,7 @@ export const fetchGroupStagesAndEvents = ({ state, dispatch, getters }) => { ...@@ -346,7 +340,7 @@ export const fetchGroupStagesAndEvents = ({ state, dispatch, getters }) => {
export const clearCustomStageFormErrors = ({ commit }) => { export const clearCustomStageFormErrors = ({ commit }) => {
commit(types.CLEAR_CUSTOM_STAGE_FORM_ERRORS); commit(types.CLEAR_CUSTOM_STAGE_FORM_ERRORS);
removeError(); removeFlash();
}; };
export const requestCreateCustomStage = ({ commit }) => commit(types.REQUEST_CREATE_CUSTOM_STAGE); export const requestCreateCustomStage = ({ commit }) => commit(types.REQUEST_CREATE_CUSTOM_STAGE);
...@@ -622,7 +616,6 @@ export const updateSelectedDurationChartStages = ({ state, commit }, stages) => ...@@ -622,7 +616,6 @@ export const updateSelectedDurationChartStages = ({ state, commit }, stages) =>
}; };
export const setTasksByTypeFilters = ({ dispatch, commit }, data) => { export const setTasksByTypeFilters = ({ dispatch, commit }, data) => {
console.log('setTasksByTypeFilters', data);
commit(types.SET_TASKS_BY_TYPE_FILTERS, data); commit(types.SET_TASKS_BY_TYPE_FILTERS, data);
dispatch('fetchTasksByTypeData'); dispatch('fetchTasksByTypeData');
}; };
......
...@@ -2,6 +2,7 @@ import { isNumber } from 'underscore'; ...@@ -2,6 +2,7 @@ import { isNumber } from 'underscore';
import dateFormat from 'dateformat'; import dateFormat from 'dateformat';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { convertToSnakeCase } from '~/lib/utils/text_utility'; import { convertToSnakeCase } from '~/lib/utils/text_utility';
import { hideFlash } from '~/flash';
import { import {
newDate, newDate,
dayAfter, dayAfter,
...@@ -17,6 +18,13 @@ import { toYmd } from '../shared/utils'; ...@@ -17,6 +18,13 @@ import { toYmd } from '../shared/utils';
const EVENT_TYPE_LABEL = 'label'; const EVENT_TYPE_LABEL = 'label';
export const removeFlash = (type = 'alert') => {
const flashEl = document.querySelector(`.flash-${type}`);
if (flashEl) {
hideFlash(flashEl);
}
};
export const isStartEvent = ev => Boolean(ev) && Boolean(ev.canBeStartEvent) && ev.canBeStartEvent; export const isStartEvent = ev => Boolean(ev) && Boolean(ev.canBeStartEvent) && ev.canBeStartEvent;
export const eventToOption = (obj = null) => { export const eventToOption = (obj = null) => {
......
...@@ -4,9 +4,9 @@ exports[`MergeRequestTable component template matches the snapshot 1`] = ` ...@@ -4,9 +4,9 @@ exports[`MergeRequestTable component template matches the snapshot 1`] = `
<table <table
aria-busy="false" aria-busy="false"
aria-colcount="7" aria-colcount="7"
aria-describedby="__BVID__57__caption_" aria-describedby="__BVID__59__caption_"
class="table b-table gl-table my-3 b-table-stacked-sm" class="table b-table gl-table my-3 b-table-stacked-sm"
id="__BVID__57" id="__BVID__59"
role="table" role="table"
> >
<!----> <!---->
......
...@@ -7,7 +7,7 @@ exports[`CustomStageForm Editing a custom stage isSavingCustomStage=true display ...@@ -7,7 +7,7 @@ exports[`CustomStageForm Editing a custom stage isSavingCustomStage=true display
`; `;
exports[`CustomStageForm Start event with events does not select events with canBeStartEvent=false for the start events dropdown 1`] = ` exports[`CustomStageForm Start event with events does not select events with canBeStartEvent=false for the start events dropdown 1`] = `
"<select name=\\"custom-stage-start-event\\" required=\\"required\\" aria-required=\\"true\\" class=\\"gl-form-select custom-select\\" id=\\"__BVID__255\\"> "<select name=\\"custom-stage-start-event\\" required=\\"required\\" aria-required=\\"true\\" class=\\"gl-form-select custom-select\\" id=\\"__BVID__257\\">
<option value=\\"\\">Select start event</option> <option value=\\"\\">Select start event</option>
<option value=\\"issue_created\\">Issue created</option> <option value=\\"issue_created\\">Issue created</option>
<option value=\\"issue_first_mentioned_in_commit\\">Issue first mentioned in a commit</option> <option value=\\"issue_first_mentioned_in_commit\\">Issue first mentioned in a commit</option>
...@@ -30,7 +30,7 @@ exports[`CustomStageForm Start event with events does not select events with can ...@@ -30,7 +30,7 @@ exports[`CustomStageForm Start event with events does not select events with can
`; `;
exports[`CustomStageForm Start event with events selects events with canBeStartEvent=true for the start events dropdown 1`] = ` exports[`CustomStageForm Start event with events selects events with canBeStartEvent=true for the start events dropdown 1`] = `
"<select name=\\"custom-stage-start-event\\" required=\\"required\\" aria-required=\\"true\\" class=\\"gl-form-select custom-select\\" id=\\"__BVID__215\\"> "<select name=\\"custom-stage-start-event\\" required=\\"required\\" aria-required=\\"true\\" class=\\"gl-form-select custom-select\\" id=\\"__BVID__217\\">
<option value=\\"\\">Select start event</option> <option value=\\"\\">Select start event</option>
<option value=\\"issue_created\\">Issue created</option> <option value=\\"issue_created\\">Issue created</option>
<option value=\\"issue_first_mentioned_in_commit\\">Issue first mentioned in a commit</option> <option value=\\"issue_first_mentioned_in_commit\\">Issue first mentioned in a commit</option>
......
...@@ -17,7 +17,7 @@ exports[`TasksByTypeChart with data available should render the loading chart 1` ...@@ -17,7 +17,7 @@ exports[`TasksByTypeChart with data available should render the loading chart 1`
<h3>Type of work</h3> <h3>Type of work</h3>
<div> <div>
<p>Showing data for group 'Gitlab Org' from Dec 11, 2019 to Jan 10, 2020</p> <p>Showing data for group 'Gitlab Org' from Dec 11, 2019 to Jan 10, 2020</p>
<tasks-by-type-filters-stub maxlabels=\\"2\\" labels=\\"[object Object],[object Object],[object Object]\\" selectedlabelids=\\"1,2,3\\" subjectfilter=\\"Issue\\"></tasks-by-type-filters-stub> <tasks-by-type-filters-stub maxlabels=\\"15\\" labels=\\"[object Object],[object Object],[object Object]\\" selectedlabelids=\\"1,2,3\\" subjectfilter=\\"Issue\\"></tasks-by-type-filters-stub>
<gl-stacked-column-chart-stub data=\\"0,1,2,5,2,3,2,4,1\\" option=\\"[object Object]\\" presentation=\\"stacked\\" groupby=\\"Group 1,Group 2,Group 3\\" xaxistype=\\"category\\" xaxistitle=\\"Date\\" yaxistitle=\\"Number of tasks\\" seriesnames=\\"Cool label,Normal label\\" legendaveragetext=\\"Avg\\" legendmaxtext=\\"Max\\" y-axis-type=\\"value\\"></gl-stacked-column-chart-stub> <gl-stacked-column-chart-stub data=\\"0,1,2,5,2,3,2,4,1\\" option=\\"[object Object]\\" presentation=\\"stacked\\" groupby=\\"Group 1,Group 2,Group 3\\" xaxistype=\\"category\\" xaxistitle=\\"Date\\" yaxistitle=\\"Number of tasks\\" seriesnames=\\"Cool label,Normal label\\" legendaveragetext=\\"Avg\\" legendmaxtext=\\"Max\\" y-axis-type=\\"value\\"></gl-stacked-column-chart-stub>
</div> </div>
</div> </div>
......
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`TasksByTypeFilters no data available should render the no data available message 1`] = `
"<div class=\\"js-tasks-by-type-chart-filters d-flex flex-row justify-content-between align-items-center\\">
<div class=\\"flex-column\\">
<h4>Tasks by type</h4>
<p>Showing Issues and 1 labels</p>
</div>
<div class=\\"flex-column\\">
<glnewdropdown-stub headertext=\\"\\" text=\\"\\" category=\\"tertiary\\" variant=\\"default\\" size=\\"medium\\" icon=\\"settings\\" aria-expanded=\\"false\\" aria-label=\\"CycleAnalytics|Display chart filters\\" right=\\"\\">
<div class=\\"js-tasks-by-type-chart-filters-subject mb-3 px-3\\">
<p class=\\"font-weight-bold text-left mb-2\\">Show</p>
<gl-segmented-control-stub checked=\\"Issue\\" options=\\"[object Object],[object Object]\\"></gl-segmented-control-stub>
</div>
<gl-dropdown-divider-stub></gl-dropdown-divider-stub>
<div class=\\"js-tasks-by-type-chart-filters-labels mb-3 px-3\\">
<p class=\\"font-weight-bold text-left my-2\\">
Select labels
<br> <small>1 selected (2 max)</small></p>
<gl-search-box-by-type-stub value=\\"\\" class=\\"js-tasks-by-type-chart-filters-subject mb-2\\"></gl-search-box-by-type-stub>
<glnewdropdownitem-stub avatarurl=\\"\\" iconcolor=\\"\\" iconname=\\"\\" iconrightname=\\"\\" ischecked=\\"true\\" secondarytext=\\"\\"><span class=\\"d-inline-block dropdown-label-box\\" style=\\"background-color: rgb(255, 0, 0);\\"></span>
roses
</glnewdropdownitem-stub>
<glnewdropdownitem-stub avatarurl=\\"\\" iconcolor=\\"\\" iconname=\\"\\" iconrightname=\\"\\" secondarytext=\\"\\"><span class=\\"d-inline-block dropdown-label-box\\" style=\\"background-color: rgb(255, 255, 255);\\"></span>
some space
</glnewdropdownitem-stub>
<glnewdropdownitem-stub avatarurl=\\"\\" iconcolor=\\"\\" iconname=\\"\\" iconrightname=\\"\\" secondarytext=\\"\\"><span class=\\"d-inline-block dropdown-label-box\\" style=\\"background-color: rgb(0, 0, 255);\\"></span>
violets
</glnewdropdownitem-stub>
<div class=\\"text-secondary\\" style=\\"display: none;\\">
No matching labels
</div>
</div>
</glnewdropdown-stub>
</div>
</div>"
`;
exports[`TasksByTypeFilters with data available labels with label dropdown open renders the group labels as dropdown items 1`] = `
"<glnewdropdown-stub headertext=\\"\\" text=\\"\\" category=\\"tertiary\\" variant=\\"default\\" size=\\"medium\\" icon=\\"settings\\" aria-expanded=\\"false\\" aria-label=\\"CycleAnalytics|Display chart filters\\" right=\\"\\">
<div class=\\"js-tasks-by-type-chart-filters-subject mb-3 px-3\\">
<p class=\\"font-weight-bold text-left mb-2\\">Show</p>
<gl-segmented-control-stub checked=\\"Issue\\" options=\\"[object Object],[object Object]\\"></gl-segmented-control-stub>
</div>
<gl-dropdown-divider-stub></gl-dropdown-divider-stub>
<div class=\\"js-tasks-by-type-chart-filters-labels mb-3 px-3\\">
<p class=\\"font-weight-bold text-left my-2\\">
Select labels
<br> <small>1 selected (2 max)</small></p>
<gl-search-box-by-type-stub value=\\"\\" class=\\"js-tasks-by-type-chart-filters-subject mb-2\\"></gl-search-box-by-type-stub>
<glnewdropdownitem-stub avatarurl=\\"\\" iconcolor=\\"\\" iconname=\\"\\" iconrightname=\\"\\" ischecked=\\"true\\" secondarytext=\\"\\"><span class=\\"d-inline-block dropdown-label-box\\" style=\\"background-color: rgb(255, 0, 0);\\"></span>
roses
</glnewdropdownitem-stub>
<glnewdropdownitem-stub avatarurl=\\"\\" iconcolor=\\"\\" iconname=\\"\\" iconrightname=\\"\\" secondarytext=\\"\\"><span class=\\"d-inline-block dropdown-label-box\\" style=\\"background-color: rgb(255, 255, 255);\\"></span>
some space
</glnewdropdownitem-stub>
<glnewdropdownitem-stub avatarurl=\\"\\" iconcolor=\\"\\" iconname=\\"\\" iconrightname=\\"\\" secondarytext=\\"\\"><span class=\\"d-inline-block dropdown-label-box\\" style=\\"background-color: rgb(0, 0, 255);\\"></span>
violets
</glnewdropdownitem-stub>
<div class=\\"text-secondary\\" style=\\"display: none;\\">
No matching labels
</div>
</div>
</glnewdropdown-stub>"
`;
exports[`TasksByTypeFilters with data available should render the filters 1`] = `
"<div class=\\"js-tasks-by-type-chart-filters d-flex flex-row justify-content-between align-items-center\\">
<div class=\\"flex-column\\">
<h4>Tasks by type</h4>
<p>Showing Issues and 1 labels</p>
</div>
<div class=\\"flex-column\\">
<glnewdropdown-stub headertext=\\"\\" text=\\"\\" category=\\"tertiary\\" variant=\\"default\\" size=\\"medium\\" icon=\\"settings\\" aria-expanded=\\"false\\" aria-label=\\"CycleAnalytics|Display chart filters\\" right=\\"\\">
<div class=\\"js-tasks-by-type-chart-filters-subject mb-3 px-3\\">
<p class=\\"font-weight-bold text-left mb-2\\">Show</p>
<gl-segmented-control-stub checked=\\"Issue\\" options=\\"[object Object],[object Object]\\"></gl-segmented-control-stub>
</div>
<gl-dropdown-divider-stub></gl-dropdown-divider-stub>
<div class=\\"js-tasks-by-type-chart-filters-labels mb-3 px-3\\">
<p class=\\"font-weight-bold text-left my-2\\">
Select labels
<br> <small>1 selected (2 max)</small></p>
<gl-search-box-by-type-stub value=\\"\\" class=\\"js-tasks-by-type-chart-filters-subject mb-2\\"></gl-search-box-by-type-stub>
<glnewdropdownitem-stub avatarurl=\\"\\" iconcolor=\\"\\" iconname=\\"\\" iconrightname=\\"\\" ischecked=\\"true\\" secondarytext=\\"\\"><span class=\\"d-inline-block dropdown-label-box\\" style=\\"background-color: rgb(255, 0, 0);\\"></span>
roses
</glnewdropdownitem-stub>
<glnewdropdownitem-stub avatarurl=\\"\\" iconcolor=\\"\\" iconname=\\"\\" iconrightname=\\"\\" secondarytext=\\"\\"><span class=\\"d-inline-block dropdown-label-box\\" style=\\"background-color: rgb(255, 255, 255);\\"></span>
some space
</glnewdropdownitem-stub>
<glnewdropdownitem-stub avatarurl=\\"\\" iconcolor=\\"\\" iconname=\\"\\" iconrightname=\\"\\" secondarytext=\\"\\"><span class=\\"d-inline-block dropdown-label-box\\" style=\\"background-color: rgb(0, 0, 255);\\"></span>
violets
</glnewdropdownitem-stub>
<div class=\\"text-secondary\\" style=\\"display: none;\\">
No matching labels
</div>
</div>
</glnewdropdown-stub>
</div>
</div>"
`;
exports[`TasksByTypeFilters with data available subject has subject filters 1`] = `"<gl-segmented-control-stub checked=\\"Issue\\" options=\\"[object Object],[object Object]\\"></gl-segmented-control-stub>"`;
import { shallowMount, mount } from '@vue/test-utils'; import { shallowMount, mount } from '@vue/test-utils';
import { GlNewDropdown, GlNewDropdownItem, GlSegmentedControl } from '@gitlab/ui'; import { GlNewDropdownItem, GlSegmentedControl } from '@gitlab/ui';
import TasksByTypeFilters from 'ee/analytics/cycle_analytics/components/tasks_by_type_filters.vue'; import TasksByTypeFilters from 'ee/analytics/cycle_analytics/components/tasks_by_type_filters.vue';
import { import {
TASKS_BY_TYPE_SUBJECT_ISSUE, TASKS_BY_TYPE_SUBJECT_ISSUE,
...@@ -13,10 +13,12 @@ const selectedLabelIds = [groupLabels[0].id]; ...@@ -13,10 +13,12 @@ const selectedLabelIds = [groupLabels[0].id];
const findSubjectFilters = ctx => const findSubjectFilters = ctx =>
ctx.find('.js-tasks-by-type-chart-filters-subject').find(GlSegmentedControl); ctx.find('.js-tasks-by-type-chart-filters-subject').find(GlSegmentedControl);
const findSelectedSubjectFilters = ctx => findSubjectFilters(ctx).attributes('checked'); const findSelectedSubjectFilters = ctx => findSubjectFilters(ctx).attributes('checked');
const findDropdown = ctx => ctx.find(GlNewDropdown);
const findDropdownLabels = ctx => const findDropdownLabels = ctx =>
ctx.find('.js-tasks-by-type-chart-filters-labels').findAll(GlNewDropdownItem); ctx.find('.js-tasks-by-type-chart-filters-labels').findAll(GlNewDropdownItem);
const shouldFlashAMessage = (msg = '') =>
expect(document.querySelector('.flash-container .flash-text').innerText.trim()).toBe(msg);
const selectLabelAtIndex = (ctx, index) => { const selectLabelAtIndex = (ctx, index) => {
findDropdownLabels(ctx) findDropdownLabels(ctx)
.at(index) .at(index)
...@@ -43,104 +45,100 @@ function createComponent({ props = {}, shallow = true }) { ...@@ -43,104 +45,100 @@ function createComponent({ props = {}, shallow = true }) {
describe('TasksByTypeFilters', () => { describe('TasksByTypeFilters', () => {
let wrapper = null; let wrapper = null;
describe('with data available', () => { beforeEach(() => {
wrapper = createComponent({});
});
afterEach(() => {
wrapper.destroy();
});
describe('labels', () => {
beforeEach(() => { beforeEach(() => {
wrapper = createComponent({}); wrapper = createComponent({});
}); });
afterEach(() => { it('emits the `updateFilter` event when a subject label is clicked', () => {
wrapper.destroy(); expect(wrapper.emitted().updateFilter).toBeUndefined();
}); return selectLabelAtIndex(wrapper, 0).then(() => {
expect(wrapper.emitted().updateFilter).toBeDefined();
it('should render the filters', () => { expect(wrapper.emitted().updateFilter[0]).toEqual([
expect(wrapper.html()).toMatchSnapshot(); { filter: TASKS_BY_TYPE_FILTERS.LABEL, value: groupLabels[0].id },
}); ]);
describe('labels', () => {
it(`should have ${selectedLabelIds.length} selected`, () => {
expect(wrapper.text()).toContain('1 selected (15 max)');
}); });
});
describe('with label dropdown open', () => { describe('with the warningMessageThreshold label threshold reached', () => {
beforeEach(() => { beforeEach(() => {
wrapper = createComponent({}); setFixtures('<div class="flash-container"></div>');
}); wrapper = createComponent({
props: {
it('renders the group labels as dropdown items', () => { maxLabels: 5,
expect(findDropdown(wrapper).html()).toMatchSnapshot(); selectedLabelIds: [groupLabels[0].id, groupLabels[1].id],
warningMessageThreshold: 2,
},
}); });
it('emits the `updateFilter` event when a subject label is clicked', () => { return selectLabelAtIndex(wrapper, 2);
expect(wrapper.emitted().updateFilter).toBeUndefined(); });
return selectLabelAtIndex(wrapper, 0).then(() => {
expect(wrapper.emitted().updateFilter).toBeDefined();
expect(wrapper.emitted().updateFilter[0]).toEqual([
{ filter: TASKS_BY_TYPE_FILTERS.LABEL, value: groupLabels[0].id },
]);
});
});
describe('with maximum labels selected', () => { it('should indicate how many labels are selected', () => {
beforeEach(() => { expect(wrapper.text()).toContain('2 selected (5 max)');
wrapper = createComponent({
props: {
maxLabels: 2,
selectedLabelIds: [groupLabels[0].id, groupLabels[1].id],
},
});
});
it('should not allow selecting another label', () => {
expect(wrapper.emitted().updateFilter).toBeUndefined();
return selectLabelAtIndex(wrapper, 2).then(() => {
expect(wrapper.emitted().updateFilter).toBeUndefined();
});
});
});
}); });
}); });
describe('subject', () => { describe('with maximum labels selected', () => {
it('has subject filters', () => { beforeEach(() => {
expect(findSubjectFilters(wrapper).html()).toMatchSnapshot(); setFixtures('<div class="flash-container"></div>');
wrapper = createComponent({
props: {
maxLabels: 2,
selectedLabelIds: [groupLabels[0].id, groupLabels[1].id],
warningMessageThreshold: 1,
},
});
return selectLabelAtIndex(wrapper, 2);
}); });
it('has the issue subject set by default', () => { it('should indicate how many labels are selected', () => {
expect(findSelectedSubjectFilters(wrapper)).toBe(TASKS_BY_TYPE_SUBJECT_ISSUE); expect(wrapper.text()).toContain('2 selected (2 max)');
}); });
it('emits the `updateFilter` event when a subject filter is clicked', () => { it('should not allow selecting another label', () => {
wrapper = createComponent({ shallow: false });
expect(wrapper.emitted().updateFilter).toBeUndefined(); expect(wrapper.emitted().updateFilter).toBeUndefined();
});
findSubjectFilters(wrapper) it('should display a message', () => {
.findAll('label:not(.active)') shouldFlashAMessage('Only 2 labels can be selected at this time');
.at(0)
.trigger('click');
return wrapper.vm.$nextTick(() => {
expect(wrapper.emitted().updateFilter).toBeDefined();
expect(wrapper.emitted().updateFilter[0]).toEqual([
{ filter: TASKS_BY_TYPE_FILTERS.SUBJECT, value: TASKS_BY_TYPE_SUBJECT_MERGE_REQUEST },
]);
});
}); });
}); });
}); });
describe('no data available', () => { describe('subject', () => {
beforeEach(() => { it('has the issue subject set by default', () => {
wrapper = createComponent({}); expect(findSelectedSubjectFilters(wrapper)).toBe(TASKS_BY_TYPE_SUBJECT_ISSUE);
});
afterEach(() => {
wrapper.destroy();
}); });
it('should render the no data available message', () => { it('emits the `updateFilter` event when a subject filter is clicked', () => {
expect(wrapper.html()).toMatchSnapshot(); wrapper = createComponent({ shallow: false });
expect(wrapper.emitted().updateFilter).toBeUndefined();
findSubjectFilters(wrapper)
.findAll('label:not(.active)')
.at(0)
.trigger('click');
return wrapper.vm.$nextTick(() => {
expect(wrapper.emitted().updateFilter).toBeDefined();
expect(wrapper.emitted().updateFilter[0]).toEqual([
{
filter: TASKS_BY_TYPE_FILTERS.SUBJECT,
value: TASKS_BY_TYPE_SUBJECT_MERGE_REQUEST,
},
]);
});
}); });
}); });
}); });
...@@ -303,17 +303,17 @@ describe('Cycle analytics mutations', () => { ...@@ -303,17 +303,17 @@ describe('Cycle analytics mutations', () => {
expect(state.tasksByType).toEqual({ subject: 'cool-subject' }); expect(state.tasksByType).toEqual({ subject: 'cool-subject' });
}); });
it('will toggle the specified label id in the tasksByType.labelIds state key', () => { it('will toggle the specified label id in the tasksByType.selectedLabelIds state key', () => {
state = { state = {
tasksByType: { labelIds: [10, 20, 30] }, tasksByType: { selectedLabelIds: [10, 20, 30] },
}; };
const labelFilter = { filter: TASKS_BY_TYPE_FILTERS.LABEL, value: 20 }; const labelFilter = { filter: TASKS_BY_TYPE_FILTERS.LABEL, value: 20 };
mutations[types.SET_TASKS_BY_TYPE_FILTERS](state, labelFilter); mutations[types.SET_TASKS_BY_TYPE_FILTERS](state, labelFilter);
expect(state.tasksByType).toEqual({ labelIds: [10, 30] }); expect(state.tasksByType).toEqual({ selectedLabelIds: [10, 30] });
mutations[types.SET_TASKS_BY_TYPE_FILTERS](state, labelFilter); mutations[types.SET_TASKS_BY_TYPE_FILTERS](state, labelFilter);
expect(state.tasksByType).toEqual({ labelIds: [10, 30, 20] }); expect(state.tasksByType).toEqual({ selectedLabelIds: [10, 30, 20] });
}); });
}); });
......
...@@ -6134,6 +6134,9 @@ msgstr "" ...@@ -6134,6 +6134,9 @@ msgstr ""
msgid "CycleAnalytics|Number of tasks" msgid "CycleAnalytics|Number of tasks"
msgstr "" msgstr ""
msgid "CycleAnalytics|Only %{maxLabels} labels can be selected at this time"
msgstr ""
msgid "CycleAnalytics|Project selected" msgid "CycleAnalytics|Project selected"
msgid_plural "CycleAnalytics|%d projects selected" msgid_plural "CycleAnalytics|%d projects selected"
msgstr[0] "" msgstr[0] ""
...@@ -13173,6 +13176,9 @@ msgstr "" ...@@ -13173,6 +13176,9 @@ msgstr ""
msgid "No licenses found." msgid "No licenses found."
msgstr "" msgstr ""
msgid "No matching labels"
msgstr ""
msgid "No matching results" msgid "No matching results"
msgstr "" msgstr ""
......
...@@ -821,25 +821,6 @@ ...@@ -821,25 +821,6 @@
vue-loader "^15.4.2" vue-loader "^15.4.2"
vue-runtime-helpers "^1.1.2" vue-runtime-helpers "^1.1.2"
"@gitlab/ui@https://gitlab.com/gitlab-org/gitlab-ui":
version "9.20.0"
resolved "https://gitlab.com/gitlab-org/gitlab-ui#53509c2a2fa8b1410941212a0aa8a2b05a7558e9"
dependencies:
"@babel/standalone" "^7.0.0"
"@gitlab/vue-toasted" "^1.3.0"
bootstrap-vue "2.1.0"
copy-to-clipboard "^3.0.8"
echarts "^4.2.1"
highlight.js "^9.13.1"
js-beautify "^1.8.8"
lodash "^4.17.14"
portal-vue "^2.1.6"
resize-observer-polyfill "^1.5.1"
url-search-params-polyfill "^5.0.0"
vue "^2.6.10"
vue-loader "^15.4.2"
vue-runtime-helpers "^1.1.2"
"@gitlab/visual-review-tools@1.5.1": "@gitlab/visual-review-tools@1.5.1":
version "1.5.1" version "1.5.1"
resolved "https://registry.yarnpkg.com/@gitlab/visual-review-tools/-/visual-review-tools-1.5.1.tgz#2552927cd7a376f1f06ef3293a69fe2ffcdddb52" resolved "https://registry.yarnpkg.com/@gitlab/visual-review-tools/-/visual-review-tools-1.5.1.tgz#2552927cd7a376f1f06ef3293a69fe2ffcdddb52"
......
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