Commit 4ed86290 authored by Natalia Tepluhina's avatar Natalia Tepluhina

Merge branch...

Merge branch '235367-migrate-bootstrap-dropdown-to-gitlab-ui-gldropdown-in-app-assets-javascripts-boards' into 'master'

Refactor assignee select in board scope

See merge request gitlab-org/gitlab!66225
parents 38161a12 b7c83cc3
......@@ -2,7 +2,7 @@
import { GlModal, GlAlert } from '@gitlab/ui';
import { mapGetters, mapActions, mapState } from 'vuex';
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 { getParameterByName, visitUrl } from '~/lib/utils/url_utility';
import { __, s__ } from '~/locale';
......@@ -21,7 +21,6 @@ const boardDefaults = {
milestone_id: undefined,
iteration_id: undefined,
assignee: {},
assignee_id: undefined,
weight: null,
hide_backlog_list: false,
hide_closed_list: false,
......@@ -190,9 +189,7 @@ export default {
issueBoardScopeMutationVariables() {
return {
weight: this.board.weight,
assigneeId: this.board.assignee?.id
? convertToGraphQLId(TYPE_USER, this.board.assignee.id)
: null,
assigneeId: this.board.assignee?.id || null,
milestoneId:
this.board.milestone?.id || this.board.milestone?.id === 0
? convertToGraphQLId(TYPE_MILESTONE, this.board.milestone.id)
......@@ -306,6 +303,11 @@ export default {
}
});
},
setAssignee(assigneeId) {
this.board.assignee = {
id: assigneeId,
};
},
},
};
</script>
......@@ -373,6 +375,7 @@ export default {
:weights="weights"
@set-iteration="setIteration"
@set-board-labels="setBoardLabels"
@set-assignee="setAssignee"
/>
</form>
</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 {
<assignee-select
v-if="isIssueBoard"
:board="board"
:selected="board.assignee"
:can-edit="canAdminBoard"
:project-id="projectId"
:group-id="groupId"
any-user-text="Any assignee"
field-name="assignee_id"
label="Assignee"
placeholder-text="Select assignee"
wrapper-class="assignee"
@set-assignee="$emit('set-assignee', $event)"
/>
<!-- eslint-disable vue/no-mutating-props -->
......
......@@ -229,7 +229,7 @@ RSpec.describe 'Scoped issue boards', :js do
edit_board.click
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)
end
......@@ -564,7 +564,7 @@ RSpec.describe 'Scoped issue boards', :js do
click_button value
end
else
click_link value
click_on value
end
end
end
......
import MockAdapter from 'axios-mock-adapter';
import Vue from 'vue';
import { GlButton, GlDropdown } from '@gitlab/ui';
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 { 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() {
return vm.$el.querySelector('.value').innerText.trim();
}
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
function activeDropdownItem(index) {
const items = document.querySelectorAll('.is-active');
if (!items[index]) return '';
return items[index].innerText.trim();
}
import { boardObj } from 'jest/boards/mock_data';
import { projectMembersResponse, groupMembersResponse, mockUser2 } from 'jest/sidebar/mock_data';
const assignee = {
id: 1,
name: 'first assignee',
};
import defaultStore from '~/boards/stores';
import searchGroupUsersQuery from '~/graphql_shared/queries/group_users_search.query.graphql';
import searchProjectUsersQuery from '~/graphql_shared/queries/users_search.query.graphql';
import { ASSIGNEES_DEBOUNCE_DELAY } from '~/sidebar/constants';
const assignee2 = {
id: 2,
name: 'second assignee',
};
const localVue = createLocalVue();
localVue.use(VueApollo);
describe('Assignee select component', () => {
beforeEach((done) => {
setFixtures('<div class="test-container"></div>');
boardsStore.create();
// eslint-disable-next-line no-new
new IssuableContext();
const Component = Vue.extend(AssigneeSelect);
vm = new Component({
let wrapper;
let fakeApollo;
let store;
const selectedText = () => wrapper.find('[data-testid="selected-assignee"]').text();
const findEditButton = () => wrapper.findComponent(GlButton);
const findDropdown = () => wrapper.findComponent(GlDropdown);
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: {
board: boardObj,
assigneePath: '/test/issue-boards/assignees.json',
canEdit: true,
label: 'Assignee',
selected: {},
fieldName: 'assignee_id',
anyUserText: 'Any assignee',
...props,
},
provide: {
fullPath: 'gitlab-org',
},
}).$mount('.test-container');
setImmediate(done);
});
describe('canEdit', () => {
it('hides Edit button', (done) => {
vm.canEdit = false;
Vue.nextTick(() => {
expect(vm.$el.querySelector('.edit-link')).toBeFalsy();
done();
});
});
// We need to mock out `showDropdown` which
// invokes `show` method of BDropdown used inside GlDropdown.
jest.spyOn(wrapper.vm, 'showDropdown').mockImplementation();
};
it('shows Edit button if true', (done) => {
vm.canEdit = true;
Vue.nextTick(() => {
expect(vm.$el.querySelector('.edit-link')).toBeTruthy();
done();
});
beforeEach(() => {
createStore({ isProjectBoard: true });
createComponent();
});
afterEach(() => {
wrapper.destroy();
fakeApollo = null;
store = null;
});
describe('selected value', () => {
describe('when not editing', () => {
it('defaults to Any Assignee', () => {
expect(selectedText()).toContain('Any assignee');
});
it('shows selected assignee', (done) => {
vm.selected = assignee;
Vue.nextTick(() => {
expect(selectedText()).toContain('first assignee');
done();
it('skips the queries and does not render dropdown', () => {
expect(usersQueryHandlerSuccess).not.toHaveBeenCalled();
expect(findDropdown().isVisible()).toBe(false);
});
});
describe('clicking dropdown items', () => {
let mock;
describe('when editing', () => {
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(() => {
mock = new MockAdapter(axios);
mock.onGet('/-/autocomplete/users.json').reply(200, [assignee, assignee2]);
expect(findDropdown().isVisible()).toBe(true);
expect(wrapper.findAll('[data-testid="unselected-user"]')).toHaveLength(3); // 2 users + Any assignee item
});
afterEach(() => {
mock.restore();
});
it('renders selected assignee', async () => {
findEditButton().vm.$emit('click');
await waitForPromises();
jest.advanceTimersByTime(ASSIGNEES_DEBOUNCE_DELAY);
await nextTick();
it('sets assignee', (done) => {
vm.$el.querySelector('.edit-link').click();
wrapper
.findAll('[data-testid="unselected-user"]')
.at(1)
.vm.$emit('click', new Event('click'));
jest.runOnlyPendingTimers();
await waitForPromises();
expect(selectedText()).toContain(mockUser2.username);
});
});
setImmediate(() => {
vm.$el.querySelectorAll('li a')[2].click();
describe('canEdit', () => {
it('hides Edit button', async () => {
wrapper.setProps({ canEdit: false });
await nextTick();
setImmediate(() => {
expect(activeDropdownItem(0)).toEqual('second assignee');
expect(vm.board.assignee).toEqual(assignee2);
done();
});
expect(findEditButton().exists()).toBe(false);
});
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 { nextTick } from 'vue';
import { mount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import BoardScope from 'ee/boards/components/board_scope.vue';
import { useMockIntersectionObserver } from 'helpers/mock_dom_observer';
import { TEST_HOST } from 'helpers/test_constants';
import LabelsSelect from '~/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue';
const localVue = createLocalVue();
localVue.use(Vuex);
Vue.use(Vuex);
describe('BoardScope', () => {
let wrapper;
......@@ -25,7 +24,6 @@ describe('BoardScope', () => {
function mountComponent() {
wrapper = mount(BoardScope, {
localVue,
store,
propsData: {
collapseScope: false,
......@@ -37,6 +35,9 @@ describe('BoardScope', () => {
labelsPath: `${TEST_HOST}/labels`,
labelsWebUrl: `${TEST_HOST}/-/labels`,
},
stubs: {
AssigneeSelect: true,
},
});
}
......
......@@ -3912,9 +3912,6 @@ msgstr ""
msgid "Any namespace"
msgstr ""
msgid "Any user"
msgstr ""
msgid "App ID"
msgstr ""
......@@ -5299,6 +5296,24 @@ msgstr ""
msgid "BoardNewIssue|Select a project"
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"
msgstr ""
......@@ -29585,9 +29600,6 @@ msgstr ""
msgid "Select type"
msgstr ""
msgid "Select user"
msgstr ""
msgid "Selected"
msgstr ""
......
......@@ -415,7 +415,7 @@ const mockUser1 = {
status: null,
};
const mockUser2 = {
export const mockUser2 = {
id: 'gid://gitlab/User/4',
avatarUrl: '/avatar2',
name: 'rookie',
......@@ -452,9 +452,40 @@ export const projectMembersResponse = {
null,
null,
// Remove duplicated entry https://gitlab.com/gitlab-org/gitlab/-/issues/327822
mockUser1,
mockUser1,
mockUser2,
{ user: mockUser1 },
{ user: mockUser1 },
{ 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: {
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