Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
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.vue296
1 files changed, 163 insertions, 133 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 cc2201ad359..78cac989850 100644
--- a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees_widget.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees_widget.vue
@@ -1,26 +1,28 @@
<script>
-import {
- GlDropdownItem,
- GlDropdownDivider,
- GlAvatarLabeled,
- GlAvatarLink,
- GlSearchBoxByType,
- GlLoadingIcon,
-} from '@gitlab/ui';
+import { GlDropdownItem, GlDropdownDivider, GlSearchBoxByType, GlLoadingIcon } 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 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,
});
+
+const hideDropdownEvent = new CustomEvent('hiddenGlDropdown', {
+ bubbles: true,
+});
+
export default {
i18n: {
unassigned: __('Unassigned'),
@@ -28,17 +30,26 @@ export default {
assignees: __('Assignees'),
assignTo: __('Assign to'),
},
- assigneesQueries,
components: {
SidebarEditableItem,
IssuableAssignees,
MultiSelectDropdown,
GlDropdownItem,
GlDropdownDivider,
- GlAvatarLabeled,
- GlAvatarLink,
GlSearchBoxByType,
GlLoadingIcon,
+ SidebarInviteMembers,
+ SidebarParticipant,
+ SidebarAssigneesRealtime,
+ },
+ mixins: [glFeatureFlagsMixin()],
+ inject: {
+ directlyInviteMembers: {
+ default: false,
+ },
+ indirectlyInviteMembers: {
+ default: false,
+ },
},
props: {
iid: {
@@ -76,12 +87,13 @@ export default {
selected: [],
isSettingAssignees: false,
isSearching: false,
+ isDirty: false,
};
},
apollo: {
issuable: {
query() {
- return this.$options.assigneesQueries[this.issuableType].query;
+ return assigneesQueries[this.issuableType].query;
},
variables() {
return this.queryVariables;
@@ -109,15 +121,20 @@ export default {
},
update(data) {
const searchResults = data.workspace?.users?.nodes.map(({ user }) => user) || [];
- const mergedSearchResults = this.participants.reduce((acc, current) => {
- if (
- !acc.some((user) => current.username === user.username) &&
- (current.name.includes(this.search) || current.username.includes(this.search))
- ) {
+ 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;
- }, searchResults);
+ }, filteredParticipants);
+
return mergedSearchResults;
},
debounce: ASSIGNEES_DEBOUNCE_DELAY,
@@ -134,6 +151,10 @@ export default {
},
},
computed: {
+ shouldEnableRealtime() {
+ // Note: Realtime is only available on issues right now, future support for MR wil be built later.
+ return this.glFeatures.realTimeIssueSidebar && this.issuableType === IssuableType.Issue;
+ },
queryVariables() {
return {
iid: this.iid,
@@ -155,6 +176,9 @@ export default {
},
assigneeText() {
const items = this.$apollo.queries.issuable.loading ? this.initialAssignees : this.selected;
+ if (!items) {
+ return __('Assignee');
+ }
return n__('Assignee', '%d Assignees', items.length);
},
selectedFiltered() {
@@ -197,8 +221,15 @@ export default {
noUsersFound() {
return !this.isSearchEmpty && this.searchUsers.length === 0;
},
+ signedIn() {
+ return this.currentUser.username !== undefined;
+ },
showCurrentUser() {
- return !this.isCurrentUserInParticipants && (this.isSearchEmpty || this.isSearching);
+ return (
+ this.signedIn &&
+ !this.isCurrentUserInParticipants &&
+ (this.isSearchEmpty || this.isSearching)
+ );
},
},
watch: {
@@ -221,7 +252,7 @@ export default {
this.isSettingAssignees = true;
return this.$apollo
.mutate({
- mutation: this.$options.assigneesQueries[this.issuableType].mutation,
+ mutation: assigneesQueries[this.issuableType].mutation,
variables: {
...this.queryVariables,
assigneeUsernames,
@@ -239,20 +270,22 @@ export default {
});
},
selectAssignee(name) {
- if (name === undefined) {
- this.clearSelected();
- return;
- }
+ this.isDirty = true;
if (!this.multipleAssignees) {
- this.selected = [name];
+ this.selected = name ? [name] : [];
this.collapseWidget();
- } else {
- this.selected = this.selected.concat(name);
+ 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();
@@ -265,7 +298,9 @@ export default {
this.selected = [];
},
saveAssignees() {
+ this.isDirty = false;
this.updateAssignees(this.selectedUserNames);
+ this.$el.dispatchEvent(hideDropdownEvent);
},
isChecked(id) {
return this.selectedUserNames.includes(id);
@@ -291,6 +326,9 @@ export default {
collapseWidget() {
this.$refs.toggle.collapse();
},
+ expandWidget() {
+ this.$refs.toggle.expand();
+ },
showDivider(list) {
return list.length > 0 && this.isSearchEmpty;
},
@@ -299,121 +337,113 @@ export default {
</script>
<template>
- <div
- v-if="isAssigneesLoading"
- class="gl-display-flex gl-align-items-center assignee"
- data-testid="loading-assignees"
- >
- {{ __('Assignee') }}
- <gl-loading-icon size="sm" class="gl-ml-2" />
- </div>
- <sidebar-editable-item
- v-else
- ref="toggle"
- :loading="isSettingAssignees"
- :title="assigneeText"
- @open="focusSearch"
- @close="saveAssignees"
- >
- <template #collapsed>
- <issuable-assignees
- :users="assignees"
- :issuable-type="issuableType"
- class="gl-mt-2"
- @assign-self="assignSelf"
- />
- </template>
+ <div data-testid="assignees-widget">
+ <sidebar-assignees-realtime
+ v-if="shouldEnableRealtime"
+ :project-path="fullPath"
+ :issuable-iid="iid"
+ :issuable-type="issuableType"
+ />
+ <sidebar-editable-item
+ ref="toggle"
+ :loading="isSettingAssignees"
+ :initial-loading="isAssigneesLoading"
+ :title="assigneeText"
+ :is-dirty="isDirty"
+ @open="focusSearch"
+ @close="saveAssignees"
+ >
+ <template #collapsed>
+ <slot name="collapsed" :users="assignees" :on-click="expandWidget"></slot>
+ <issuable-assignees
+ :users="assignees"
+ :issuable-type="issuableType"
+ :signed-in="signedIn"
+ @assign-self="assignSelf"
+ @expand-widget="expandWidget"
+ />
+ </template>
- <template #default>
- <multi-select-dropdown
- class="gl-w-full dropdown-menu-user"
- :text="$options.i18n.assignees"
- :header-text="$options.i18n.assignTo"
- @toggle="collapseWidget"
- >
- <template #search>
- <gl-search-box-by-type ref="search" v-model.trim="search" />
- </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">
+ <template #default>
+ <multi-select-dropdown
+ class="gl-w-full dropdown-menu-user"
+ :text="$options.i18n.assignees"
+ :header-text="$options.i18n.assignTo"
+ @toggle="collapseWidget"
+ >
+ <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
- :is-checked="selectedIsEmpty"
+ v-for="item in selectedFiltered"
+ :key="item.id"
+ :is-checked="isChecked(item.username)"
:is-check-centered="true"
- data-testid="unassign"
- @click="selectAssignee()"
+ data-testid="selected-participant"
+ @click.stop="unselect(item.username)"
>
- <span
- :class="selectedIsEmpty ? 'gl-pl-0' : 'gl-pl-6'"
- class="gl-font-weight-bold"
- >{{ $options.i18n.unassigned }}</span
- ></gl-dropdown-item
+ <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)"
>
- </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)"
- >
- <gl-avatar-link>
- <gl-avatar-labeled
- :size="32"
- :label="item.name"
- :sub-label="item.username"
- :src="item.avatarUrl || item.avatar || item.avatar_url"
- class="gl-align-items-center"
- />
- </gl-avatar-link>
- </gl-dropdown-item>
- <template v-if="showCurrentUser">
- <gl-dropdown-divider />
+ <sidebar-participant :user="unselectedUser" class="gl-pl-6!" />
+ </gl-dropdown-item>
<gl-dropdown-item
- data-testid="current-user"
- @click.stop="selectAssignee(currentUser)"
+ v-if="noUsersFound && !isSearching"
+ data-testid="empty-results"
+ class="gl-pl-6!"
>
- <gl-avatar-link>
- <gl-avatar-labeled
- :size="32"
- :label="currentUser.name"
- :sub-label="currentUser.username"
- :src="currentUser.avatarUrl"
- class="gl-align-items-center gl-pl-6!"
- />
- </gl-avatar-link>
+ {{ __('No matching results') }}
</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)"
- >
- <gl-avatar-link class="gl-pl-6!">
- <gl-avatar-labeled
- :size="32"
- :label="unselectedUser.name"
- :sub-label="unselectedUser.username"
- :src="unselectedUser.avatarUrl || unselectedUser.avatar"
- class="gl-align-items-center"
- />
- </gl-avatar-link>
- </gl-dropdown-item>
- <gl-dropdown-item v-if="noUsersFound && !isSearching" data-testid="empty-results">
- {{ __('No matching results') }}
+ </template>
+ <template #footer>
+ <gl-dropdown-item>
+ <sidebar-invite-members v-if="directlyInviteMembers || indirectlyInviteMembers" />
</gl-dropdown-item>
</template>
- </template>
- </multi-select-dropdown>
- </template>
- </sidebar-editable-item>
+ </multi-select-dropdown>
+ </template>
+ </sidebar-editable-item>
+ </div>
</template>