diff options
Diffstat (limited to 'app/assets/javascripts/work_items/components/work_item_assignees.vue')
-rw-r--r-- | app/assets/javascripts/work_items/components/work_item_assignees.vue | 84 |
1 files changed, 65 insertions, 19 deletions
diff --git a/app/assets/javascripts/work_items/components/work_item_assignees.vue b/app/assets/javascripts/work_items/components/work_item_assignees.vue index 7342f215b5e..4585426edaa 100644 --- a/app/assets/javascripts/work_items/components/work_item_assignees.vue +++ b/app/assets/javascripts/work_items/components/work_item_assignees.vue @@ -8,6 +8,7 @@ import { GlButton, GlDropdownItem, GlDropdownDivider, + GlIntersectionObserver, } from '@gitlab/ui'; import { debounce } from 'lodash'; import { getIdFromGraphQLId } from '~/graphql_shared/utils'; @@ -19,7 +20,7 @@ import Tracking from '~/tracking'; import SidebarParticipant from '~/sidebar/components/assignees/sidebar_participant.vue'; import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants'; import updateWorkItemMutation from '../graphql/update_work_item.mutation.graphql'; -import { i18n, TRACKING_CATEGORY_SHOW } from '../constants'; +import { i18n, TRACKING_CATEGORY_SHOW, DEFAULT_PAGE_SIZE_ASSIGNEES } from '../constants'; function isTokenSelectorElement(el) { return ( @@ -50,9 +51,9 @@ export default { InviteMembersTrigger, GlDropdownItem, GlDropdownDivider, + GlIntersectionObserver, }, mixins: [Tracking.mixin()], - inject: ['fullPath'], props: { workItemId: { type: String, @@ -80,6 +81,10 @@ export default { required: false, default: false, }, + fullPath: { + type: String, + required: true, + }, }, data() { return { @@ -87,12 +92,15 @@ export default { searchStarted: false, localAssignees: this.assignees.map(addClass), searchKey: '', - searchUsers: [], + users: { + nodes: [], + }, currentUser: null, + isLoadingMore: false, }; }, apollo: { - searchUsers: { + users: { query() { return userSearchQuery; }, @@ -100,13 +108,14 @@ export default { return { fullPath: this.fullPath, search: this.searchKey, + first: DEFAULT_PAGE_SIZE_ASSIGNEES, }; }, skip() { return !this.searchStarted; }, update(data) { - return data.workspace?.users?.nodes.map((node) => addClass({ ...node, ...node.user })); + return data.workspace?.users; }, error() { this.$emit('error', i18n.fetchError); @@ -117,6 +126,12 @@ export default { }, }, computed: { + searchUsers() { + return this.users.nodes.map((node) => addClass({ ...node, ...node.user })); + }, + pageInfo() { + return this.users.pageInfo; + }, tracking() { return { category: TRACKING_CATEGORY_SHOW, @@ -131,7 +146,7 @@ export default { return !this.isEditing ? 'gl-shadow-none!' : ''; }, isLoadingUsers() { - return this.$apollo.queries.searchUsers.loading; + return this.$apollo.queries.users.loading; }, assigneeText() { return n__('WorkItem|Assignee', 'WorkItem|Assignees', this.localAssignees.length); @@ -159,6 +174,12 @@ export default { assigneeIds() { return this.localAssignees.map(({ id }) => id); }, + hasNextPage() { + return this.pageInfo?.hasNextPage; + }, + showIntersectionSkeletonLoader() { + return this.isLoadingMore && this.dropdownItems.length; + }, }, watch: { assignees: { @@ -221,6 +242,16 @@ export default { this.isEditing = true; this.searchStarted = true; }, + async fetchMoreAssignees() { + this.isLoadingMore = true; + await this.$apollo.queries.users.fetchMore({ + variables: { + after: this.pageInfo.endCursor, + first: DEFAULT_PAGE_SIZE_ASSIGNEES, + }, + }); + this.isLoadingMore = false; + }, async focusTokenSelector() { this.handleFocus(); await this.$nextTick(); @@ -263,7 +294,7 @@ export default { </script> <template> - <div class="form-row gl-mb-5 work-item-assignees gl-relative"> + <div class="form-row gl-mb-5 work-item-assignees gl-relative gl-flex-nowrap"> <span class="gl-font-weight-bold col-lg-2 col-3 gl-pt-2 min-w-fit-content gl-overflow-wrap-break" data-testid="assignees-title" @@ -275,7 +306,7 @@ export default { :container-class="containerClass" :class="{ 'gl-hover-border-gray-200': canUpdate }" :dropdown-items="dropdownItems" - :loading="isLoadingUsers" + :loading="isLoadingUsers && !isLoadingMore" :view-only="!canUpdate" :allow-clear-all="isEditing" class="assignees-selector gl-flex-grow-1 gl-border gl-border-white gl-rounded-base col-9 gl-align-self-start gl-px-0! gl-mx-2" @@ -326,17 +357,32 @@ export default { <rect width="280" height="20" x="10" y="130" rx="4" /> </gl-skeleton-loader> </template> - <template v-if="canInviteMembers" #dropdown-footer> - <gl-dropdown-divider /> - <gl-dropdown-item @click="closeDropdown"> - <invite-members-trigger - :display-text="__('Invite members')" - trigger-element="side-nav" - icon="plus" - trigger-source="work-item-assignees-dropdown" - classes="gl-display-block gl-text-body! gl-hover-text-decoration-none gl-pb-2" - /> - </gl-dropdown-item> + <template #dropdown-footer> + <gl-intersection-observer + v-if="hasNextPage && !isLoadingUsers" + @appear="fetchMoreAssignees" + /> + <gl-skeleton-loader + v-if="showIntersectionSkeletonLoader" + :height="100" + data-testid="next-page-loading" + class="gl-text-center gl-py-3" + > + <rect width="380" height="20" x="10" y="15" rx="4" /> + <rect width="280" height="20" x="10" y="50" rx="4" /> + </gl-skeleton-loader> + <div v-if="canInviteMembers"> + <gl-dropdown-divider /> + <gl-dropdown-item @click="closeDropdown"> + <invite-members-trigger + :display-text="__('Invite members')" + trigger-element="side-nav" + icon="plus" + trigger-source="work-item-assignees-dropdown" + classes="gl-display-block gl-text-body! gl-hover-text-decoration-none gl-pb-2" + /> + </gl-dropdown-item> + </div> </template> </gl-token-selector> </div> |