Commit 65cb4d96 authored by Vitaly Slobodin's avatar Vitaly Slobodin

Merge branch...

Merge branch '332257-refactor-dropdowncontentseditview-component-to-use-graphql-apollo' into 'master'

Refactor DropdownContentsEditView component to use GraphQL + Apollo

See merge request gitlab-org/gitlab!65440
parents 4640daaf 7523392c
...@@ -55,12 +55,13 @@ export default { ...@@ -55,12 +55,13 @@ export default {
}, },
getUpdateVariables(dropdownLabels) { getUpdateVariables(dropdownLabels) {
const currentLabelIds = this.selectedLabels.map((label) => label.id); const currentLabelIds = this.selectedLabels.map((label) => label.id);
const userAddedLabelIds = dropdownLabels const dropdownLabelIds = dropdownLabels.map((label) => label.id);
.filter((label) => label.set) const userAddedLabelIds = this.glFeatures.labelsWidget
.map((label) => label.id); ? difference(dropdownLabelIds, currentLabelIds)
const userRemovedLabelIds = dropdownLabels : dropdownLabels.filter((label) => label.set).map((label) => label.id);
.filter((label) => !label.set) const userRemovedLabelIds = this.glFeatures.labelsWidget
.map((label) => label.id); ? difference(currentLabelIds, dropdownLabelIds)
: dropdownLabels.filter((label) => !label.set).map((label) => label.id);
const labelIds = difference(union(currentLabelIds, userAddedLabelIds), userRemovedLabelIds); const labelIds = difference(union(currentLabelIds, userAddedLabelIds), userRemovedLabelIds);
...@@ -155,7 +156,7 @@ export default { ...@@ -155,7 +156,7 @@ export default {
:labels-manage-path="labelsManagePath" :labels-manage-path="labelsManagePath"
:labels-select-in-progress="isLabelsSelectInProgress" :labels-select-in-progress="isLabelsSelectInProgress"
:selected-labels="selectedLabels" :selected-labels="selectedLabels"
:variant="$options.sidebar" :variant="$options.variant"
data-qa-selector="labels_block" data-qa-selector="labels_block"
@onDropdownClose="handleDropdownClose" @onDropdownClose="handleDropdownClose"
@onLabelRemove="handleLabelRemove" @onLabelRemove="handleLabelRemove"
......
...@@ -24,6 +24,7 @@ import SidebarDropdownWidget from '~/sidebar/components/sidebar_dropdown_widget. ...@@ -24,6 +24,7 @@ import SidebarDropdownWidget from '~/sidebar/components/sidebar_dropdown_widget.
import SidebarTodoWidget from '~/sidebar/components/todo_toggle/sidebar_todo_widget.vue'; import SidebarTodoWidget from '~/sidebar/components/todo_toggle/sidebar_todo_widget.vue';
import { apolloProvider } from '~/sidebar/graphql'; import { apolloProvider } from '~/sidebar/graphql';
import trackShowInviteMemberLink from '~/sidebar/track_invite_members'; import trackShowInviteMemberLink from '~/sidebar/track_invite_members';
import { DropdownVariant } from '~/vue_shared/components/sidebar/labels_select_vue/constants';
import Translate from '../vue_shared/translate'; import Translate from '../vue_shared/translate';
import SidebarAssignees from './components/assignees/sidebar_assignees.vue'; import SidebarAssignees from './components/assignees/sidebar_assignees.vue';
import CopyEmailToClipboard from './components/copy_email_to_clipboard.vue'; import CopyEmailToClipboard from './components/copy_email_to_clipboard.vue';
...@@ -256,6 +257,7 @@ export function mountSidebarLabels() { ...@@ -256,6 +257,7 @@ export function mountSidebarLabels() {
allowLabelEdit: parseBoolean(el.dataset.canEdit), allowLabelEdit: parseBoolean(el.dataset.canEdit),
allowScopedLabels: parseBoolean(el.dataset.allowScopedLabels), allowScopedLabels: parseBoolean(el.dataset.allowScopedLabels),
initiallySelectedLabels: JSON.parse(el.dataset.selectedLabels), initiallySelectedLabels: JSON.parse(el.dataset.selectedLabels),
variant: DropdownVariant.Sidebar,
}, },
render: (createElement) => createElement(SidebarLabels), render: (createElement) => createElement(SidebarLabels),
}); });
......
...@@ -21,9 +21,29 @@ export default { ...@@ -21,9 +21,29 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
selectedLabels: {
type: Array,
required: true,
},
allowMultiselect: {
type: Boolean,
required: true,
},
labelsListTitle: {
type: String,
required: true,
},
footerCreateLabelTitle: {
type: String,
required: true,
},
footerManageLabelTitle: {
type: String,
required: true,
},
}, },
computed: { computed: {
...mapState(['showDropdownContentsCreateView', 'labelsListTitle']), ...mapState(['showDropdownContentsCreateView']),
...mapGetters(['isDropdownVariantSidebar', 'isDropdownVariantEmbedded']), ...mapGetters(['isDropdownVariantSidebar', 'isDropdownVariantEmbedded']),
dropdownContentsView() { dropdownContentsView() {
if (this.showDropdownContentsCreateView) { if (this.showDropdownContentsCreateView) {
...@@ -75,6 +95,16 @@ export default { ...@@ -75,6 +95,16 @@ export default {
@click="toggleDropdownContents" @click="toggleDropdownContents"
/> />
</div> </div>
<component :is="dropdownContentsView" @hideCreateView="toggleDropdownContentsCreateView" /> <component
:is="dropdownContentsView"
:selected-labels="selectedLabels"
:allow-multiselect="allowMultiselect"
:labels-list-title="labelsListTitle"
:footer-create-label-title="footerCreateLabelTitle"
:footer-manage-label-title="footerManageLabelTitle"
@hideCreateView="toggleDropdownContentsCreateView"
@closeDropdown="$emit('closeDropdown', $event)"
@toggleDropdownContentsCreateView="toggleDropdownContentsCreateView"
/>
</div> </div>
</template> </template>
<script> <script>
import { GlIntersectionObserver, GlLoadingIcon, GlSearchBoxByType, GlLink } from '@gitlab/ui'; import { GlLoadingIcon, GlSearchBoxByType, GlLink } from '@gitlab/ui';
import fuzzaldrinPlus from 'fuzzaldrin-plus'; import fuzzaldrinPlus from 'fuzzaldrin-plus';
import { mapState, mapGetters, mapActions } from 'vuex'; import { debounce } from 'lodash';
import createFlash from '~/flash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import { UP_KEY_CODE, DOWN_KEY_CODE, ENTER_KEY_CODE, ESC_KEY_CODE } from '~/lib/utils/keycodes'; import { UP_KEY_CODE, DOWN_KEY_CODE, ENTER_KEY_CODE, ESC_KEY_CODE } from '~/lib/utils/keycodes';
import { __ } from '~/locale';
import { DropdownVariant } from './constants';
import projectLabelsQuery from './graphql/project_labels.query.graphql';
import LabelItem from './label_item.vue'; import LabelItem from './label_item.vue';
export default { export default {
components: { components: {
GlIntersectionObserver,
GlLoadingIcon, GlLoadingIcon,
GlSearchBoxByType, GlSearchBoxByType,
GlLink, GlLink,
LabelItem, LabelItem,
}, },
inject: ['projectPath', 'allowLabelCreate', 'labelsManagePath', 'variant'],
props: {
selectedLabels: {
type: Array,
required: true,
},
allowMultiselect: {
type: Boolean,
required: true,
},
labelsListTitle: {
type: String,
required: true,
},
footerCreateLabelTitle: {
type: String,
required: true,
},
footerManageLabelTitle: {
type: String,
required: true,
},
},
data() { data() {
return { return {
searchKey: '', searchKey: '',
labels: [],
currentHighlightItem: -1, currentHighlightItem: -1,
localSelectedLabels: [...this.selectedLabels],
};
},
apollo: {
labels: {
query: projectLabelsQuery,
variables() {
return {
fullPath: this.projectPath,
searchTerm: this.searchKey,
}; };
}, },
skip() {
return this.searchKey.length === 1;
},
update: (data) => data.workspace?.labels?.nodes || [],
async result() {
if (this.$refs.searchInput) {
await this.$nextTick();
this.$refs.searchInput.focusInput();
}
},
error() {
createFlash({ message: __('Error fetching labels.') });
},
},
},
computed: { computed: {
...mapState([ isDropdownVariantSidebar() {
'allowLabelCreate', return this.variant === DropdownVariant.Sidebar;
'allowMultiselect', },
'labelsManagePath', isDropdownVariantEmbedded() {
'labels', return this.variant === DropdownVariant.Embedded;
'labelsFetchInProgress', },
'labelsListTitle', labelsFetchInProgress() {
'footerCreateLabelTitle', return this.$apollo.queries.labels.loading;
'footerManageLabelTitle', },
]), localSelectedLabelsIds() {
...mapGetters(['selectedLabelsList', 'isDropdownVariantSidebar', 'isDropdownVariantEmbedded']), return this.localSelectedLabels.map((label) => label.id);
},
visibleLabels() { visibleLabels() {
if (this.searchKey) { if (this.searchKey) {
return fuzzaldrinPlus.filter(this.labels, this.searchKey, { return fuzzaldrinPlus.filter(this.labels, this.searchKey, {
...@@ -55,17 +108,16 @@ export default { ...@@ -55,17 +108,16 @@ export default {
} }
}, },
}, },
created() {
this.debouncedSearchKeyUpdate = debounce(this.setSearchKey, DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
},
beforeDestroy() {
this.$emit('closeDropdown', this.localSelectedLabels);
this.debouncedSearchKeyUpdate.cancel();
},
methods: { methods: {
...mapActions([
'toggleDropdownContents',
'toggleDropdownContentsCreateView',
'fetchLabels',
'receiveLabelsSuccess',
'updateSelectedLabels',
'toggleDropdownContents',
]),
isLabelSelected(label) { isLabelSelected(label) {
return this.selectedLabelsList.includes(label.id); return this.localSelectedLabelsIds.includes(getIdFromGraphQLId(label.id));
}, },
/** /**
* This method scrolls item from dropdown into * This method scrolls item from dropdown into
...@@ -86,23 +138,17 @@ export default { ...@@ -86,23 +138,17 @@ export default {
} }
} }
}, },
handleComponentAppear() { updateSelectedLabels(label) {
// We can avoid putting `catch` block here if (this.isLabelSelected(label)) {
// as failure is handled within actions.js already. this.localSelectedLabels = this.localSelectedLabels.filter(
return this.fetchLabels().then(() => { ({ id }) => id !== getIdFromGraphQLId(label.id),
this.$refs.searchInput.focusInput(); );
} else {
this.localSelectedLabels.push({
...label,
id: getIdFromGraphQLId(label.id),
}); });
}, }
/**
* We want to remove loaded labels to ensure component
* fetches fresh set of labels every time when shown.
*/
handleComponentDisappear() {
this.receiveLabelsSuccess([]);
},
handleCreateLabelClick() {
this.receiveLabelsSuccess([]);
this.toggleDropdownContentsCreateView();
}, },
/** /**
* This method enables keyboard navigation support for * This method enables keyboard navigation support for
...@@ -117,10 +163,10 @@ export default { ...@@ -117,10 +163,10 @@ export default {
) { ) {
this.currentHighlightItem += 1; this.currentHighlightItem += 1;
} else if (e.keyCode === ENTER_KEY_CODE && this.currentHighlightItem > -1) { } else if (e.keyCode === ENTER_KEY_CODE && this.currentHighlightItem > -1) {
this.updateSelectedLabels([this.visibleLabels[this.currentHighlightItem]]); this.updateSelectedLabels(this.visibleLabels[this.currentHighlightItem]);
this.searchKey = ''; this.searchKey = '';
} else if (e.keyCode === ESC_KEY_CODE) { } else if (e.keyCode === ESC_KEY_CODE) {
this.toggleDropdownContents(); this.$emit('closeDropdown', this.localSelectedLabels);
} }
if (e.keyCode !== ESC_KEY_CODE) { if (e.keyCode !== ESC_KEY_CODE) {
...@@ -132,40 +178,54 @@ export default { ...@@ -132,40 +178,54 @@ export default {
} }
}, },
handleLabelClick(label) { handleLabelClick(label) {
this.updateSelectedLabels([label]); this.updateSelectedLabels(label);
if (!this.allowMultiselect) this.toggleDropdownContents(); if (!this.allowMultiselect) {
this.$emit('closeDropdown', this.localSelectedLabels);
}
},
setSearchKey(value) {
this.searchKey = value;
}, },
}, },
}; };
</script> </script>
<template> <template>
<gl-intersection-observer @appear="handleComponentAppear" @disappear="handleComponentDisappear"> <div
<div class="labels-select-contents-list js-labels-list" @keydown="handleKeyDown"> class="labels-select-contents-list js-labels-list"
data-testid="dropdown-wrapper"
@keydown="handleKeyDown"
>
<div class="dropdown-input" @click.stop="() => {}"> <div class="dropdown-input" @click.stop="() => {}">
<gl-search-box-by-type <gl-search-box-by-type
ref="searchInput" ref="searchInput"
v-model="searchKey" :value="searchKey"
:disabled="labelsFetchInProgress" :disabled="labelsFetchInProgress"
data-qa-selector="dropdown_input_field" data-qa-selector="dropdown_input_field"
data-testid="dropdown-input-field"
@input="debouncedSearchKeyUpdate"
/> />
</div> </div>
<div ref="labelsListContainer" class="dropdown-content" data-testid="dropdown-content"> <div ref="labelsListContainer" class="dropdown-content" data-testid="dropdown-content">
<gl-loading-icon <gl-loading-icon
v-if="labelsFetchInProgress" v-if="labelsFetchInProgress"
class="labels-fetch-loading gl-align-items-center w-100 h-100" class="labels-fetch-loading gl-align-items-center gl-w-full gl-h-full"
size="md" size="md"
/> />
<ul v-else class="list-unstyled gl-mb-0 gl-word-break-word"> <ul v-else class="list-unstyled gl-mb-0 gl-word-break-word" data-testid="labels-list">
<label-item <label-item
v-for="(label, index) in visibleLabels" v-for="(label, index) in visibleLabels"
:key="label.id" :key="label.id"
:label="label" :label="label"
:is-label-set="label.set" :is-label-set="isLabelSelected(label)"
:highlight="index === currentHighlightItem" :highlight="index === currentHighlightItem"
@clickLabel="handleLabelClick(label)" @clickLabel="handleLabelClick(label)"
/> />
<li v-show="showNoMatchingResultsMessage" class="gl-p-3 gl-text-center"> <li
v-show="showNoMatchingResultsMessage"
class="gl-p-3 gl-text-center"
data-testid="no-results"
>
{{ __('No matching results') }} {{ __('No matching results') }}
</li> </li>
</ul> </ul>
...@@ -178,8 +238,9 @@ export default { ...@@ -178,8 +238,9 @@ export default {
<ul class="list-unstyled"> <ul class="list-unstyled">
<li v-if="allowLabelCreate"> <li v-if="allowLabelCreate">
<gl-link <gl-link
class="gl-display-flex w-100 flex-row text-break-word label-item" class="gl-display-flex gl-flex-direction-row gl-w-full gl-overflow-break-word label-item"
@click="handleCreateLabelClick" data-testid="create-label-button"
@click="$emit('toggleDropdownContentsCreateView')"
> >
{{ footerCreateLabelTitle }} {{ footerCreateLabelTitle }}
</gl-link> </gl-link>
...@@ -187,7 +248,7 @@ export default { ...@@ -187,7 +248,7 @@ export default {
<li> <li>
<gl-link <gl-link
:href="labelsManagePath" :href="labelsManagePath"
class="gl-display-flex flex-row text-break-word label-item" class="gl-display-flex gl-flex-direction-row gl-w-full gl-overflow-break-word label-item"
> >
{{ footerManageLabelTitle }} {{ footerManageLabelTitle }}
</gl-link> </gl-link>
...@@ -195,5 +256,4 @@ export default { ...@@ -195,5 +256,4 @@ export default {
</ul> </ul>
</div> </div>
</div> </div>
</gl-intersection-observer>
</template> </template>
query projectLabels($fullPath: ID!, $searchTerm: String) {
workspace: project(fullPath: $fullPath) {
labels(searchTerm: $searchTerm, includeAncestorGroups: true) {
nodes {
id
title
color
description
}
}
}
}
...@@ -196,23 +196,6 @@ export default { ...@@ -196,23 +196,6 @@ export default {
}, },
methods: { methods: {
...mapActions(['setInitialState', 'toggleDropdownContents']), ...mapActions(['setInitialState', 'toggleDropdownContents']),
/**
* This method differentiates between
* dispatched actions and calls necessary method.
*/
handleVuexActionDispatch(action, state) {
if (
action.type === 'toggleDropdownContents' &&
!state.showDropdownButton &&
!state.showDropdownContents
) {
let filterFn = (label) => label.touched;
if (this.isDropdownVariantEmbedded) {
filterFn = (label) => label.set;
}
this.handleDropdownClose(state.labels.filter(filterFn));
}
},
/** /**
* This method stores a mousedown event's target. * This method stores a mousedown event's target.
* Required by the click listener because the click * Required by the click listener because the click
...@@ -276,6 +259,9 @@ export default { ...@@ -276,6 +259,9 @@ export default {
handleDropdownClose(labels) { handleDropdownClose(labels) {
// Only emit label updates if there are any labels to update // Only emit label updates if there are any labels to update
// on UI. // on UI.
if (this.showDropdownContents) {
this.toggleDropdownContents();
}
if (labels.length) this.$emit('updateSelectedLabels', labels); if (labels.length) this.$emit('updateSelectedLabels', labels);
this.$emit('onDropdownClose'); this.$emit('onDropdownClose');
}, },
...@@ -332,8 +318,14 @@ export default { ...@@ -332,8 +318,14 @@ export default {
<dropdown-contents <dropdown-contents
v-if="dropdownButtonVisible && showDropdownContents" v-if="dropdownButtonVisible && showDropdownContents"
ref="dropdownContents" ref="dropdownContents"
:allow-multiselect="allowMultiselect"
:labels-list-title="labelsListTitle"
:footer-create-label-title="footerCreateLabelTitle"
:footer-manage-label-title="footerManageLabelTitle"
:render-on-top="!contentIsOnViewport" :render-on-top="!contentIsOnViewport"
:labels-create-title="labelsCreateTitle" :labels-create-title="labelsCreateTitle"
:selected-labels="selectedLabels"
@closeDropdown="handleDropdownClose"
/> />
</template> </template>
<template v-if="isDropdownVariantStandalone || isDropdownVariantEmbedded"> <template v-if="isDropdownVariantStandalone || isDropdownVariantEmbedded">
...@@ -341,7 +333,13 @@ export default { ...@@ -341,7 +333,13 @@ export default {
<dropdown-contents <dropdown-contents
v-if="dropdownButtonVisible && showDropdownContents" v-if="dropdownButtonVisible && showDropdownContents"
ref="dropdownContents" ref="dropdownContents"
:allow-multiselect="allowMultiselect"
:labels-list-title="labelsListTitle"
:footer-create-label-title="footerCreateLabelTitle"
:footer-manage-label-title="footerManageLabelTitle"
:render-on-top="!contentIsOnViewport" :render-on-top="!contentIsOnViewport"
:selected-labels="selectedLabels"
@closeDropdown="handleDropdownClose"
/> />
</template> </template>
</div> </div>
......
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
import * as types from './mutation_types'; import * as types from './mutation_types';
export const setInitialState = ({ commit }, props) => commit(types.SET_INITIAL_STATE, props); export const setInitialState = ({ commit }, props) => commit(types.SET_INITIAL_STATE, props);
...@@ -11,24 +8,5 @@ export const toggleDropdownContents = ({ commit }) => commit(types.TOGGLE_DROPDO ...@@ -11,24 +8,5 @@ export const toggleDropdownContents = ({ commit }) => commit(types.TOGGLE_DROPDO
export const toggleDropdownContentsCreateView = ({ commit }) => export const toggleDropdownContentsCreateView = ({ commit }) =>
commit(types.TOGGLE_DROPDOWN_CONTENTS_CREATE_VIEW); commit(types.TOGGLE_DROPDOWN_CONTENTS_CREATE_VIEW);
export const requestLabels = ({ commit }) => commit(types.REQUEST_LABELS);
export const receiveLabelsSuccess = ({ commit }, labels) =>
commit(types.RECEIVE_SET_LABELS_SUCCESS, labels);
export const receiveLabelsFailure = ({ commit }) => {
commit(types.RECEIVE_SET_LABELS_FAILURE);
createFlash({
message: __('Error fetching labels.'),
});
};
export const fetchLabels = ({ state, dispatch }) => {
dispatch('requestLabels');
return axios
.get(state.labelsFetchPath)
.then(({ data }) => {
dispatch('receiveLabelsSuccess', data);
})
.catch(() => dispatch('receiveLabelsFailure'));
};
export const updateSelectedLabels = ({ commit }, labels) => export const updateSelectedLabels = ({ commit }, labels) =>
commit(types.UPDATE_SELECTED_LABELS, { labels }); commit(types.UPDATE_SELECTED_LABELS, { labels });
export const SET_INITIAL_STATE = 'SET_INITIAL_STATE'; export const SET_INITIAL_STATE = 'SET_INITIAL_STATE';
export const REQUEST_LABELS = 'REQUEST_LABELS';
export const RECEIVE_LABELS_SUCCESS = 'RECEIVE_LABELS_SUCCESS';
export const RECEIVE_LABELS_FAILURE = 'RECEIVE_LABELS_FAILURE';
export const REQUEST_SET_LABELS = 'REQUEST_SET_LABELS';
export const RECEIVE_SET_LABELS_SUCCESS = 'RECEIVE_SET_LABELS_SUCCESS';
export const RECEIVE_SET_LABELS_FAILURE = 'RECEIVE_SET_LABELS_FAILURE';
export const TOGGLE_DROPDOWN_BUTTON = 'TOGGLE_DROPDOWN_VISIBILITY'; export const TOGGLE_DROPDOWN_BUTTON = 'TOGGLE_DROPDOWN_VISIBILITY';
export const TOGGLE_DROPDOWN_CONTENTS = 'TOGGLE_DROPDOWN_CONTENTS'; export const TOGGLE_DROPDOWN_CONTENTS = 'TOGGLE_DROPDOWN_CONTENTS';
......
...@@ -26,27 +26,6 @@ export default { ...@@ -26,27 +26,6 @@ export default {
[types.TOGGLE_DROPDOWN_CONTENTS_CREATE_VIEW](state) { [types.TOGGLE_DROPDOWN_CONTENTS_CREATE_VIEW](state) {
state.showDropdownContentsCreateView = !state.showDropdownContentsCreateView; state.showDropdownContentsCreateView = !state.showDropdownContentsCreateView;
}, },
[types.REQUEST_LABELS](state) {
state.labelsFetchInProgress = true;
},
[types.RECEIVE_SET_LABELS_SUCCESS](state, labels) {
// Iterate over every label and add a `set` prop
// to determine whether it is already a part of
// selectedLabels array.
const selectedLabelIds = state.selectedLabels.map((label) => label.id);
state.labelsFetchInProgress = false;
state.labels = labels.reduce((allLabels, label) => {
allLabels.push({
...label,
set: selectedLabelIds.includes(label.id),
});
return allLabels;
}, []);
},
[types.RECEIVE_SET_LABELS_FAILURE](state) {
state.labelsFetchInProgress = false;
},
[types.UPDATE_SELECTED_LABELS](state, { labels }) { [types.UPDATE_SELECTED_LABELS](state, { labels }) {
// Find the label to update from all the labels // Find the label to update from all the labels
// and change `set` prop value to represent their current state. // and change `set` prop value to represent their current state.
......
import { GlLoadingIcon, GlLink } from '@gitlab/ui'; import { GlLoadingIcon, GlLink } from '@gitlab/ui';
import { shallowMount, createLocalVue } from '@vue/test-utils'; import { shallowMount, createLocalVue } from '@vue/test-utils';
import Vue, { nextTick } from 'vue'; import { nextTick } from 'vue';
import VueApollo from 'vue-apollo'; import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper'; import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises'; import waitForPromises from 'helpers/wait_for_promises';
...@@ -14,7 +14,7 @@ jest.mock('~/flash'); ...@@ -14,7 +14,7 @@ jest.mock('~/flash');
const colors = Object.keys(mockSuggestedColors); const colors = Object.keys(mockSuggestedColors);
const localVue = createLocalVue(); const localVue = createLocalVue();
Vue.use(VueApollo); localVue.use(VueApollo);
const userRecoverableError = { const userRecoverableError = {
...createLabelSuccessfulResponse, ...createLabelSuccessfulResponse,
......
...@@ -5,7 +5,7 @@ import { DropdownVariant } from '~/vue_shared/components/sidebar/labels_select_w ...@@ -5,7 +5,7 @@ import { DropdownVariant } from '~/vue_shared/components/sidebar/labels_select_w
import DropdownContents from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue'; import DropdownContents from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue';
import labelsSelectModule from '~/vue_shared/components/sidebar/labels_select_widget/store'; import labelsSelectModule from '~/vue_shared/components/sidebar/labels_select_widget/store';
import { mockConfig } from './mock_data'; import { mockConfig, mockLabels } from './mock_data';
const localVue = createLocalVue(); const localVue = createLocalVue();
localVue.use(Vuex); localVue.use(Vuex);
...@@ -19,6 +19,11 @@ const createComponent = (initialState = mockConfig, defaultProps = {}) => { ...@@ -19,6 +19,11 @@ const createComponent = (initialState = mockConfig, defaultProps = {}) => {
propsData: { propsData: {
...defaultProps, ...defaultProps,
labelsCreateTitle: 'test', labelsCreateTitle: 'test',
selectedLabels: mockLabels,
allowMultiselect: true,
labelsListTitle: 'Assign labels',
footerCreateLabelTitle: 'create',
footerManageLabelTitle: 'manage',
}, },
localVue, localVue,
store, store,
......
...@@ -50,58 +50,6 @@ describe('LabelsSelectRoot', () => { ...@@ -50,58 +50,6 @@ describe('LabelsSelectRoot', () => {
}); });
describe('methods', () => { describe('methods', () => {
describe('handleVuexActionDispatch', () => {
it('calls `handleDropdownClose` when params `action.type` is `toggleDropdownContents` and state has `showDropdownButton` & `showDropdownContents` props `false`', () => {
createComponent();
jest.spyOn(wrapper.vm, 'handleDropdownClose').mockImplementation();
wrapper.vm.handleVuexActionDispatch(
{ type: 'toggleDropdownContents' },
{
showDropdownButton: false,
showDropdownContents: false,
labels: [{ id: 1 }, { id: 2, touched: true }],
},
);
expect(wrapper.vm.handleDropdownClose).toHaveBeenCalledWith(
expect.arrayContaining([
{
id: 2,
touched: true,
},
]),
);
});
it('calls `handleDropdownClose` with state.labels filterd using `set` prop when dropdown variant is `embedded`', () => {
createComponent({
...mockConfig,
variant: 'embedded',
});
jest.spyOn(wrapper.vm, 'handleDropdownClose').mockImplementation();
wrapper.vm.handleVuexActionDispatch(
{ type: 'toggleDropdownContents' },
{
showDropdownButton: false,
showDropdownContents: false,
labels: [{ id: 1 }, { id: 2, set: true }],
},
);
expect(wrapper.vm.handleDropdownClose).toHaveBeenCalledWith(
expect.arrayContaining([
{
id: 2,
set: true,
},
]),
);
});
});
describe('handleDropdownClose', () => { describe('handleDropdownClose', () => {
beforeEach(() => { beforeEach(() => {
createComponent(); createComponent();
......
...@@ -48,6 +48,8 @@ export const mockConfig = { ...@@ -48,6 +48,8 @@ export const mockConfig = {
labelsManagePath: '/gitlab-org/my-project/-/labels', labelsManagePath: '/gitlab-org/my-project/-/labels',
labelsFilterBasePath: '/gitlab-org/my-project/issues', labelsFilterBasePath: '/gitlab-org/my-project/issues',
labelsFilterParam: 'label_name', labelsFilterParam: 'label_name',
footerCreateLabelTitle: 'create',
footerManageLabelTitle: 'manage',
}; };
export const mockSuggestedColors = { export const mockSuggestedColors = {
...@@ -91,3 +93,26 @@ export const createLabelSuccessfulResponse = { ...@@ -91,3 +93,26 @@ export const createLabelSuccessfulResponse = {
}, },
}, },
}; };
export const labelsQueryResponse = {
data: {
workspace: {
labels: {
nodes: [
{
color: '#330066',
description: null,
id: 'gid://gitlab/ProjectLabel/1',
title: 'Label1',
},
{
color: '#2f7b2e',
description: null,
id: 'gid://gitlab/ProjectLabel/2',
title: 'Label2',
},
],
},
},
},
};
import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper'; import testAction from 'helpers/vuex_action_helper';
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import * as actions from '~/vue_shared/components/sidebar/labels_select_widget/store/actions'; import * as actions from '~/vue_shared/components/sidebar/labels_select_widget/store/actions';
import * as types from '~/vue_shared/components/sidebar/labels_select_widget/store/mutation_types'; import * as types from '~/vue_shared/components/sidebar/labels_select_widget/store/mutation_types';
import defaultState from '~/vue_shared/components/sidebar/labels_select_widget/store/state'; import defaultState from '~/vue_shared/components/sidebar/labels_select_widget/store/state';
...@@ -72,90 +68,6 @@ describe('LabelsSelect Actions', () => { ...@@ -72,90 +68,6 @@ describe('LabelsSelect Actions', () => {
}); });
}); });
describe('requestLabels', () => {
it('sets value of `state.labelsFetchInProgress` to `true`', (done) => {
testAction(actions.requestLabels, {}, state, [{ type: types.REQUEST_LABELS }], [], done);
});
});
describe('receiveLabelsSuccess', () => {
it('sets provided labels to `state.labels`', (done) => {
const labels = [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }];
testAction(
actions.receiveLabelsSuccess,
labels,
state,
[{ type: types.RECEIVE_SET_LABELS_SUCCESS, payload: labels }],
[],
done,
);
});
});
describe('receiveLabelsFailure', () => {
it('sets value `state.labelsFetchInProgress` to `false`', (done) => {
testAction(
actions.receiveLabelsFailure,
{},
state,
[{ type: types.RECEIVE_SET_LABELS_FAILURE }],
[],
done,
);
});
it('shows flash error', () => {
actions.receiveLabelsFailure({ commit: () => {} });
expect(createFlash).toHaveBeenCalledWith({ message: 'Error fetching labels.' });
});
});
describe('fetchLabels', () => {
let mock;
beforeEach(() => {
mock = new MockAdapter(axios);
state.labelsFetchPath = 'labels.json';
});
afterEach(() => {
mock.restore();
});
describe('on success', () => {
it('dispatches `requestLabels` & `receiveLabelsSuccess` actions', (done) => {
const labels = [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }];
mock.onGet(/labels.json/).replyOnce(200, labels);
testAction(
actions.fetchLabels,
{},
state,
[],
[{ type: 'requestLabels' }, { type: 'receiveLabelsSuccess', payload: labels }],
done,
);
});
});
describe('on failure', () => {
it('dispatches `requestLabels` & `receiveLabelsFailure` actions', (done) => {
mock.onGet(/labels.json/).replyOnce(500, {});
testAction(
actions.fetchLabels,
{},
state,
[],
[{ type: 'requestLabels' }, { type: 'receiveLabelsFailure' }],
done,
);
});
});
});
describe('updateSelectedLabels', () => { describe('updateSelectedLabels', () => {
it('updates `state.labels` based on provided `labels` param', (done) => { it('updates `state.labels` based on provided `labels` param', (done) => {
const labels = [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }]; const labels = [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }];
......
...@@ -67,58 +67,6 @@ describe('LabelsSelect Mutations', () => { ...@@ -67,58 +67,6 @@ describe('LabelsSelect Mutations', () => {
}); });
}); });
describe(`${types.REQUEST_LABELS}`, () => {
it('sets value of `state.labelsFetchInProgress` to true', () => {
const state = {
labelsFetchInProgress: false,
};
mutations[types.REQUEST_LABELS](state);
expect(state.labelsFetchInProgress).toBe(true);
});
});
describe(`${types.RECEIVE_SET_LABELS_SUCCESS}`, () => {
const selectedLabels = [{ id: 2 }, { id: 4 }];
const labels = [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }];
it('sets value of `state.labelsFetchInProgress` to false', () => {
const state = {
selectedLabels,
labelsFetchInProgress: true,
};
mutations[types.RECEIVE_SET_LABELS_SUCCESS](state, labels);
expect(state.labelsFetchInProgress).toBe(false);
});
it('sets provided `labels` to `state.labels` along with `set` prop based on `state.selectedLabels`', () => {
const selectedLabelIds = selectedLabels.map((label) => label.id);
const state = {
selectedLabels,
labelsFetchInProgress: true,
};
mutations[types.RECEIVE_SET_LABELS_SUCCESS](state, labels);
state.labels.forEach((label) => {
if (selectedLabelIds.includes(label.id)) {
expect(label.set).toBe(true);
}
});
});
});
describe(`${types.RECEIVE_SET_LABELS_FAILURE}`, () => {
it('sets value of `state.labelsFetchInProgress` to false', () => {
const state = {
labelsFetchInProgress: true,
};
mutations[types.RECEIVE_SET_LABELS_FAILURE](state);
expect(state.labelsFetchInProgress).toBe(false);
});
});
describe(`${types.UPDATE_SELECTED_LABELS}`, () => { describe(`${types.UPDATE_SELECTED_LABELS}`, () => {
let labels; let labels;
......
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