diff --git a/app/assets/javascripts/boards/components/board_form.vue b/app/assets/javascripts/boards/components/board_form.vue index 386ed6bd0a1945e0eddf2d4a10d25441d499821d..d7aea7b7290d206cd75f5e3253131a4bbb890f78 100644 --- a/app/assets/javascripts/boards/components/board_form.vue +++ b/app/assets/javascripts/boards/components/board_form.vue @@ -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> diff --git a/app/assets/javascripts/graphql_shared/queries/group_users_search.query.graphql b/app/assets/javascripts/graphql_shared/queries/group_users_search.query.graphql new file mode 100644 index 0000000000000000000000000000000000000000..e345fe97281bee5b14c165a4f44a3f00f9a9dcea --- /dev/null +++ b/app/assets/javascripts/graphql_shared/queries/group_users_search.query.graphql @@ -0,0 +1,15 @@ +#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 + } + } + } + } +} diff --git a/ee/app/assets/javascripts/boards/components/assignee_select.vue b/ee/app/assets/javascripts/boards/components/assignee_select.vue index 21a40e94e8b598b298552032b9d42e634fe6af68..40adb8673153e6d42d19b6adb15809ed61b92b7c 100644 --- a/ee/app/assets/javascripts/boards/components/assignee_select.vue +++ b/ee/app/assets/javascripts/boards/components/assignee_select.vue @@ -1,21 +1,34 @@ <script> -import { GlLoadingIcon, GlIcon } from '@gitlab/ui'; -import { __ } from '~/locale'; -import UsersSelect from '~/users_select'; +import { + GlButton, + GlDropdown, + GlDropdownForm, + GlDropdownDivider, + GlDropdownItem, + GlSearchBoxByType, + GlLoadingIcon, +} from '@gitlab/ui'; +import { isEmpty } from 'lodash'; +import { mapActions, mapGetters } from 'vuex'; +import searchGroupUsers from '~/graphql_shared/queries/group_users_search.query.graphql'; +import searchProjectUsers from '~/graphql_shared/queries/users_search.query.graphql'; +import { s__ } from '~/locale'; +import { ASSIGNEES_DEBOUNCE_DELAY } from '~/sidebar/constants'; import UserAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue'; export default { components: { UserAvatarImage, + GlButton, + GlDropdown, + GlDropdownForm, + GlDropdownDivider, + GlDropdownItem, + GlSearchBoxByType, GlLoadingIcon, - GlIcon, }, + inject: ['fullPath'], props: { - anyUserText: { - type: String, - required: false, - default: __('Any user'), - }, board: { type: Object, required: true, @@ -25,150 +38,200 @@ export default { required: false, default: false, }, - fieldName: { - type: String, - required: true, - }, groupId: { type: Number, required: false, default: 0, }, - label: { - type: String, - required: true, - }, - placeholderText: { - type: String, - required: false, - default: __('Select user'), - }, projectId: { type: Number, required: false, default: 0, }, - selected: { - type: Object, - required: false, - default: () => null, - }, - wrapperClass: { - type: String, - required: false, - default: '', + }, + data() { + return { + search: '', + searchUsers: [], + selected: this.board.assignee, + isEditing: false, + isDropdownShowing: false, + }; + }, + apollo: { + searchUsers: { + query() { + return this.isProjectBoard ? searchProjectUsers : searchGroupUsers; + }, + variables() { + return { + fullPath: this.fullPath, + search: this.search, + first: 20, + }; + }, + skip() { + return !this.isEditing; + }, + update(data) { + // TODO Remove null filter (BE fix required) + // https://gitlab.com/gitlab-org/gitlab/-/issues/329750 + return data.workspace?.users?.nodes.filter((x) => x?.user).map(({ user }) => user) || []; + }, + debounce: ASSIGNEES_DEBOUNCE_DELAY, + error() { + this.setError({ message: this.$options.i18n.errorSearchingUsers }); + }, }, }, computed: { - hasValue() { - return this.selected && this.selected.id > 0; + ...mapGetters(['isProjectBoard']), + isLoading() { + return this.$apollo.queries.searchUsers.loading; }, - selectedId() { - return this.selected ? this.selected.id : null; + isSearchEmpty() { + return this.search === '' && !this.isLoading; }, - }, - watch: { - selected() { - this.initSelect(); + selectedIsEmpty() { + return isEmpty(this.selected); + }, + noUsersFound() { + return !this.isSearchEmpty && this.users.length === 0; + }, + users() { + const filteredUsers = this.searchUsers.filter( + (user) => user.name.includes(this.search) || user.username.includes(this.search), + ); + + // TODO this de-duplication is temporary (BE fix required) + // https://gitlab.com/gitlab-org/gitlab/-/issues/327822 + return filteredUsers + .concat(this.searchUsers) + .reduce( + (acc, current) => (acc.some((user) => current.id === user.id) ? acc : [...acc, current]), + [], + ); }, - }, - mounted() { - this.initSelect(); }, methods: { - initSelect() { - this.userDropdown = new UsersSelect(null, this.$refs.dropdown, { - handleClick: this.selectUser, - }); - }, - selectUser(user, isMarking) { - let assignee = user; - if (!isMarking) { - // correctly select "unassigned" in Assignee dropdown - assignee = { - id: undefined, - }; + ...mapActions(['setError']), + selectAssignee(user) { + this.selected = user; + this.toggleEdit(); + this.$emit('set-assignee', user?.id || null); + }, + toggleEdit() { + if (!this.isEditing && !this.isDropdownShowing) { + this.isEditing = true; + this.showDropdown(); + } else { + this.isEditing = false; + this.isDropdownShowing = false; } - // eslint-disable-next-line vue/no-mutating-props - this.board.assignee_id = assignee.id; - // eslint-disable-next-line vue/no-mutating-props - this.board.assignee = assignee; + }, + isSelected(user) { + return this.selected?.username === user.username; + }, + showDropdown() { + this.$refs.editDropdown.show(); + this.isDropdownShowing = true; + }, + setFocus() { + this.$refs.search.focusInput(); + }, + hideDropdown() { + this.isEditing = false; }, }, + i18n: { + label: s__('BoardScope|Assignee'), + anyAssignee: s__('BoardScope|Any assignee'), + selectAssignee: s__('BoardScope|Select assignee'), + noMatchingResults: s__('BoardScope|No matching results'), + errorSearchingUsers: s__( + 'BoardScope|An error occurred while searching for users, please try again.', + ), + edit: s__('BoardScope|Edit'), + }, }; </script> <template> - <div :class="wrapperClass" class="block"> + <div class="block assignee"> <div class="title gl-mb-3"> - {{ label }} - <button v-if="canEdit" type="button" class="edit-link btn btn-blank float-right"> - {{ __('Edit') }} - </button> + {{ $options.i18n.label }} + <gl-button + v-if="canEdit" + variant="link" + class="edit-link float-right gl-text-gray-900!" + @click="toggleEdit" + > + {{ $options.i18n.edit }} + </gl-button> </div> - <div class="value"> - <div v-if="hasValue" class="media gl-display-flex gl-align-items-center"> - <div class="align-center"> - <user-avatar-image :img-src="selected.avatar_url" :size="32" /> - </div> - <div class="media-body"> - <div class="bold author">{{ selected.name }}</div> - <div class="username">@{{ selected.username }}</div> + <div v-if="!isEditing" data-testid="selected-assignee"> + <div v-if="!selectedIsEmpty" class="gl-display-flex gl-align-items-center"> + <user-avatar-image :img-src="selected.avatarUrl || selected.avatar_url" :size="32" /> + <div> + <div class="gl-font-weight-bold">{{ selected.name }}</div> + <div>@{{ selected.username }}</div> </div> </div> - <div v-else class="text-secondary">{{ anyUserText }}</div> + <div v-else class="gl-text-gray-500">{{ $options.i18n.anyAssignee }}</div> </div> - <div class="selectbox" style="display: none"> - <div class="dropdown"> - <!-- eslint-disable @gitlab/vue-no-data-toggle --> - <button - ref="dropdown" - :data-field-name="fieldName" - :data-dropdown-title="placeholderText" - :data-any-user="anyUserText" - :data-group-id="groupId" - :data-project-id="projectId" - :data-selected="selectedId" - class="dropdown-menu-toggle wide" - data-toggle="dropdown" - aria-expanded="false" - type="button" - > - <span class="dropdown-toggle-text">{{ placeholderText }}</span> - <gl-icon - name="chevron-down" - class="gl-absolute gl-top-3 gl-right-3 gl-text-gray-500" - :size="16" - /> - </button> - <!-- eslint-enable @gitlab/vue-no-data-toggle --> - - <div - class="dropdown-menu dropdown-select dropdown-menu-paging dropdown-menu-user dropdown-menu-selectable dropdown-menu-author" - > - <div class="dropdown-input"> - <input - autocomplete="off" - class="dropdown-input-field" - :placeholder="__('Search')" - type="search" - /> - <gl-icon - name="search" - class="dropdown-input-search gl-absolute gl-top-3 gl-right-5 gl-text-gray-300 gl-pointer-events-none" - /> - <gl-icon - name="close" - class="dropdown-input-clear js-dropdown-input-clear gl-absolute gl-top-3 gl-right-5 gl-text-gray-500" - /> - </div> - <div class="dropdown-content"></div> - <div class="dropdown-loading"> - <gl-loading-icon size="sm" /> - </div> - </div> - </div> - </div> + <gl-dropdown + v-show="isEditing" + ref="editDropdown" + :text="$options.i18n.selectAssignee" + lazy + menu-class="gl-w-full!" + class="gl-w-full" + @shown="setFocus" + @hide="hideDropdown" + > + <template #header> + <gl-search-box-by-type ref="search" v-model.trim="search" class="js-dropdown-input-field" /> + </template> + <gl-dropdown-form class="gl-relative gl-min-h-7"> + <gl-loading-icon + v-if="isLoading" + size="md" + class="gl-absolute gl-left-0 gl-top-0 gl-right-0" + /> + <template v-else> + <gl-dropdown-item + v-if="isSearchEmpty" + :is-checked="selectedIsEmpty" + :is-check-centered="true" + @click="selectAssignee(null)" + > + <span :class="selectedIsEmpty ? 'gl-pl-0' : 'gl-pl-6'" class="gl-font-weight-bold"> + {{ $options.i18n.anyAssignee }} + </span> + </gl-dropdown-item> + <gl-dropdown-divider /> + <gl-dropdown-item + v-for="user in users" + :key="user.id" + :is-checked="isSelected(user)" + :is-check-centered="true" + :is-check-item="true" + :avatar-url="user.avatar_url || user.avatarUrl" + :secondary-text="user.username" + data-testid="unselected-user" + @click="selectAssignee(user)" + > + {{ user.name }} + </gl-dropdown-item> + <gl-dropdown-item v-if="noUsersFound" class="gl-pl-6!"> + {{ $options.i18n.noMatchingResults }} + </gl-dropdown-item> + </template> + </gl-dropdown-form> + <template #footer> + <slot name="footer"></slot> + </template> + </gl-dropdown> </div> </template> diff --git a/ee/app/assets/javascripts/boards/components/board_scope.vue b/ee/app/assets/javascripts/boards/components/board_scope.vue index be0da9ba3e0aa812a006d067ec1b2786b855bddc..2054cffd323189a86c38409c988036097de3016f 100644 --- a/ee/app/assets/javascripts/boards/components/board_scope.vue +++ b/ee/app/assets/javascripts/boards/components/board_scope.vue @@ -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 --> diff --git a/ee/spec/features/boards/scoped_issue_board_spec.rb b/ee/spec/features/boards/scoped_issue_board_spec.rb index fa1e30b5afa25f73424a19ccee84874a19efdd49..5ba58069afe9f3d413f24da9b66684afdfd3d58a 100644 --- a/ee/spec/features/boards/scoped_issue_board_spec.rb +++ b/ee/spec/features/boards/scoped_issue_board_spec.rb @@ -229,7 +229,7 @@ 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 @@ def click_value(filter, value) click_button value end else - click_link value + click_on value end end end diff --git a/ee/spec/frontend/boards/components/assignee_select_spec.js b/ee/spec/frontend/boards/components/assignee_select_spec.js index 7b1cc05513949e7a935d002b099a5b09120b4c59..a52b96a102f44812c4df6fe8629d15c060b52894 100644 --- a/ee/spec/frontend/boards/components/assignee_select_spec.js +++ b/ee/spec/frontend/boards/components/assignee_select_spec.js @@ -1,117 +1,152 @@ -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; +import createMockApollo from 'helpers/mock_apollo_helper'; +import waitForPromises from 'helpers/wait_for_promises'; -function selectedText() { - return vm.$el.querySelector('.value').innerText.trim(); -} - -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, }, - }).$mount('.test-container'); + provide: { + fullPath: 'gitlab-org', + }, + }); - setImmediate(done); - }); + // We need to mock out `showDropdown` which + // invokes `show` method of BDropdown used inside GlDropdown. + jest.spyOn(wrapper.vm, 'showDropdown').mockImplementation(); + }; - describe('canEdit', () => { - it('hides Edit button', (done) => { - vm.canEdit = false; - Vue.nextTick(() => { - expect(vm.$el.querySelector('.edit-link')).toBeFalsy(); - done(); - }); - }); + beforeEach(() => { + createStore({ isProjectBoard: true }); + createComponent(); + }); - it('shows Edit button if true', (done) => { - vm.canEdit = true; - Vue.nextTick(() => { - 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', () => { 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')); + + await waitForPromises(); + expect(selectedText()).toContain(mockUser2.username); + }); + }); - jest.runOnlyPendingTimers(); + describe('canEdit', () => { + it('hides Edit button', async () => { + wrapper.setProps({ canEdit: false }); + await nextTick(); - setImmediate(() => { - vm.$el.querySelectorAll('li a')[2].click(); + expect(findEditButton().exists()).toBe(false); + }); - setImmediate(() => { - 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(); + }, + ); }); diff --git a/ee/spec/frontend/boards/components/board_scope_spec.js b/ee/spec/frontend/boards/components/board_scope_spec.js index 8b24c43be25901e1d4010ebef11c516be1a2c549..2d095cede5e3316e3ba38de71aba16a1418c6fc6 100644 --- a/ee/spec/frontend/boards/components/board_scope_spec.js +++ b/ee/spec/frontend/boards/components/board_scope_spec.js @@ -1,13 +1,12 @@ -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, + }, }); } diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 94487ec3be1e19b6b46c08f17acb146afc84c12a..a5377bb1749ff7a0b6344c20e4f668e6f792b088 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -3912,9 +3912,6 @@ msgstr "" msgid "Any namespace" msgstr "" -msgid "Any user" -msgstr "" - msgid "App ID" msgstr "" @@ -5284,6 +5281,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 "" @@ -29525,9 +29540,6 @@ msgstr "" msgid "Select type" msgstr "" -msgid "Select user" -msgstr "" - msgid "Selected" msgstr "" diff --git a/spec/frontend/sidebar/mock_data.js b/spec/frontend/sidebar/mock_data.js index 9fab24d75188eae65b8991815bdd5936806c9850..2da007fb549e8a954eed1ad2928f3dc8a9681219 100644 --- a/spec/frontend/sidebar/mock_data.js +++ b/spec/frontend/sidebar/mock_data.js @@ -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',