summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/work_items/components/work_item_assignees.vue
diff options
context:
space:
mode:
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.vue115
1 files changed, 98 insertions, 17 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 4d1c171772e..46920969415 100644
--- a/app/assets/javascripts/work_items/components/work_item_assignees.vue
+++ b/app/assets/javascripts/work_items/components/work_item_assignees.vue
@@ -1,10 +1,23 @@
<script>
-import { GlTokenSelector, GlIcon, GlAvatar, GlLink } from '@gitlab/ui';
+import { GlTokenSelector, GlIcon, GlAvatar, GlLink, GlSkeletonLoader } from '@gitlab/ui';
+import { debounce } from 'lodash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import userSearchQuery from '~/graphql_shared/queries/users_search.query.graphql';
+import { n__ } from '~/locale';
+import SidebarParticipant from '~/sidebar/components/assignees/sidebar_participant.vue';
+import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import localUpdateWorkItemMutation from '../graphql/local_update_work_item.mutation.graphql';
+import { i18n } from '../constants';
-function isClosingIcon(el) {
- return el?.classList.contains('gl-token-close');
+function isTokenSelectorElement(el) {
+ return el?.classList.contains('gl-token-close') || el?.classList.contains('dropdown-item');
+}
+
+function addClass(el) {
+ return {
+ ...el,
+ class: 'gl-bg-transparent',
+ };
}
export default {
@@ -13,7 +26,10 @@ export default {
GlIcon,
GlAvatar,
GlLink,
+ GlSkeletonLoader,
+ SidebarParticipant,
},
+ inject: ['fullPath'],
props: {
workItemId: {
type: String,
@@ -27,45 +43,95 @@ export default {
data() {
return {
isEditing: false,
- localAssignees: this.assignees.map((assignee) => ({
- ...assignee,
- class: 'gl-bg-transparent!',
- })),
+ searchStarted: false,
+ localAssignees: this.assignees.map(addClass),
+ searchKey: '',
+ searchUsers: [],
};
},
- computed: {
- assigneeIds() {
- return this.localAssignees.map((assignee) => assignee.id);
+ apollo: {
+ searchUsers: {
+ query() {
+ return userSearchQuery;
+ },
+ variables() {
+ return {
+ fullPath: this.fullPath,
+ search: this.searchKey,
+ };
+ },
+ skip() {
+ return !this.searchStarted;
+ },
+ update(data) {
+ return data.workspace?.users?.nodes.map((node) => addClass({ ...node, ...node.user }));
+ },
+ error() {
+ this.$emit('error', i18n.fetchError);
+ },
},
+ },
+ computed: {
assigneeListEmpty() {
return this.assignees.length === 0;
},
containerClass() {
return !this.isEditing ? 'gl-shadow-none! gl-bg-transparent!' : '';
},
+ isLoading() {
+ return this.$apollo.queries.searchUsers.loading;
+ },
+ assigneeText() {
+ return n__('WorkItem|Assignee', 'WorkItem|Assignees', this.localAssignees.length);
+ },
+ },
+ watch: {
+ assignees(newVal) {
+ if (!this.isEditing) {
+ this.localAssignees = newVal.map(addClass);
+ }
+ },
+ },
+ created() {
+ this.debouncedSearchKeyUpdate = debounce(this.setSearchKey, DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
},
methods: {
getUserId(id) {
return getIdFromGraphQLId(id);
},
setAssignees(e) {
- if (isClosingIcon(e.relatedTarget) || !this.isEditing) return;
+ if (isTokenSelectorElement(e.relatedTarget) || !this.isEditing) return;
this.isEditing = false;
this.$apollo.mutate({
mutation: localUpdateWorkItemMutation,
variables: {
input: {
id: this.workItemId,
- assigneeIds: this.assigneeIds,
+ assignees: this.localAssignees,
},
},
});
},
- async focusTokenSelector() {
+ handleFocus() {
this.isEditing = true;
+ this.searchStarted = true;
+ },
+ async focusTokenSelector() {
+ this.handleFocus();
await this.$nextTick();
this.$refs.tokenSelector.focusTextInput();
},
+ handleMouseOver() {
+ this.timeout = setTimeout(() => {
+ this.searchStarted = true;
+ }, DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
+ },
+ handleMouseOut() {
+ clearTimeout(this.timeout);
+ },
+ setSearchKey(value) {
+ this.searchKey = value;
+ },
},
};
</script>
@@ -73,17 +139,21 @@ export default {
<template>
<div class="gl-display-flex gl-mb-4 work-item-assignees gl-relative">
<span class="gl-font-weight-bold gl-w-15 gl-pt-2" data-testid="assignees-title">{{
- __('Assignee(s)')
+ assigneeText
}}</span>
<gl-token-selector
ref="tokenSelector"
v-model="localAssignees"
- hide-dropdown-with-no-items
:container-class="containerClass"
+ :dropdown-items="searchUsers"
+ :loading="isLoading"
class="gl-w-full gl-border gl-border-white gl-hover-border-gray-200 gl-rounded-base"
- @token-remove="focusTokenSelector"
- @focus="isEditing = true"
+ @input="focusTokenSelector"
+ @text-input="debouncedSearchKeyUpdate"
+ @focus="handleFocus"
@blur="setAssignees"
+ @mouseover.native="handleMouseOver"
+ @mouseout.native="handleMouseOut"
>
<template #empty-placeholder>
<div
@@ -106,6 +176,17 @@ export default {
<span class="gl-pl-2">{{ token.name }}</span>
</gl-link>
</template>
+ <template #dropdown-item-content="{ dropdownItem }">
+ <sidebar-participant :user="dropdownItem" />
+ </template>
+ <template #loading-content>
+ <gl-skeleton-loader :height="170">
+ <rect width="380" height="20" x="10" y="15" rx="4" />
+ <rect width="280" height="20" x="10" y="50" rx="4" />
+ <rect width="380" height="20" x="10" y="95" rx="4" />
+ <rect width="280" height="20" x="10" y="130" rx="4" />
+ </gl-skeleton-loader>
+ </template>
</gl-token-selector>
</div>
</template>