diff options
Diffstat (limited to 'app/assets/javascripts/sidebar/components/assignees/sidebar_assignees_widget.vue')
-rw-r--r-- | app/assets/javascripts/sidebar/components/assignees/sidebar_assignees_widget.vue | 287 |
1 files changed, 47 insertions, 240 deletions
diff --git a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees_widget.vue b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees_widget.vue index 78cac989850..932be7addc0 100644 --- a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees_widget.vue +++ b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees_widget.vue @@ -1,19 +1,17 @@ <script> -import { GlDropdownItem, GlDropdownDivider, GlSearchBoxByType, GlLoadingIcon } from '@gitlab/ui'; +import { GlDropdownItem } from '@gitlab/ui'; import { cloneDeep } from 'lodash'; import Vue from 'vue'; import createFlash from '~/flash'; -import searchUsers from '~/graphql_shared/queries/users_search.query.graphql'; import { IssuableType } from '~/issue_show/constants'; import { __, n__ } from '~/locale'; import SidebarAssigneesRealtime from '~/sidebar/components/assignees/assignees_realtime.vue'; import IssuableAssignees from '~/sidebar/components/assignees/issuable_assignees.vue'; import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue'; -import { assigneesQueries, ASSIGNEES_DEBOUNCE_DELAY } from '~/sidebar/constants'; -import MultiSelectDropdown from '~/vue_shared/components/sidebar/multiselect_dropdown.vue'; +import { assigneesQueries } from '~/sidebar/constants'; +import UserSelect from '~/vue_shared/components/user_select/user_select.vue'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import SidebarInviteMembers from './sidebar_invite_members.vue'; -import SidebarParticipant from './sidebar_participant.vue'; export const assigneesWidget = Vue.observable({ updateAssignees: null, @@ -33,23 +31,16 @@ export default { components: { SidebarEditableItem, IssuableAssignees, - MultiSelectDropdown, GlDropdownItem, - GlDropdownDivider, - GlSearchBoxByType, - GlLoadingIcon, SidebarInviteMembers, - SidebarParticipant, SidebarAssigneesRealtime, + UserSelect, }, mixins: [glFeatureFlagsMixin()], inject: { directlyInviteMembers: { default: false, }, - indirectlyInviteMembers: { - default: false, - }, }, props: { iid: { @@ -73,20 +64,21 @@ export default { return [IssuableType.Issue, IssuableType.MergeRequest].includes(value); }, }, - multipleAssignees: { - type: Boolean, + issuableId: { + type: Number, required: false, - default: true, + default: null, + }, + allowMultipleAssignees: { + type: Boolean, + required: true, }, }, data() { return { - search: '', issuable: {}, - searchUsers: [], selected: [], isSettingAssignees: false, - isSearching: false, isDirty: false, }; }, @@ -104,51 +96,13 @@ export default { result({ data }) { const issuable = data.workspace?.issuable; if (issuable) { - this.selected = this.moveCurrentUserToStart(cloneDeep(issuable.assignees.nodes)); + this.selected = cloneDeep(issuable.assignees.nodes); } }, error() { createFlash({ message: __('An error occurred while fetching participants.') }); }, }, - searchUsers: { - query: searchUsers, - variables() { - return { - fullPath: this.fullPath, - search: this.search, - }; - }, - update(data) { - const searchResults = data.workspace?.users?.nodes.map(({ user }) => user) || []; - const filteredParticipants = this.participants.filter( - (user) => - user.name.toLowerCase().includes(this.search.toLowerCase()) || - user.username.toLowerCase().includes(this.search.toLowerCase()), - ); - const mergedSearchResults = searchResults.reduce((acc, current) => { - // Some users are duplicated in the query result: - // https://gitlab.com/gitlab-org/gitlab/-/issues/327822 - if (!acc.some((user) => current.username === user.username)) { - acc.push(current); - } - return acc; - }, filteredParticipants); - - return mergedSearchResults; - }, - debounce: ASSIGNEES_DEBOUNCE_DELAY, - skip() { - return this.isSearchEmpty; - }, - error() { - createFlash({ message: __('An error occurred while searching users.') }); - this.isSearching = false; - }, - result() { - this.isSearching = false; - }, - }, }, computed: { shouldEnableRealtime() { @@ -167,13 +121,6 @@ export default { : this.issuable?.assignees?.nodes; return currentAssignees || []; }, - participants() { - const users = - this.isSearchEmpty || this.isSearching - ? this.issuable?.participants?.nodes - : this.searchUsers; - return this.moveCurrentUserToStart(users); - }, assigneeText() { const items = this.$apollo.queries.issuable.loading ? this.initialAssignees : this.selected; if (!items) { @@ -181,28 +128,8 @@ export default { } return n__('Assignee', '%d Assignees', items.length); }, - selectedFiltered() { - if (this.isSearchEmpty || this.isSearching) { - return this.selected; - } - - const foundUsernames = this.searchUsers.map(({ username }) => username); - return this.selected.filter(({ username }) => foundUsernames.includes(username)); - }, - unselectedFiltered() { - return ( - this.participants?.filter(({ username }) => !this.selectedUserNames.includes(username)) || - [] - ); - }, - selectedIsEmpty() { - return this.selectedFiltered.length === 0; - }, - selectedUserNames() { - return this.selected.map(({ username }) => username); - }, - isSearchEmpty() { - return this.search === ''; + isAssigneesLoading() { + return !this.initialAssignees && this.$apollo.queries.issuable.loading; }, currentUser() { return { @@ -211,35 +138,9 @@ export default { avatarUrl: gon?.current_user_avatar_url, }; }, - isAssigneesLoading() { - return !this.initialAssignees && this.$apollo.queries.issuable.loading; - }, - isCurrentUserInParticipants() { - const isCurrentUser = (user) => user.username === this.currentUser.username; - return this.selected.some(isCurrentUser) || this.participants.some(isCurrentUser); - }, - noUsersFound() { - return !this.isSearchEmpty && this.searchUsers.length === 0; - }, signedIn() { return this.currentUser.username !== undefined; }, - showCurrentUser() { - return ( - this.signedIn && - !this.isCurrentUserInParticipants && - (this.isSearchEmpty || this.isSearching) - ); - }, - }, - watch: { - // We need to add this watcher to track the moment when user is alredy typing - // but query is still not started due to debounce - search(newVal) { - if (newVal) { - this.isSearching = true; - } - }, }, created() { assigneesWidget.updateAssignees = this.updateAssignees; @@ -269,59 +170,15 @@ export default { this.isSettingAssignees = false; }); }, - selectAssignee(name) { - this.isDirty = true; - - if (!this.multipleAssignees) { - this.selected = name ? [name] : []; - this.collapseWidget(); - return; - } - if (name === undefined) { - this.clearSelected(); - return; - } - this.selected = this.selected.concat(name); - }, - unselect(name) { - this.selected = this.selected.filter((user) => user.username !== name); - this.isDirty = true; - - if (!this.multipleAssignees) { - this.collapseWidget(); - } - }, assignSelf() { - this.updateAssignees(this.currentUser.username); - }, - clearSelected() { - this.selected = []; + this.updateAssignees([this.currentUser.username]); }, saveAssignees() { - this.isDirty = false; - this.updateAssignees(this.selectedUserNames); - this.$el.dispatchEvent(hideDropdownEvent); - }, - isChecked(id) { - return this.selectedUserNames.includes(id); - }, - async focusSearch() { - await this.$nextTick(); - this.$refs.search.focusInput(); - }, - moveCurrentUserToStart(users) { - if (!users) { - return []; - } - const usersCopy = [...users]; - const currentUser = usersCopy.find((user) => user.username === this.currentUser.username); - - if (currentUser) { - const index = usersCopy.indexOf(currentUser); - usersCopy.splice(0, 0, usersCopy.splice(index, 1)[0]); + if (this.isDirty) { + this.isDirty = false; + this.updateAssignees(this.selected.map(({ username }) => username)); } - - return usersCopy; + this.$el.dispatchEvent(hideDropdownEvent); }, collapseWidget() { this.$refs.toggle.collapse(); @@ -329,8 +186,17 @@ export default { expandWidget() { this.$refs.toggle.expand(); }, - showDivider(list) { - return list.length > 0 && this.isSearchEmpty; + focusSearch() { + this.$refs.userSelect.focusSearch(); + }, + showError() { + createFlash({ message: __('An error occurred while fetching participants.') }); + }, + setDirtyState() { + this.isDirty = true; + if (!this.allowMultipleAssignees) { + this.collapseWidget(); + } }, }, }; @@ -340,9 +206,9 @@ export default { <div data-testid="assignees-widget"> <sidebar-assignees-realtime v-if="shouldEnableRealtime" - :project-path="fullPath" - :issuable-iid="iid" :issuable-type="issuableType" + :issuable-id="issuableId" + :query-variables="queryVariables" /> <sidebar-editable-item ref="toggle" @@ -363,86 +229,27 @@ export default { @expand-widget="expandWidget" /> </template> - <template #default> - <multi-select-dropdown - class="gl-w-full dropdown-menu-user" + <user-select + ref="userSelect" + v-model="selected" :text="$options.i18n.assignees" :header-text="$options.i18n.assignTo" + :iid="iid" + :full-path="fullPath" + :allow-multiple-assignees="allowMultipleAssignees" + :current-user="currentUser" + :issuable-type="issuableType" + class="gl-w-full dropdown-menu-user" @toggle="collapseWidget" + @error="showError" + @input="setDirtyState" > - <template #search> - <gl-search-box-by-type - ref="search" - v-model.trim="search" - class="js-dropdown-input-field" - /> - </template> - <template #items> - <gl-loading-icon - v-if="$apollo.queries.searchUsers.loading || $apollo.queries.issuable.loading" - data-testid="loading-participants" - size="lg" - /> - <template v-else> - <template v-if="isSearchEmpty || isSearching"> - <gl-dropdown-item - :is-checked="selectedIsEmpty" - :is-check-centered="true" - data-testid="unassign" - @click="selectAssignee()" - > - <span - :class="selectedIsEmpty ? 'gl-pl-0' : 'gl-pl-6'" - class="gl-font-weight-bold" - >{{ $options.i18n.unassigned }}</span - ></gl-dropdown-item - > - </template> - <gl-dropdown-divider v-if="showDivider(selectedFiltered)" /> - <gl-dropdown-item - v-for="item in selectedFiltered" - :key="item.id" - :is-checked="isChecked(item.username)" - :is-check-centered="true" - data-testid="selected-participant" - @click.stop="unselect(item.username)" - > - <sidebar-participant :user="item" /> - </gl-dropdown-item> - <template v-if="showCurrentUser"> - <gl-dropdown-divider /> - <gl-dropdown-item - data-testid="current-user" - @click.stop="selectAssignee(currentUser)" - > - <sidebar-participant :user="currentUser" class="gl-pl-6!" /> - </gl-dropdown-item> - </template> - <gl-dropdown-divider v-if="showDivider(unselectedFiltered)" /> - <gl-dropdown-item - v-for="unselectedUser in unselectedFiltered" - :key="unselectedUser.id" - data-testid="unselected-participant" - @click="selectAssignee(unselectedUser)" - > - <sidebar-participant :user="unselectedUser" class="gl-pl-6!" /> - </gl-dropdown-item> - <gl-dropdown-item - v-if="noUsersFound && !isSearching" - data-testid="empty-results" - class="gl-pl-6!" - > - {{ __('No matching results') }} - </gl-dropdown-item> - </template> - </template> <template #footer> - <gl-dropdown-item> - <sidebar-invite-members v-if="directlyInviteMembers || indirectlyInviteMembers" /> - </gl-dropdown-item> - </template> - </multi-select-dropdown> + <gl-dropdown-item v-if="directlyInviteMembers"> + <sidebar-invite-members /> + </gl-dropdown-item> </template + ></user-select> </template> </sidebar-editable-item> </div> |