2021-04-30 12:12:30 +00:00
|
|
|
<script>
|
2022-03-15 12:07:44 +00:00
|
|
|
import { debounce } from 'lodash';
|
2021-04-30 12:12:30 +00:00
|
|
|
import {
|
|
|
|
GlDropdown,
|
|
|
|
GlDropdownForm,
|
|
|
|
GlDropdownDivider,
|
|
|
|
GlDropdownItem,
|
|
|
|
GlSearchBoxByType,
|
|
|
|
GlLoadingIcon,
|
2022-03-15 12:07:44 +00:00
|
|
|
GlTooltipDirective,
|
2021-04-30 12:12:30 +00:00
|
|
|
} from '@gitlab/ui';
|
|
|
|
import { __ } from '~/locale';
|
|
|
|
import SidebarParticipant from '~/sidebar/components/assignees/sidebar_participant.vue';
|
2022-03-15 12:07:44 +00:00
|
|
|
import { IssuableType } from '~/issues/constants';
|
|
|
|
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
|
|
|
|
import { participantsQueries, userSearchQueries } from '~/sidebar/constants';
|
|
|
|
import { convertToGraphQLId } from '~/graphql_shared/utils';
|
2021-04-30 12:12:30 +00:00
|
|
|
|
|
|
|
export default {
|
|
|
|
i18n: {
|
|
|
|
unassigned: __('Unassigned'),
|
|
|
|
},
|
|
|
|
components: {
|
|
|
|
GlDropdownForm,
|
|
|
|
GlDropdown,
|
|
|
|
GlDropdownDivider,
|
|
|
|
GlDropdownItem,
|
|
|
|
GlSearchBoxByType,
|
|
|
|
SidebarParticipant,
|
|
|
|
GlLoadingIcon,
|
|
|
|
},
|
2022-03-15 12:07:44 +00:00
|
|
|
directives: {
|
|
|
|
GlTooltip: GlTooltipDirective,
|
|
|
|
},
|
2021-04-30 12:12:30 +00:00
|
|
|
props: {
|
|
|
|
headerText: {
|
|
|
|
type: String,
|
|
|
|
required: true,
|
|
|
|
},
|
|
|
|
text: {
|
|
|
|
type: String,
|
|
|
|
required: true,
|
|
|
|
},
|
|
|
|
fullPath: {
|
|
|
|
type: String,
|
|
|
|
required: true,
|
|
|
|
},
|
|
|
|
iid: {
|
|
|
|
type: String,
|
|
|
|
required: true,
|
|
|
|
},
|
|
|
|
value: {
|
|
|
|
type: Array,
|
|
|
|
required: true,
|
|
|
|
},
|
|
|
|
allowMultipleAssignees: {
|
|
|
|
type: Boolean,
|
|
|
|
required: false,
|
|
|
|
default: false,
|
|
|
|
},
|
|
|
|
currentUser: {
|
|
|
|
type: Object,
|
|
|
|
required: true,
|
|
|
|
},
|
|
|
|
issuableType: {
|
|
|
|
type: String,
|
|
|
|
required: false,
|
2022-03-15 12:07:44 +00:00
|
|
|
default: IssuableType.Issue,
|
2021-04-30 12:12:30 +00:00
|
|
|
},
|
2021-05-27 15:10:39 +00:00
|
|
|
isEditing: {
|
|
|
|
type: Boolean,
|
|
|
|
required: false,
|
|
|
|
default: true,
|
|
|
|
},
|
2022-03-15 12:07:44 +00:00
|
|
|
issuableId: {
|
|
|
|
type: Number,
|
|
|
|
required: false,
|
|
|
|
default: null,
|
|
|
|
},
|
2021-04-30 12:12:30 +00:00
|
|
|
},
|
|
|
|
data() {
|
|
|
|
return {
|
|
|
|
search: '',
|
|
|
|
participants: [],
|
|
|
|
searchUsers: [],
|
|
|
|
isSearching: false,
|
|
|
|
};
|
|
|
|
},
|
|
|
|
apollo: {
|
|
|
|
participants: {
|
|
|
|
query() {
|
|
|
|
return participantsQueries[this.issuableType].query;
|
|
|
|
},
|
2021-05-25 21:10:26 +00:00
|
|
|
skip() {
|
2021-05-27 15:10:39 +00:00
|
|
|
return Boolean(participantsQueries[this.issuableType].skipQuery) || !this.isEditing;
|
2021-05-25 21:10:26 +00:00
|
|
|
},
|
2021-04-30 12:12:30 +00:00
|
|
|
variables() {
|
|
|
|
return {
|
|
|
|
iid: this.iid,
|
|
|
|
fullPath: this.fullPath,
|
|
|
|
};
|
|
|
|
},
|
|
|
|
update(data) {
|
2022-03-15 12:07:44 +00:00
|
|
|
return data.workspace?.issuable?.participants.nodes.map((node) => ({
|
|
|
|
...node,
|
|
|
|
canMerge: false,
|
|
|
|
}));
|
2021-04-30 12:12:30 +00:00
|
|
|
},
|
|
|
|
error() {
|
|
|
|
this.$emit('error');
|
|
|
|
},
|
|
|
|
},
|
|
|
|
searchUsers: {
|
2022-03-15 12:07:44 +00:00
|
|
|
query() {
|
|
|
|
return userSearchQueries[this.issuableType].query;
|
|
|
|
},
|
2021-04-30 12:12:30 +00:00
|
|
|
variables() {
|
2022-03-15 12:07:44 +00:00
|
|
|
return this.searchUsersVariables;
|
2021-04-30 12:12:30 +00:00
|
|
|
},
|
2021-05-27 15:10:39 +00:00
|
|
|
skip() {
|
|
|
|
return !this.isEditing;
|
|
|
|
},
|
2021-04-30 12:12:30 +00:00
|
|
|
update(data) {
|
2022-03-15 12:07:44 +00:00
|
|
|
return (
|
|
|
|
data.workspace?.users?.nodes
|
|
|
|
.filter((x) => x?.user)
|
|
|
|
.map((node) => ({
|
|
|
|
...node.user,
|
|
|
|
canMerge: node.mergeRequestInteraction?.canMerge || false,
|
|
|
|
})) || []
|
|
|
|
);
|
2021-04-30 12:12:30 +00:00
|
|
|
},
|
2021-06-21 12:07:45 +00:00
|
|
|
error() {
|
2021-04-30 12:12:30 +00:00
|
|
|
this.$emit('error');
|
|
|
|
this.isSearching = false;
|
|
|
|
},
|
|
|
|
result() {
|
|
|
|
this.isSearching = false;
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
computed: {
|
2022-03-15 12:07:44 +00:00
|
|
|
isMergeRequest() {
|
|
|
|
return this.issuableType === IssuableType.MergeRequest;
|
|
|
|
},
|
|
|
|
searchUsersVariables() {
|
|
|
|
const variables = {
|
|
|
|
fullPath: this.fullPath,
|
|
|
|
search: this.search,
|
|
|
|
first: 20,
|
|
|
|
};
|
|
|
|
if (!this.isMergeRequest) {
|
|
|
|
return variables;
|
|
|
|
}
|
|
|
|
return {
|
|
|
|
...variables,
|
|
|
|
mergeRequestId: convertToGraphQLId('MergeRequest', this.issuableId),
|
|
|
|
};
|
|
|
|
},
|
2021-04-30 12:12:30 +00:00
|
|
|
isLoading() {
|
|
|
|
return this.$apollo.queries.searchUsers.loading || this.$apollo.queries.participants.loading;
|
|
|
|
},
|
|
|
|
users() {
|
|
|
|
if (!this.participants) {
|
|
|
|
return [];
|
|
|
|
}
|
2021-05-05 09:10:02 +00:00
|
|
|
|
|
|
|
const filteredParticipants = this.participants.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
|
2022-03-15 12:07:44 +00:00
|
|
|
const mergedSearchResults = this.searchUsers
|
|
|
|
.concat(filteredParticipants)
|
2021-05-05 09:10:02 +00:00
|
|
|
.reduce(
|
|
|
|
(acc, current) => (acc.some((user) => current.id === user.id) ? acc : [...acc, current]),
|
|
|
|
[],
|
|
|
|
);
|
|
|
|
|
2021-04-30 12:12:30 +00:00
|
|
|
return this.moveCurrentUserToStart(mergedSearchResults);
|
|
|
|
},
|
|
|
|
isSearchEmpty() {
|
|
|
|
return this.search === '';
|
|
|
|
},
|
|
|
|
shouldShowParticipants() {
|
|
|
|
return this.isSearchEmpty || this.isSearching;
|
|
|
|
},
|
|
|
|
isCurrentUserInList() {
|
|
|
|
const isCurrentUser = (user) => user.username === this.currentUser.username;
|
|
|
|
return this.users.some(isCurrentUser);
|
|
|
|
},
|
|
|
|
noUsersFound() {
|
|
|
|
return !this.isSearchEmpty && this.users.length === 0;
|
|
|
|
},
|
|
|
|
showCurrentUser() {
|
|
|
|
return this.currentUser.username && !this.isCurrentUserInList && this.isSearchEmpty;
|
|
|
|
},
|
|
|
|
selectedFiltered() {
|
|
|
|
if (this.shouldShowParticipants) {
|
|
|
|
return this.moveCurrentUserToStart(this.value);
|
|
|
|
}
|
|
|
|
|
|
|
|
const foundUsernames = this.users.map(({ username }) => username);
|
|
|
|
const filtered = this.value.filter(({ username }) => foundUsernames.includes(username));
|
|
|
|
return this.moveCurrentUserToStart(filtered);
|
|
|
|
},
|
|
|
|
selectedUserNames() {
|
|
|
|
return this.value.map(({ username }) => username);
|
|
|
|
},
|
|
|
|
unselectedFiltered() {
|
|
|
|
return this.users?.filter(({ username }) => !this.selectedUserNames.includes(username)) || [];
|
|
|
|
},
|
|
|
|
selectedIsEmpty() {
|
|
|
|
return this.selectedFiltered.length === 0;
|
|
|
|
},
|
|
|
|
},
|
2022-03-15 12:07:44 +00:00
|
|
|
|
2021-04-30 12:12:30 +00:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
},
|
|
|
|
},
|
2022-03-15 12:07:44 +00:00
|
|
|
created() {
|
|
|
|
this.debouncedSearchKeyUpdate = debounce(this.setSearchKey, DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
|
|
|
|
},
|
2021-04-30 12:12:30 +00:00
|
|
|
methods: {
|
|
|
|
selectAssignee(user) {
|
|
|
|
let selected = [...this.value];
|
|
|
|
if (!this.allowMultipleAssignees) {
|
|
|
|
selected = [user];
|
2022-03-15 12:07:44 +00:00
|
|
|
this.$emit('input', selected);
|
|
|
|
this.$refs.dropdown.hide();
|
|
|
|
this.$emit('toggle');
|
2021-04-30 12:12:30 +00:00
|
|
|
} else {
|
|
|
|
selected.push(user);
|
2022-03-15 12:07:44 +00:00
|
|
|
this.$emit('input', selected);
|
2021-04-30 12:12:30 +00:00
|
|
|
}
|
|
|
|
},
|
|
|
|
unselect(name) {
|
|
|
|
const selected = this.value.filter((user) => user.username !== name);
|
|
|
|
this.$emit('input', selected);
|
|
|
|
},
|
|
|
|
focusSearch() {
|
|
|
|
this.$refs.search.focusInput();
|
|
|
|
},
|
2022-03-15 12:07:44 +00:00
|
|
|
showDropdown() {
|
|
|
|
this.$refs.dropdown.show();
|
|
|
|
},
|
2021-04-30 12:12:30 +00:00
|
|
|
showDivider(list) {
|
|
|
|
return list.length > 0 && this.isSearchEmpty;
|
|
|
|
},
|
|
|
|
moveCurrentUserToStart(users) {
|
|
|
|
if (!users) {
|
|
|
|
return [];
|
|
|
|
}
|
|
|
|
const usersCopy = [...users];
|
|
|
|
const currentUser = usersCopy.find((user) => user.username === this.currentUser.username);
|
|
|
|
|
|
|
|
if (currentUser) {
|
2022-03-15 12:07:44 +00:00
|
|
|
currentUser.canMerge = this.currentUser.canMerge;
|
2021-04-30 12:12:30 +00:00
|
|
|
const index = usersCopy.indexOf(currentUser);
|
|
|
|
usersCopy.splice(0, 0, usersCopy.splice(index, 1)[0]);
|
|
|
|
}
|
|
|
|
|
|
|
|
return usersCopy;
|
|
|
|
},
|
2022-03-15 12:07:44 +00:00
|
|
|
setSearchKey(value) {
|
|
|
|
this.search = value.trim();
|
|
|
|
},
|
|
|
|
tooltipText(user) {
|
|
|
|
if (!this.isMergeRequest) {
|
|
|
|
return '';
|
|
|
|
}
|
|
|
|
return user.canMerge ? '' : __('Cannot merge');
|
|
|
|
},
|
2021-04-30 12:12:30 +00:00
|
|
|
},
|
|
|
|
};
|
|
|
|
</script>
|
|
|
|
|
|
|
|
<template>
|
2022-03-15 12:07:44 +00:00
|
|
|
<gl-dropdown ref="dropdown" :text="text" @toggle="$emit('toggle')" @shown="focusSearch">
|
2021-04-30 12:12:30 +00:00
|
|
|
<template #header>
|
|
|
|
<p class="gl-font-weight-bold gl-text-center gl-mt-2 gl-mb-4">{{ headerText }}</p>
|
|
|
|
<gl-dropdown-divider />
|
2022-03-15 12:07:44 +00:00
|
|
|
<gl-search-box-by-type
|
|
|
|
ref="search"
|
|
|
|
:value="search"
|
|
|
|
class="js-dropdown-input-field"
|
|
|
|
@input="debouncedSearchKeyUpdate"
|
|
|
|
/>
|
2021-04-30 12:12:30 +00:00
|
|
|
</template>
|
|
|
|
<gl-dropdown-form class="gl-relative gl-min-h-7">
|
|
|
|
<gl-loading-icon
|
|
|
|
v-if="isLoading"
|
|
|
|
data-testid="loading-participants"
|
2022-05-09 21:07:53 +00:00
|
|
|
size="lg"
|
2021-04-30 12:12:30 +00:00
|
|
|
class="gl-absolute gl-left-0 gl-top-0 gl-right-0"
|
|
|
|
/>
|
|
|
|
<template v-else>
|
|
|
|
<template v-if="shouldShowParticipants">
|
|
|
|
<gl-dropdown-item
|
|
|
|
v-if="isSearchEmpty"
|
|
|
|
:is-checked="selectedIsEmpty"
|
|
|
|
:is-check-centered="true"
|
|
|
|
data-testid="unassign"
|
2022-03-15 12:07:44 +00:00
|
|
|
@click.native.capture.stop="$emit('input', [])"
|
2021-04-30 12:12:30 +00:00
|
|
|
>
|
|
|
|
<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"
|
2022-03-15 12:07:44 +00:00
|
|
|
v-gl-tooltip.left.viewport
|
|
|
|
:title="tooltipText(item)"
|
|
|
|
boundary="viewport"
|
2021-04-30 12:12:30 +00:00
|
|
|
is-checked
|
|
|
|
is-check-centered
|
|
|
|
data-testid="selected-participant"
|
2022-03-15 12:07:44 +00:00
|
|
|
@click.native.capture.stop="unselect(item.username)"
|
2021-04-30 12:12:30 +00:00
|
|
|
>
|
2022-03-15 12:07:44 +00:00
|
|
|
<sidebar-participant :user="item" :issuable-type="issuableType" />
|
2021-04-30 12:12:30 +00:00
|
|
|
</gl-dropdown-item>
|
|
|
|
<template v-if="showCurrentUser">
|
|
|
|
<gl-dropdown-divider />
|
2022-03-15 12:07:44 +00:00
|
|
|
<gl-dropdown-item
|
|
|
|
data-testid="current-user"
|
|
|
|
@click.native.capture.stop="selectAssignee(currentUser)"
|
|
|
|
>
|
|
|
|
<sidebar-participant
|
|
|
|
:user="currentUser"
|
|
|
|
:issuable-type="issuableType"
|
|
|
|
class="gl-pl-6!"
|
|
|
|
/>
|
2021-04-30 12:12:30 +00:00
|
|
|
</gl-dropdown-item>
|
|
|
|
</template>
|
|
|
|
<gl-dropdown-divider v-if="showDivider(unselectedFiltered)" />
|
|
|
|
<gl-dropdown-item
|
|
|
|
v-for="unselectedUser in unselectedFiltered"
|
|
|
|
:key="unselectedUser.id"
|
2022-03-15 12:07:44 +00:00
|
|
|
v-gl-tooltip.left.viewport
|
|
|
|
:title="tooltipText(unselectedUser)"
|
|
|
|
boundary="viewport"
|
2021-04-30 12:12:30 +00:00
|
|
|
data-testid="unselected-participant"
|
2022-03-15 12:07:44 +00:00
|
|
|
@click.native.capture.stop="selectAssignee(unselectedUser)"
|
2021-04-30 12:12:30 +00:00
|
|
|
>
|
2022-03-15 12:07:44 +00:00
|
|
|
<sidebar-participant
|
|
|
|
:user="unselectedUser"
|
|
|
|
:issuable-type="issuableType"
|
|
|
|
class="gl-pl-6!"
|
|
|
|
/>
|
2021-04-30 12:12:30 +00:00
|
|
|
</gl-dropdown-item>
|
|
|
|
<gl-dropdown-item v-if="noUsersFound" data-testid="empty-results" class="gl-pl-6!">
|
|
|
|
{{ __('No matching results') }}
|
|
|
|
</gl-dropdown-item>
|
|
|
|
</template>
|
|
|
|
</gl-dropdown-form>
|
|
|
|
<template #footer>
|
|
|
|
<slot name="footer"></slot>
|
|
|
|
</template>
|
|
|
|
</gl-dropdown>
|
|
|
|
</template>
|