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

Refactor assignee select in board scope

parent a795f0fa
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
import { GlModal, GlAlert } from '@gitlab/ui'; import { GlModal, GlAlert } from '@gitlab/ui';
import { mapGetters, mapActions, mapState } from 'vuex'; import { mapGetters, mapActions, mapState } from 'vuex';
import ListLabel from '~/boards/models/label'; import ListLabel from '~/boards/models/label';
import { TYPE_ITERATION, TYPE_MILESTONE, TYPE_USER } from '~/graphql_shared/constants'; import { TYPE_ITERATION, TYPE_MILESTONE } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils'; import { convertToGraphQLId } from '~/graphql_shared/utils';
import { getParameterByName, visitUrl } from '~/lib/utils/url_utility'; import { getParameterByName, visitUrl } from '~/lib/utils/url_utility';
import { __, s__ } from '~/locale'; import { __, s__ } from '~/locale';
...@@ -21,7 +21,6 @@ const boardDefaults = { ...@@ -21,7 +21,6 @@ const boardDefaults = {
milestone_id: undefined, milestone_id: undefined,
iteration_id: undefined, iteration_id: undefined,
assignee: {}, assignee: {},
assignee_id: undefined,
weight: null, weight: null,
hide_backlog_list: false, hide_backlog_list: false,
hide_closed_list: false, hide_closed_list: false,
...@@ -190,9 +189,7 @@ export default { ...@@ -190,9 +189,7 @@ export default {
issueBoardScopeMutationVariables() { issueBoardScopeMutationVariables() {
return { return {
weight: this.board.weight, weight: this.board.weight,
assigneeId: this.board.assignee?.id assigneeId: this.board.assignee?.id || null,
? convertToGraphQLId(TYPE_USER, this.board.assignee.id)
: null,
milestoneId: milestoneId:
this.board.milestone?.id || this.board.milestone?.id === 0 this.board.milestone?.id || this.board.milestone?.id === 0
? convertToGraphQLId(TYPE_MILESTONE, this.board.milestone.id) ? convertToGraphQLId(TYPE_MILESTONE, this.board.milestone.id)
...@@ -306,6 +303,11 @@ export default { ...@@ -306,6 +303,11 @@ export default {
} }
}); });
}, },
setAssignee(assigneeId) {
this.board.assignee = {
id: assigneeId,
};
},
}, },
}; };
</script> </script>
...@@ -373,6 +375,7 @@ export default { ...@@ -373,6 +375,7 @@ export default {
:weights="weights" :weights="weights"
@set-iteration="setIteration" @set-iteration="setIteration"
@set-board-labels="setBoardLabels" @set-board-labels="setBoardLabels"
@set-assignee="setAssignee"
/> />
</form> </form>
</gl-modal> </gl-modal>
......
#import "../fragments/user.fragment.graphql"
#import "~/graphql_shared/fragments/user_availability.fragment.graphql"
query usersSearch($search: String!, $fullPath: ID!) {
workspace: group(fullPath: $fullPath) {
users: groupMembers(search: $search, relations: [DIRECT, INHERITED]) {
nodes {
user {
...User
...UserAvailability
}
}
}
}
}
...@@ -134,15 +134,10 @@ export default { ...@@ -134,15 +134,10 @@ export default {
<assignee-select <assignee-select
v-if="isIssueBoard" v-if="isIssueBoard"
:board="board" :board="board"
:selected="board.assignee"
:can-edit="canAdminBoard" :can-edit="canAdminBoard"
:project-id="projectId" :project-id="projectId"
:group-id="groupId" :group-id="groupId"
any-user-text="Any assignee" @set-assignee="$emit('set-assignee', $event)"
field-name="assignee_id"
label="Assignee"
placeholder-text="Select assignee"
wrapper-class="assignee"
/> />
<!-- eslint-disable vue/no-mutating-props --> <!-- eslint-disable vue/no-mutating-props -->
......
...@@ -229,7 +229,7 @@ RSpec.describe 'Scoped issue boards', :js do ...@@ -229,7 +229,7 @@ RSpec.describe 'Scoped issue boards', :js do
edit_board.click edit_board.click
expect(find('.milestone .value')).to have_content(milestone.title) expect(find('.milestone .value')).to have_content(milestone.title)
expect(find('.assignee .value')).to have_content(user.name) expect(find('[data-testid="selected-assignee"]')).to have_content(user.name)
expect(find('.weight .value')).to have_content(2) expect(find('.weight .value')).to have_content(2)
end end
...@@ -564,7 +564,7 @@ RSpec.describe 'Scoped issue boards', :js do ...@@ -564,7 +564,7 @@ RSpec.describe 'Scoped issue boards', :js do
click_button value click_button value
end end
else else
click_link value click_on value
end end
end end
end end
......
import MockAdapter from 'axios-mock-adapter'; import { GlButton, GlDropdown } from '@gitlab/ui';
import Vue from 'vue'; import { shallowMount, createLocalVue } from '@vue/test-utils';
import { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import Vuex from 'vuex';
import AssigneeSelect from 'ee/boards/components/assignee_select.vue'; import AssigneeSelect from 'ee/boards/components/assignee_select.vue';
import { boardObj } from 'jest/boards/mock_data';
import boardsStore from '~/boards/stores/boards_store';
import IssuableContext from '~/issuable_context';
import axios from '~/lib/utils/axios_utils';
let vm;
function selectedText() { import createMockApollo from 'helpers/mock_apollo_helper';
return vm.$el.querySelector('.value').innerText.trim(); import waitForPromises from 'helpers/wait_for_promises';
}
function activeDropdownItem(index) { import { boardObj } from 'jest/boards/mock_data';
const items = document.querySelectorAll('.is-active'); import { projectMembersResponse, groupMembersResponse, mockUser2 } from 'jest/sidebar/mock_data';
if (!items[index]) return '';
return items[index].innerText.trim();
}
const assignee = { import defaultStore from '~/boards/stores';
id: 1, import searchGroupUsersQuery from '~/graphql_shared/queries/group_users_search.query.graphql';
name: 'first assignee', import searchProjectUsersQuery from '~/graphql_shared/queries/users_search.query.graphql';
}; import { ASSIGNEES_DEBOUNCE_DELAY } from '~/sidebar/constants';
const assignee2 = { const localVue = createLocalVue();
id: 2, localVue.use(VueApollo);
name: 'second assignee',
};
describe('Assignee select component', () => { describe('Assignee select component', () => {
beforeEach((done) => { let wrapper;
setFixtures('<div class="test-container"></div>'); let fakeApollo;
boardsStore.create(); let store;
// eslint-disable-next-line no-new const selectedText = () => wrapper.find('[data-testid="selected-assignee"]').text();
new IssuableContext(); const findEditButton = () => wrapper.findComponent(GlButton);
const findDropdown = () => wrapper.findComponent(GlDropdown);
const Component = Vue.extend(AssigneeSelect);
vm = new Component({ const usersQueryHandlerSuccess = jest.fn().mockResolvedValue(projectMembersResponse);
const groupUsersQueryHandlerSuccess = jest.fn().mockResolvedValue(groupMembersResponse);
const createStore = ({ isGroupBoard = false, isProjectBoard = false } = {}) => {
store = new Vuex.Store({
...defaultStore,
getters: {
isGroupBoard: () => isGroupBoard,
isProjectBoard: () => isProjectBoard,
},
});
};
const createComponent = ({ props = {}, usersQueryHandler = usersQueryHandlerSuccess } = {}) => {
fakeApollo = createMockApollo([
[searchProjectUsersQuery, usersQueryHandler],
[searchGroupUsersQuery, groupUsersQueryHandlerSuccess],
]);
wrapper = shallowMount(AssigneeSelect, {
localVue,
store,
apolloProvider: fakeApollo,
propsData: { propsData: {
board: boardObj, board: boardObj,
assigneePath: '/test/issue-boards/assignees.json',
canEdit: true, canEdit: true,
label: 'Assignee', ...props,
selected: {}, },
fieldName: 'assignee_id', provide: {
anyUserText: 'Any assignee', fullPath: 'gitlab-org',
}, },
}).$mount('.test-container');
setImmediate(done);
}); });
describe('canEdit', () => { // We need to mock out `showDropdown` which
it('hides Edit button', (done) => { // invokes `show` method of BDropdown used inside GlDropdown.
vm.canEdit = false; jest.spyOn(wrapper.vm, 'showDropdown').mockImplementation();
Vue.nextTick(() => { };
expect(vm.$el.querySelector('.edit-link')).toBeFalsy();
done();
});
});
it('shows Edit button if true', (done) => { beforeEach(() => {
vm.canEdit = true; createStore({ isProjectBoard: true });
Vue.nextTick(() => { createComponent();
expect(vm.$el.querySelector('.edit-link')).toBeTruthy();
done();
});
}); });
afterEach(() => {
wrapper.destroy();
fakeApollo = null;
store = null;
}); });
describe('selected value', () => { describe('when not editing', () => {
it('defaults to Any Assignee', () => { it('defaults to Any Assignee', () => {
expect(selectedText()).toContain('Any assignee'); expect(selectedText()).toContain('Any assignee');
}); });
it('shows selected assignee', (done) => { it('skips the queries and does not render dropdown', () => {
vm.selected = assignee; expect(usersQueryHandlerSuccess).not.toHaveBeenCalled();
Vue.nextTick(() => { expect(findDropdown().isVisible()).toBe(false);
expect(selectedText()).toContain('first assignee');
done();
}); });
}); });
describe('clicking dropdown items', () => { describe('when editing', () => {
let mock; it('trigger query and renders dropdown with returned users', async () => {
findEditButton().vm.$emit('click');
await waitForPromises();
jest.advanceTimersByTime(ASSIGNEES_DEBOUNCE_DELAY);
await nextTick();
expect(usersQueryHandlerSuccess).toHaveBeenCalled();
beforeEach(() => { expect(findDropdown().isVisible()).toBe(true);
mock = new MockAdapter(axios); expect(wrapper.findAll('[data-testid="unselected-user"]')).toHaveLength(3); // 2 users + Any assignee item
mock.onGet('/-/autocomplete/users.json').reply(200, [assignee, assignee2]);
}); });
afterEach(() => { it('renders selected assignee', async () => {
mock.restore(); findEditButton().vm.$emit('click');
}); await waitForPromises();
jest.advanceTimersByTime(ASSIGNEES_DEBOUNCE_DELAY);
await nextTick();
it('sets assignee', (done) => { wrapper
vm.$el.querySelector('.edit-link').click(); .findAll('[data-testid="unselected-user"]')
.at(1)
.vm.$emit('click', new Event('click'));
jest.runOnlyPendingTimers(); await waitForPromises();
expect(selectedText()).toContain(mockUser2.username);
});
});
setImmediate(() => { describe('canEdit', () => {
vm.$el.querySelectorAll('li a')[2].click(); it('hides Edit button', async () => {
wrapper.setProps({ canEdit: false });
await nextTick();
setImmediate(() => { expect(findEditButton().exists()).toBe(false);
expect(activeDropdownItem(0)).toEqual('second assignee');
expect(vm.board.assignee).toEqual(assignee2);
done();
});
}); });
it('shows Edit button if true', () => {
expect(findEditButton().exists()).toBe(true);
}); });
}); });
it.each`
boardType | mockedResponse | queryHandler | notCalledHandler
${'group'} | ${groupMembersResponse} | ${groupUsersQueryHandlerSuccess} | ${usersQueryHandlerSuccess}
${'project'} | ${projectMembersResponse} | ${usersQueryHandlerSuccess} | ${groupUsersQueryHandlerSuccess}
`(
'fetches $boardType users',
async ({ boardType, mockedResponse, queryHandler, notCalledHandler }) => {
createStore({ isProjectBoard: boardType === 'project', isGroupBoard: boardType === 'group' });
createComponent({
[queryHandler]: jest.fn().mockResolvedValue(mockedResponse),
}); });
findEditButton().vm.$emit('click');
await waitForPromises();
jest.advanceTimersByTime(ASSIGNEES_DEBOUNCE_DELAY);
await nextTick();
expect(queryHandler).toHaveBeenCalled();
expect(notCalledHandler).not.toHaveBeenCalled();
},
);
}); });
import { createLocalVue, mount } from '@vue/test-utils'; import { mount } from '@vue/test-utils';
import { nextTick } from 'vue'; import Vue, { nextTick } from 'vue';
import Vuex from 'vuex'; import Vuex from 'vuex';
import BoardScope from 'ee/boards/components/board_scope.vue'; import BoardScope from 'ee/boards/components/board_scope.vue';
import { useMockIntersectionObserver } from 'helpers/mock_dom_observer'; import { useMockIntersectionObserver } from 'helpers/mock_dom_observer';
import { TEST_HOST } from 'helpers/test_constants'; import { TEST_HOST } from 'helpers/test_constants';
import LabelsSelect from '~/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue'; import LabelsSelect from '~/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue';
const localVue = createLocalVue(); Vue.use(Vuex);
localVue.use(Vuex);
describe('BoardScope', () => { describe('BoardScope', () => {
let wrapper; let wrapper;
...@@ -25,7 +24,6 @@ describe('BoardScope', () => { ...@@ -25,7 +24,6 @@ describe('BoardScope', () => {
function mountComponent() { function mountComponent() {
wrapper = mount(BoardScope, { wrapper = mount(BoardScope, {
localVue,
store, store,
propsData: { propsData: {
collapseScope: false, collapseScope: false,
...@@ -37,6 +35,9 @@ describe('BoardScope', () => { ...@@ -37,6 +35,9 @@ describe('BoardScope', () => {
labelsPath: `${TEST_HOST}/labels`, labelsPath: `${TEST_HOST}/labels`,
labelsWebUrl: `${TEST_HOST}/-/labels`, labelsWebUrl: `${TEST_HOST}/-/labels`,
}, },
stubs: {
AssigneeSelect: true,
},
}); });
} }
......
...@@ -3912,9 +3912,6 @@ msgstr "" ...@@ -3912,9 +3912,6 @@ msgstr ""
msgid "Any namespace" msgid "Any namespace"
msgstr "" msgstr ""
msgid "Any user"
msgstr ""
msgid "App ID" msgid "App ID"
msgstr "" msgstr ""
...@@ -5284,6 +5281,24 @@ msgstr "" ...@@ -5284,6 +5281,24 @@ msgstr ""
msgid "BoardNewIssue|Select a project" msgid "BoardNewIssue|Select a project"
msgstr "" msgstr ""
msgid "BoardScope|An error occurred while searching for users, please try again."
msgstr ""
msgid "BoardScope|Any assignee"
msgstr ""
msgid "BoardScope|Assignee"
msgstr ""
msgid "BoardScope|Edit"
msgstr ""
msgid "BoardScope|No matching results"
msgstr ""
msgid "BoardScope|Select assignee"
msgstr ""
msgid "Boards" msgid "Boards"
msgstr "" msgstr ""
...@@ -29525,9 +29540,6 @@ msgstr "" ...@@ -29525,9 +29540,6 @@ msgstr ""
msgid "Select type" msgid "Select type"
msgstr "" msgstr ""
msgid "Select user"
msgstr ""
msgid "Selected" msgid "Selected"
msgstr "" msgstr ""
......
...@@ -415,7 +415,7 @@ const mockUser1 = { ...@@ -415,7 +415,7 @@ const mockUser1 = {
status: null, status: null,
}; };
const mockUser2 = { export const mockUser2 = {
id: 'gid://gitlab/User/4', id: 'gid://gitlab/User/4',
avatarUrl: '/avatar2', avatarUrl: '/avatar2',
name: 'rookie', name: 'rookie',
...@@ -452,9 +452,40 @@ export const projectMembersResponse = { ...@@ -452,9 +452,40 @@ export const projectMembersResponse = {
null, null,
null, null,
// Remove duplicated entry https://gitlab.com/gitlab-org/gitlab/-/issues/327822 // Remove duplicated entry https://gitlab.com/gitlab-org/gitlab/-/issues/327822
mockUser1, { user: mockUser1 },
mockUser1, { user: mockUser1 },
mockUser2, { user: mockUser2 },
{
user: {
id: 'gid://gitlab/User/2',
avatarUrl:
'https://www.gravatar.com/avatar/a95e5b71488f4b9d69ce5ff58bfd28d6?s=80\u0026d=identicon',
name: 'Jacki Kub',
username: 'francina.skiles',
webUrl: '/franc',
status: {
availability: 'BUSY',
},
},
},
],
},
},
},
};
export const groupMembersResponse = {
data: {
workspace: {
__typename: 'roup',
users: {
nodes: [
// Remove nulls https://gitlab.com/gitlab-org/gitlab/-/issues/329750
null,
null,
// Remove duplicated entry https://gitlab.com/gitlab-org/gitlab/-/issues/327822
{ user: mockUser1 },
{ user: mockUser1 },
{ {
user: { user: {
id: 'gid://gitlab/User/2', id: 'gid://gitlab/User/2',
......
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