Commit d8f8ecb8 authored by Ezekiel Kigbo's avatar Ezekiel Kigbo

Restrict selecting more labels than max

Ensure that the user can only select 15
labels at most. This is required to fix
https://gitlab.com/gitlab-org/gitlab/issues/38260

Fixed broken specs
parent 4a0a2b10
<script> <script>
import $ from 'jquery'; import {
import _ from 'underscore'; GlDropdownDivider,
import { GlButton, GlDropdownDivider, GlSegmentedControl } from '@gitlab/ui'; GlSegmentedControl,
GlNewDropdown,
GlNewDropdownItem,
GlSearchBoxByType,
} from '@gitlab/ui';
import { s__, sprintf } from '~/locale'; import { s__, sprintf } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue';
import { import {
TASKS_BY_TYPE_FILTERS, TASKS_BY_TYPE_FILTERS,
TASKS_BY_TYPE_SUBJECT_ISSUE, TASKS_BY_TYPE_SUBJECT_ISSUE,
...@@ -14,12 +17,19 @@ import { ...@@ -14,12 +17,19 @@ import {
export default { export default {
name: 'TasksByTypeFilters', name: 'TasksByTypeFilters',
components: { components: {
GlButton,
GlDropdownDivider,
GlSegmentedControl, GlSegmentedControl,
Icon, GlDropdownDivider,
GlNewDropdown,
GlNewDropdownItem,
GlSearchBoxByType,
}, },
props: { props: {
maxLabels: {
type: Number,
required: false,
// default: TASKS_BY_TYPE_MAX_LABELS,
default: 2,
},
labels: { labels: {
type: Array, type: Array,
required: true, required: true,
...@@ -38,6 +48,7 @@ export default { ...@@ -38,6 +48,7 @@ export default {
const { subjectFilter: selectedSubjectFilter } = this; const { subjectFilter: selectedSubjectFilter } = this;
return { return {
selectedSubjectFilter, selectedSubjectFilter,
labelsSearchTerm: '',
}; };
}, },
computed: { computed: {
...@@ -61,51 +72,36 @@ export default { ...@@ -61,51 +72,36 @@ export default {
}, },
); );
}, },
availableLabels() {
return this.labels.filter(({ name }) =>
name.toLowerCase().includes(this.labelsSearchTerm.toLowerCase()),
);
},
selectedLabelLimitText() { selectedLabelLimitText() {
const { selectedLabelIds } = this; const { selectedLabelIds, maxLabels } = this;
return sprintf(s__('CycleAnalytics|%{selectedLabelsCount} selected (%{maxLabels} max)'), { return sprintf(s__('CycleAnalytics|%{selectedLabelsCount} selected (%{maxLabels} max)'), {
selectedLabelsCount: selectedLabelIds.length, selectedLabelsCount: selectedLabelIds.length,
maxLabels: TASKS_BY_TYPE_MAX_LABELS, maxLabels,
}); });
}, },
}, maxLabelsSelected() {
mounted() { return this.selectedLabelIds.length >= this.maxLabels;
$(this.$refs.labelsDropdown).glDropdown({ },
selectable: true,
multiSelect: true,
filterable: true,
search: {
fields: ['title'],
},
clicked: this.onClick.bind(this),
data: this.formatData.bind(this),
renderRow: group => this.rowTemplate(group),
text: label => label.title,
});
}, },
methods: { methods: {
onClick({ e, selectedObj }) { canUpdateLabelFilters(value) {
e.preventDefault(); // we can always remove a filter
const { id: value } = selectedObj; return this.selectedLabelIds.includes(value) || !this.maxLabelsSelected;
this.$emit('updateFilter', { filter: TASKS_BY_TYPE_FILTERS.LABEL, value });
}, },
formatData(term, callback) { isLabelSelected(id) {
callback(this.labels); return this.selectedLabelIds.includes(id);
}, },
rowTemplate(label) { handleLabelSelected(value) {
const isActiveClass = console.log('handleLabelSelected', value);
this.selectedLabelIds.length && this.selectedLabelIds.includes(label.id) ? 'is-active' : ''; // e.preventDefault();
return ` if (this.canUpdateLabelFilters(value)) {
<li> this.$emit('updateFilter', { filter: TASKS_BY_TYPE_FILTERS.LABEL, value });
<a href='#' class='dropdown-menu-link ${isActiveClass}'> }
<span style="background-color: ${
label.color
};" class="d-inline-block dropdown-label-box">
</span>
${_.escape(label.name)}
</a>
</li>
`;
}, },
}, },
TASKS_BY_TYPE_FILTERS, TASKS_BY_TYPE_FILTERS,
...@@ -121,44 +117,52 @@ export default { ...@@ -121,44 +117,52 @@ export default {
<p>{{ selectedFiltersText }}</p> <p>{{ selectedFiltersText }}</p>
</div> </div>
<div class="flex-column"> <div class="flex-column">
<div ref="labelsDropdown" class="dropdown dropdown-labels"> <gl-new-dropdown
<gl-button icon="settings"
class="shadow-none bg-white btn-svg" aria-expanded="false"
type="button" :aria-label="__('CycleAnalytics|Display chart filters')"
data-toggle="dropdown" right
aria-expanded="false" >
:aria-label="__('CycleAnalytics|Display chart filters')" <div ref="subjectFilter" class="js-tasks-by-type-chart-filters-subject mb-3 px-3">
> <p class="font-weight-bold text-left mb-2">{{ s__('CycleAnalytics|Show') }}</p>
<icon :size="16" name="settings" /> <gl-segmented-control
<icon :size="16" name="chevron-down" /> v-model="selectedSubjectFilter"
</gl-button> :options="subjectFilterOptions"
<div class="dropdown-menu dropdown-menu-selectable dropdown-menu-right"> @input="
<div class="js-tasks-by-type-chart-filters-subject mb-3 px-3"> value =>
<p class="font-weight-bold text-left mb-2">{{ s__('CycleAnalytics|Show') }}</p> $emit('updateFilter', { filter: $options.TASKS_BY_TYPE_FILTERS.SUBJECT, value })
<gl-segmented-control "
v-model="selectedSubjectFilter" />
:options="subjectFilterOptions" </div>
@input=" <gl-dropdown-divider />
value => <div ref="labelsFilter" class="js-tasks-by-type-chart-filters-labels mb-3 px-3">
$emit('updateFilter', { filter: $options.TASKS_BY_TYPE_FILTERS.SUBJECT, value }) <p class="font-weight-bold text-left my-2">
" {{ s__('CycleAnalytics|Select labels') }}
/> <br />
</div> <small>{{ selectedLabelLimitText }}</small>
<gl-dropdown-divider /> </p>
<div class="js-tasks-by-type-chart-filters-labels mb-3 px-3"> <gl-search-box-by-type
<p class="font-weight-bold text-left my-2"> v-model.trim="labelsSearchTerm"
{{ s__('CycleAnalytics|Select labels') }} class="js-tasks-by-type-chart-filters-subject mb-2"
<br /> />
<small>{{ selectedLabelLimitText }}</small> <!-- TODO: make label dropdown item? -->
</p> <gl-new-dropdown-item
<div class="dropdown-input px-0"> v-for="label in availableLabels"
<input class="dropdown-input-field" type="search" /> :key="label.id"
<icon name="search" class="dropdown-input-search" data-hidden="true" /> :is-checked="isLabelSelected(label.id)"
</div> @click="() => handleLabelSelected(label.id)"
<div class="dropdown-content px-0"></div> >
<span
:style="{ 'background-color': label.color }"
class="d-inline-block dropdown-label-box"
></span>
{{ label.name }}
</gl-new-dropdown-item>
<div v-show="availableLabels.length < 1" class="text-secondary">
{{ __('No matching labels') }}
</div> </div>
</div> </div>
</div> </gl-new-dropdown>
</div> </div>
</div> </div>
</template> </template>
...@@ -43,7 +43,7 @@ export const TASKS_BY_TYPE_SUBJECT_FILTER_OPTIONS = { ...@@ -43,7 +43,7 @@ export const TASKS_BY_TYPE_SUBJECT_FILTER_OPTIONS = {
export const TASKS_BY_TYPE_FILTERS = { export const TASKS_BY_TYPE_FILTERS = {
SUBJECT: 'SUBJECT', SUBJECT: 'SUBJECT',
LABELS: 'LABELS', LABEL: 'LABEL',
}; };
export const STAGE_ACTIONS = { export const STAGE_ACTIONS = {
......
...@@ -622,6 +622,7 @@ export const updateSelectedDurationChartStages = ({ state, commit }, stages) => ...@@ -622,6 +622,7 @@ 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');
}; };
......
...@@ -226,15 +226,15 @@ export default { ...@@ -226,15 +226,15 @@ export default {
}, },
[types.SET_TASKS_BY_TYPE_FILTERS](state, { filter, value }) { [types.SET_TASKS_BY_TYPE_FILTERS](state, { filter, value }) {
const { const {
tasksByType: { labelIds, ...tasksByTypeRest }, tasksByType: { selectedLabelIds, ...tasksByTypeRest },
} = state; } = state;
let updatedFilter = {}; let updatedFilter = {};
switch (filter) { switch (filter) {
case TASKS_BY_TYPE_FILTERS.LABEL: case TASKS_BY_TYPE_FILTERS.LABEL:
updatedFilter = { updatedFilter = {
labelIds: labelIds.includes(value) selectedLabelIds: selectedLabelIds.includes(value)
? labelIds.filter(v => v !== value) ? selectedLabelIds.filter(v => v !== value)
: [...labelIds, value], : [...selectedLabelIds, value],
}; };
break; break;
case TASKS_BY_TYPE_FILTERS.SUBJECT: case TASKS_BY_TYPE_FILTERS.SUBJECT:
...@@ -243,7 +243,7 @@ export default { ...@@ -243,7 +243,7 @@ export default {
default: default:
break; break;
} }
state.tasksByType = { ...tasksByTypeRest, labelIds, ...updatedFilter }; state.tasksByType = { ...tasksByTypeRest, selectedLabelIds, ...updatedFilter };
}, },
[types.INITIALIZE_CYCLE_ANALYTICS]( [types.INITIALIZE_CYCLE_ANALYTICS](
state, state,
......
...@@ -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__59__caption_" aria-describedby="__BVID__57__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__59" id="__BVID__57"
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__257\\"> "<select name=\\"custom-stage-start-event\\" required=\\"required\\" aria-required=\\"true\\" class=\\"gl-form-select custom-select\\" id=\\"__BVID__255\\">
<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__217\\"> "<select name=\\"custom-stage-start-event\\" required=\\"required\\" aria-required=\\"true\\" class=\\"gl-form-select custom-select\\" id=\\"__BVID__215\\">
<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>
......
...@@ -11,62 +11,13 @@ exports[`TasksByTypeChart no data available should render the no data available ...@@ -11,62 +11,13 @@ exports[`TasksByTypeChart no data available should render the no data available
</div>" </div>"
`; `;
exports[`TasksByTypeChart with data available filters labels has label filters 1`] = `
"<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>3 selected (15 max)</small></p>
<div class=\\"dropdown-input px-0\\"><input type=\\"search\\" class=\\"dropdown-input-field\\"> <svg aria-hidden=\\"true\\" class=\\"dropdown-input-search s16 ic-search\\" data-hidden=\\"true\\">
<use xlink:href=\\"#search\\"></use>
</svg></div>
<div class=\\"dropdown-content px-0\\"></div>
</div>"
`;
exports[`TasksByTypeChart with data available filters labels with label dropdown open renders the group labels as dropdown items 1`] = `
"<div class=\\"dropdown-content px-0\\">
<ul>
<li>
<a href=\\"#\\" class=\\"dropdown-menu-link \\">
<span style=\\"background-color: #FF0000;\\" class=\\"d-inline-block dropdown-label-box\\">
</span>
roses
</a>
</li>
<li>
<a href=\\"#\\" class=\\"dropdown-menu-link \\">
<span style=\\"background-color: #FFFFFF;\\" class=\\"d-inline-block dropdown-label-box\\">
</span>
some space
</a>
</li>
<li>
<a href=\\"#\\" class=\\"dropdown-menu-link \\">
<span style=\\"background-color: #0000FF;\\" class=\\"d-inline-block dropdown-label-box\\">
</span>
violets
</a>
</li>
</ul>
</div>"
`;
exports[`TasksByTypeChart with data available filters subject has subject filters 1`] = `
"<div class=\\"js-tasks-by-type-chart-filters-subject mb-3 px-3\\">
<p class=\\"font-weight-bold text-left mb-2\\">Show</p>
<div role=\\"radiogroup\\" tabindex=\\"-1\\" class=\\"gl-segmented-control btn-group-toggle btn-group\\" id=\\"__BVID__74\\"><label class=\\"btn btn-gl-segmented-button active\\"><input id=\\"__BVID__74__BV_option_0_\\" type=\\"radio\\" name=\\"__BVID__74\\" autocomplete=\\"off\\" class=\\"\\" value=\\"Issue\\"><span>Issues</span></label><label class=\\"btn btn-gl-segmented-button\\"><input id=\\"__BVID__74__BV_option_1_\\" type=\\"radio\\" name=\\"__BVID__74\\" autocomplete=\\"off\\" class=\\"\\" value=\\"MergeRequest\\"><span>Merge Requests</span></label></div>
</div>"
`;
exports[`TasksByTypeChart with data available should render the loading chart 1`] = ` exports[`TasksByTypeChart with data available should render the loading chart 1`] = `
"<div class=\\"row\\"> "<div class=\\"row\\">
<div class=\\"col-12\\"> <div class=\\"col-12\\">
<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 labels=\\"[object Object],[object Object],[object Object]\\" selectedlabelids=\\"1,2,3\\" subjectfilter=\\"Issue\\"></tasks-by-type-filters-stub> <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>
<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 { mount, shallowMount } from '@vue/test-utils'; import { mount, shallowMount } from '@vue/test-utils';
import $ from 'jquery';
import 'bootstrap';
import '~/gl_dropdown';
import TasksByTypeChart from 'ee/analytics/cycle_analytics/components/tasks_by_type_chart.vue'; import TasksByTypeChart from 'ee/analytics/cycle_analytics/components/tasks_by_type_chart.vue';
import { import { TASKS_BY_TYPE_SUBJECT_ISSUE } from 'ee/analytics/cycle_analytics/constants';
TASKS_BY_TYPE_SUBJECT_ISSUE,
TASKS_BY_TYPE_SUBJECT_MERGE_REQUEST,
TASKS_BY_TYPE_FILTERS,
} from 'ee/analytics/cycle_analytics/constants';
import { groupLabels } from '../mock_data'; import { groupLabels } from '../mock_data';
const seriesNames = ['Cool label', 'Normal label']; const seriesNames = ['Cool label', 'Normal label'];
...@@ -63,91 +56,6 @@ describe('TasksByTypeChart', () => { ...@@ -63,91 +56,6 @@ describe('TasksByTypeChart', () => {
it('should render the loading chart', () => { it('should render the loading chart', () => {
expect(wrapper.html()).toMatchSnapshot(); expect(wrapper.html()).toMatchSnapshot();
}); });
describe('filters', () => {
const findSubjectFilters = ctx => ctx.find('.js-tasks-by-type-chart-filters-subject');
const findSelectedSubjectFilters = ctx =>
ctx.find('.js-tasks-by-type-chart-filters-subject .active');
const findLabelFilters = ctx => ctx.find('.js-tasks-by-type-chart-filters-labels');
const findDropdown = ctx => ctx.find('.dropdown');
const findDropdownContent = ctx => ctx.find('.dropdown-content');
const openDropdown = ctx => {
$(findDropdown(ctx).element)
.parent()
.trigger('shown.bs.dropdown');
};
beforeEach(() => {
wrapper = createComponent({
shallow: false,
stubs: {
'tasks-by-type-filters': false,
},
});
});
describe('labels', () => {
it('has label filters', () => {
expect(findLabelFilters(wrapper).html()).toMatchSnapshot();
});
describe('with label dropdown open', () => {
beforeEach(() => {
openDropdown(wrapper);
return wrapper.vm.$nextTick();
});
it('renders the group labels as dropdown items', () => {
expect(findDropdownContent(wrapper).html()).toMatchSnapshot();
});
it('emits the `updateFilter` event when a subject label is clicked', done => {
expect(wrapper.emitted().updateFilter).toBeUndefined();
findLabelFilters(wrapper)
.findAll('.dropdown-menu-link')
.at(0)
.trigger('click');
wrapper.vm.$nextTick(() => {
expect(wrapper.emitted().updateFilter).toBeDefined();
expect(wrapper.emitted().updateFilter[0]).toEqual([
{ filter: TASKS_BY_TYPE_FILTERS.LABEL, value: groupLabels[0].id },
]);
done();
});
});
});
});
describe('subject', () => {
it('has subject filters', () => {
expect(findSubjectFilters(wrapper).html()).toMatchSnapshot();
});
it('has the issue subject set by default', () => {
expect(findSelectedSubjectFilters(wrapper).text()).toBe('Issues');
});
it('emits the `updateFilter` event when a subject filter is clicked', done => {
expect(wrapper.emitted().updateFilter).toBeUndefined();
findSubjectFilters(wrapper)
.findAll('label:not(.active)')
.at(0)
.trigger('click');
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 },
]);
done();
});
});
});
});
}); });
describe('no data available', () => { describe('no data available', () => {
......
import { shallowMount, mount } from '@vue/test-utils';
import { GlNewDropdown, GlNewDropdownItem, GlSegmentedControl } from '@gitlab/ui';
import TasksByTypeFilters from 'ee/analytics/cycle_analytics/components/tasks_by_type_filters.vue';
import {
TASKS_BY_TYPE_SUBJECT_ISSUE,
TASKS_BY_TYPE_SUBJECT_MERGE_REQUEST,
TASKS_BY_TYPE_FILTERS,
} from 'ee/analytics/cycle_analytics/constants';
import { groupLabels } from '../mock_data';
const selectedLabelIds = [groupLabels[0].id];
const findSubjectFilters = ctx =>
ctx.find('.js-tasks-by-type-chart-filters-subject').find(GlSegmentedControl);
const findSelectedSubjectFilters = ctx => findSubjectFilters(ctx).attributes('checked');
const findDropdown = ctx => ctx.find(GlNewDropdown);
const findDropdownLabels = ctx =>
ctx.find('.js-tasks-by-type-chart-filters-labels').findAll(GlNewDropdownItem);
const selectLabelAtIndex = (ctx, index) => {
findDropdownLabels(ctx)
.at(index)
.vm.$emit('click');
return ctx.vm.$nextTick();
};
function createComponent({ props = {}, shallow = true }) {
const fn = shallow ? shallowMount : mount;
return fn(TasksByTypeFilters, {
propsData: {
selectedLabelIds,
labels: groupLabels,
subjectFilter: TASKS_BY_TYPE_SUBJECT_ISSUE,
...props,
},
stubs: {
GlNewDropdown: true,
GlNewDropdownItem: true,
},
});
}
describe('TasksByTypeFilters', () => {
let wrapper = null;
describe('with data available', () => {
beforeEach(() => {
wrapper = createComponent({});
});
afterEach(() => {
wrapper.destroy();
});
it('should render the filters', () => {
expect(wrapper.html()).toMatchSnapshot();
});
describe('labels', () => {
it(`should have ${selectedLabelIds.length} selected`, () => {
expect(wrapper.text()).toContain('1 selected (15 max)');
});
describe('with label dropdown open', () => {
beforeEach(() => {
wrapper = createComponent({});
});
it('renders the group labels as dropdown items', () => {
expect(findDropdown(wrapper).html()).toMatchSnapshot();
});
it('emits the `updateFilter` event when a subject label is clicked', () => {
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', () => {
beforeEach(() => {
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', () => {
it('has subject filters', () => {
expect(findSubjectFilters(wrapper).html()).toMatchSnapshot();
});
it('has the issue subject set by default', () => {
expect(findSelectedSubjectFilters(wrapper)).toBe(TASKS_BY_TYPE_SUBJECT_ISSUE);
});
it('emits the `updateFilter` event when a subject filter is clicked', () => {
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 },
]);
});
});
});
});
describe('no data available', () => {
beforeEach(() => {
wrapper = createComponent({});
});
afterEach(() => {
wrapper.destroy();
});
it('should render the no data available message', () => {
expect(wrapper.html()).toMatchSnapshot();
});
});
});
...@@ -19909,6 +19909,9 @@ msgstr "" ...@@ -19909,6 +19909,9 @@ msgstr ""
msgid "There was an error fetching the environments information." msgid "There was an error fetching the environments information."
msgstr "" msgstr ""
msgid "There was an error fetching the top labels for the selected group"
msgstr ""
msgid "There was an error fetching the variables." msgid "There was an error fetching the variables."
msgstr "" msgstr ""
......
...@@ -215,4 +215,4 @@ ...@@ -215,4 +215,4 @@
"node": ">=10.13.0", "node": ">=10.13.0",
"yarn": "^1.10.0" "yarn": "^1.10.0"
} }
} }
\ No newline at end of file
...@@ -821,6 +821,25 @@ ...@@ -821,6 +821,25 @@
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