diff --git a/app/assets/javascripts/work_items/components/shared/work_item_sidebar_dropdown_widget_with_edit.vue b/app/assets/javascripts/work_items/components/shared/work_item_sidebar_dropdown_widget_with_edit.vue index 21512ba60669d39366040b532b166caf631e3c01..d88fffd89c71ebb8666605d6dd084a20e8315aa2 100644 --- a/app/assets/javascripts/work_items/components/shared/work_item_sidebar_dropdown_widget_with_edit.vue +++ b/app/assets/javascripts/work_items/components/shared/work_item_sidebar_dropdown_widget_with_edit.vue @@ -93,6 +93,7 @@ export default { return { isEditing: false, localSelectedItem: this.itemValue, + isDirty: false, }; }, computed: { @@ -111,6 +112,7 @@ export default { handler(newVal) { if (!this.isEditing) { this.localSelectedItem = newVal; + this.isDirty = false; } }, }, @@ -127,6 +129,7 @@ export default { if (!this.multiSelect) { this.$emit('updateValue', item); } else { + this.isDirty = true; this.$emit('updateSelected', this.localSelectedItem); } }, @@ -135,7 +138,7 @@ export default { }, onListboxHide() { this.isEditing = false; - if (this.multiSelect) { + if (this.multiSelect && this.isDirty) { this.$emit('updateValue', this.localSelectedItem); } }, diff --git a/app/assets/javascripts/work_items/components/work_item_assignees_with_edit.vue b/app/assets/javascripts/work_items/components/work_item_assignees_with_edit.vue index 29c7e0032ef44db568030159c8e66bc664510180..670661a324808d032f54cf5b4e2c2f151c599946 100644 --- a/app/assets/javascripts/work_items/components/work_item_assignees_with_edit.vue +++ b/app/assets/javascripts/work_items/components/work_item_assignees_with_edit.vue @@ -1,6 +1,7 @@ <script> import { GlButton } from '@gitlab/ui'; import { unionBy } from 'lodash'; +import { sortNameAlphabetically } from '~/work_items/utils'; import currentUserQuery from '~/graphql_shared/queries/current_user.query.graphql'; import groupUsersSearchQuery from '~/graphql_shared/queries/group_users_search.query.graphql'; import usersSearchQuery from '~/graphql_shared/queries/users_search.query.graphql'; @@ -142,7 +143,11 @@ export default { return unionBy(this.assignees, this.searchUsers, 'id'); }, localAssignees() { - return this.filteredAssignees.filter(({ id }) => this.localAssigneeIds.includes(id)) || []; + return ( + this.filteredAssignees + .filter(({ id }) => this.localAssigneeIds.includes(id)) + .sort(sortNameAlphabetically) || [] + ); }, }, watch: { @@ -190,6 +195,8 @@ export default { this.throwUpdateError(); } finally { this.updateInProgress = false; + this.searchKey = ''; + this.searchStarted = false; } }, setLocalAssigneeIdsOnEvent(assignees) { diff --git a/app/assets/javascripts/work_items/utils.js b/app/assets/javascripts/work_items/utils.js index 633eef346e8c9ab73773594d012ac23d3a8e8faf..dd690e59f9fd8024ada40bc554981428163fc529 100644 --- a/app/assets/javascripts/work_items/utils.js +++ b/app/assets/javascripts/work_items/utils.js @@ -69,3 +69,7 @@ export const isReference = (input) => { return /^([\w-]+(?:\/[\w-]+)*)?[#&](\d+)$/.test(input); }; + +export const sortNameAlphabetically = (a, b) => { + return a.name.toLowerCase().localeCompare(b.name.toLowerCase()); +}; diff --git a/spec/frontend/work_items/components/work_item_assignees_with_edit_spec.js b/spec/frontend/work_items/components/work_item_assignees_with_edit_spec.js index 35062a6ebf7367a073f91867418b1f17b2e2dfb8..c7f4643cfb8af1c87ac35b70724e1913dc2ac347 100644 --- a/spec/frontend/work_items/components/work_item_assignees_with_edit_spec.js +++ b/spec/frontend/work_items/components/work_item_assignees_with_edit_spec.js @@ -1,14 +1,16 @@ import Vue, { nextTick } from 'vue'; import VueApollo from 'vue-apollo'; +import { sortNameAlphabetically } from '~/work_items/utils'; import WorkItemAssignees from '~/work_items/components/work_item_assignees_with_edit.vue'; import WorkItemSidebarDropdownWidgetWithEdit from '~/work_items/components/shared/work_item_sidebar_dropdown_widget_with_edit.vue'; import groupUsersSearchQuery from '~/graphql_shared/queries/group_users_search.query.graphql'; import usersSearchQuery from '~/graphql_shared/queries/users_search.query.graphql'; import currentUserQuery from '~/graphql_shared/queries/current_user.query.graphql'; import InviteMembersTrigger from '~/invite_members/components/invite_members_trigger.vue'; +import UncollapsedAssigneeList from '~/sidebar/components/assignees/uncollapsed_assignee_list.vue'; import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql'; import createMockApollo from 'helpers/mock_apollo_helper'; -import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import { shallowMountExtended, mountExtended } from 'helpers/vue_test_utils_helper'; import { mockTracking } from 'helpers/tracking_helper'; import waitForPromises from 'helpers/wait_for_promises'; import { @@ -33,6 +35,7 @@ describe('WorkItemAssigneesWithEdit component', () => { const findAssignSelfButton = () => wrapper.findByTestId('assign-self'); const findSidebarDropdownWidget = () => wrapper.findComponent(WorkItemSidebarDropdownWidgetWithEdit); + const findAssigneeList = () => wrapper.findComponent(UncollapsedAssigneeList); const successSearchQueryHandler = jest .fn() @@ -59,6 +62,7 @@ describe('WorkItemAssigneesWithEdit component', () => { }; const createComponent = ({ + mountFn = shallowMountExtended, assignees = mockAssignees, searchQueryHandler = successSearchQueryHandler, currentUserQueryHandler = successCurrentUserQueryHandler, @@ -73,7 +77,7 @@ describe('WorkItemAssigneesWithEdit component', () => { [updateWorkItemMutation, successUpdateWorkItemMutationHandler], ]); - wrapper = shallowMountExtended(WorkItemAssignees, { + wrapper = mountFn(WorkItemAssignees, { provide: { isGroup: false, }, @@ -246,6 +250,19 @@ describe('WorkItemAssigneesWithEdit component', () => { }); }); + describe('sorting', () => { + it('always sorts assignees based on alphabetical order on the frontend', async () => { + createComponent({ mountFn: mountExtended }); + await waitForPromises(); + + expect(findAssigneeList().exists()).toBe(true); + expect(findAssigneeList().props('users')).toHaveLength(mockAssignees.length); + expect(findAssigneeList().props('users')).toStrictEqual( + mockAssignees.sort(sortNameAlphabetically), + ); + }); + }); + describe('invite members', () => { it('does not render `Invite members` link if user has no permission to invite members', () => { createComponent();