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>
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