diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-04-13 12:11:10 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-04-13 12:11:10 +0300 |
commit | 37974ac0b196b06ffcc6cbea44385eaac1cc57bd (patch) | |
tree | 98450a46516f93a71018ec6b8d718fc023744575 /app/assets | |
parent | fcbd3db20f5dfb13ae33ddfee98be8d92cade72f (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app/assets')
32 files changed, 710 insertions, 302 deletions
diff --git a/app/assets/javascripts/boards/components/filtered_search.vue b/app/assets/javascripts/boards/components/filtered_search.vue deleted file mode 100644 index 8505ea39a6b..00000000000 --- a/app/assets/javascripts/boards/components/filtered_search.vue +++ /dev/null @@ -1,54 +0,0 @@ -<script> -import { mapActions } from 'vuex'; -import { historyPushState } from '~/lib/utils/common_utils'; -import { setUrlParams } from '~/lib/utils/url_utility'; -import { __ } from '~/locale'; -import FilteredSearch from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue'; - -export default { - i18n: { - search: __('Search'), - }, - components: { FilteredSearch }, - props: { - search: { - type: String, - required: false, - default: '', - }, - }, - computed: { - initialSearch() { - return [{ type: 'filtered-search-term', value: { data: this.search } }]; - }, - }, - methods: { - ...mapActions(['performSearch']), - handleSearch(filters) { - let itemValue = ''; - const [item] = filters; - - if (filters.length === 0) { - itemValue = ''; - } else { - itemValue = item?.value?.data; - } - - historyPushState(setUrlParams({ search: itemValue }, window.location.href)); - - this.performSearch(); - }, - }, -}; -</script> - -<template> - <filtered-search - class="gl-w-full" - namespace="" - :tokens="[]" - :search-input-placeholder="$options.i18n.search" - :initial-filter-value="initialSearch" - @onFilter="handleSearch" - /> -</template> diff --git a/app/assets/javascripts/boards/filtered_search.js b/app/assets/javascripts/boards/filtered_search.js deleted file mode 100644 index 182a2cf3724..00000000000 --- a/app/assets/javascripts/boards/filtered_search.js +++ /dev/null @@ -1,25 +0,0 @@ -import Vue from 'vue'; -import store from '~/boards/stores'; -import { queryToObject } from '~/lib/utils/url_utility'; -import FilteredSearch from './components/filtered_search.vue'; - -export default () => { - const queryParams = queryToObject(window.location.search); - const el = document.getElementById('js-board-filtered-search'); - - /* - When https://github.com/vuejs/vue-apollo/pull/1153 is merged and deployed - we can remove apolloProvider option from here. Currently without it its causing - an error - */ - - return new Vue({ - el, - store, - apolloProvider: {}, - render: (createElement) => - createElement(FilteredSearch, { - props: { search: queryParams.search }, - }), - }); -}; diff --git a/app/assets/javascripts/graphql_shared/fragments/user_availability.fragment.graphql b/app/assets/javascripts/graphql_shared/fragments/user_availability.fragment.graphql new file mode 100644 index 00000000000..0b451262b5a --- /dev/null +++ b/app/assets/javascripts/graphql_shared/fragments/user_availability.fragment.graphql @@ -0,0 +1,5 @@ +fragment UserAvailability on User { + status { + availability + } +} diff --git a/app/assets/javascripts/graphql_shared/queries/users_search.query.graphql b/app/assets/javascripts/graphql_shared/queries/users_search.query.graphql index 3397a2529f1..e18eea33041 100644 --- a/app/assets/javascripts/graphql_shared/queries/users_search.query.graphql +++ b/app/assets/javascripts/graphql_shared/queries/users_search.query.graphql @@ -1,4 +1,5 @@ #import "../fragments/user.fragment.graphql" +#import "~/graphql_shared/fragments/user_availability.fragment.graphql" query usersSearch($search: String!, $fullPath: ID!) { workspace: project(fullPath: $fullPath) { @@ -6,6 +7,7 @@ query usersSearch($search: String!, $fullPath: ID!) { nodes { user { ...User + ...UserAvailability } } } diff --git a/app/assets/javascripts/invite_member/components/invite_member_modal.vue b/app/assets/javascripts/invite_member/components/invite_member_modal.vue index 144c1a2c22a..ec77e49ae53 100644 --- a/app/assets/javascripts/invite_member/components/invite_member_modal.vue +++ b/app/assets/javascripts/invite_member/components/invite_member_modal.vue @@ -19,8 +19,10 @@ export default { GlLink, GlModal, }, - inject: { + props: { membersPath: { + type: String, + required: false, default: '', }, }, diff --git a/app/assets/javascripts/invite_member/components/invite_member_trigger.vue b/app/assets/javascripts/invite_member/components/invite_member_trigger.vue index 56cf1ab2fc2..ee89e0bbf71 100644 --- a/app/assets/javascripts/invite_member/components/invite_member_trigger.vue +++ b/app/assets/javascripts/invite_member/components/invite_member_trigger.vue @@ -7,14 +7,20 @@ export default { components: { GlLink, }, - inject: { + props: { displayText: { + type: String, + required: false, default: '', }, event: { + type: String, + required: false, default: '', }, label: { + type: String, + required: false, default: '', }, }, diff --git a/app/assets/javascripts/invite_member/init_invite_member_modal.js b/app/assets/javascripts/invite_member/init_invite_member_modal.js index c292bda1931..108f636ee3e 100644 --- a/app/assets/javascripts/invite_member/init_invite_member_modal.js +++ b/app/assets/javascripts/invite_member/init_invite_member_modal.js @@ -1,5 +1,6 @@ import { GlToast } from '@gitlab/ui'; import Vue from 'vue'; +import { isInIssuePage, isInDesignPage } from '~/lib/utils/common_utils'; import InviteMemberModal from './components/invite_member_modal.vue'; Vue.use(GlToast); @@ -7,7 +8,7 @@ Vue.use(GlToast); export default function initInviteMembersModal() { const el = document.querySelector('.js-invite-member-modal'); - if (!el) { + if (!el || isInDesignPage() || isInIssuePage()) { return false; } @@ -15,7 +16,9 @@ export default function initInviteMembersModal() { return new Vue({ el, - provide: { membersPath }, - render: (createElement) => createElement(InviteMemberModal), + render: (createElement) => + createElement(InviteMemberModal, { + props: { membersPath }, + }), }); } diff --git a/app/assets/javascripts/invite_member/init_invite_member_trigger.js b/app/assets/javascripts/invite_member/init_invite_member_trigger.js index 5e763e4f47d..eb765ae83b0 100644 --- a/app/assets/javascripts/invite_member/init_invite_member_trigger.js +++ b/app/assets/javascripts/invite_member/init_invite_member_trigger.js @@ -10,7 +10,9 @@ export default function initInviteMembersTrigger() { return new Vue({ el, - provide: { ...el.dataset }, - render: (createElement) => createElement(InviteMemberTrigger), + render: (createElement) => + createElement(InviteMemberTrigger, { + props: { ...el.dataset }, + }), }); } diff --git a/app/assets/javascripts/issuable_sidebar/queries/issue_sidebar.query.graphql b/app/assets/javascripts/issuable_sidebar/queries/issue_sidebar.query.graphql deleted file mode 100644 index 42e646391a8..00000000000 --- a/app/assets/javascripts/issuable_sidebar/queries/issue_sidebar.query.graphql +++ /dev/null @@ -1,16 +0,0 @@ -#import "~/graphql_shared/fragments/author.fragment.graphql" - -query getProjectIssue($iid: String!, $fullPath: ID!) { - project(fullPath: $fullPath) { - issue(iid: $iid) { - id - assignees { - nodes { - ...Author - id - state - } - } - } - } -} diff --git a/app/assets/javascripts/pages/projects/project.js b/app/assets/javascripts/pages/projects/project.js index da8dc527d79..91f376060f8 100644 --- a/app/assets/javascripts/pages/projects/project.js +++ b/app/assets/javascripts/pages/projects/project.js @@ -123,10 +123,19 @@ export default class Project { const loc = window.location.href; if (loc.includes('/-/')) { - const refs = this.fullData.Branches.concat(this.fullData.Tags); - const currentRef = refs.find((ref) => loc.indexOf(ref) > -1); - if (currentRef) { - const targetPath = loc.split(currentRef)[1].slice(1).split('#')[0]; + // Since the current ref in renderRow is outdated on page changes + // (To be addressed in: https://gitlab.com/gitlab-org/gitlab/-/issues/327085) + // We are deciphering the current ref from the dropdown data instead + const currentRef = $dropdown.data('ref'); + // The split and startWith is to ensure an exact word match + // and avoid partial match ie. currentRef is "dev" and loc is "development" + const splitPathAfterRefPortion = loc.split(currentRef)[1]; + const doesPathContainRef = splitPathAfterRefPortion?.startsWith('/'); + + if (doesPathContainRef) { + // We are ignoring the url containing the ref portion + // and plucking the thereafter portion to reconstructure the url that is correct + const targetPath = splitPathAfterRefPortion?.slice(1).split('#')[0]; selectedUrl.searchParams.set('path', targetPath); selectedUrl.hash = window.location.hash; } diff --git a/app/assets/javascripts/pipelines/pipeline_details_bundle.js b/app/assets/javascripts/pipelines/pipeline_details_bundle.js index 849f4c27988..cc53532b554 100644 --- a/app/assets/javascripts/pipelines/pipeline_details_bundle.js +++ b/app/assets/javascripts/pipelines/pipeline_details_bundle.js @@ -6,6 +6,8 @@ import PipelineGraphLegacy from './components/graph/graph_component_legacy.vue'; import TestReports from './components/test_reports/test_reports.vue'; import GraphBundleMixin from './mixins/graph_pipeline_bundle_mixin'; import createDagApp from './pipeline_details_dag'; +import { createPipelinesDetailApp } from './pipeline_details_graph'; +import { createPipelineHeaderApp } from './pipeline_details_header'; import { apolloProvider } from './pipeline_shared_client'; import createTestReportsStore from './stores/test_reports'; import { reportToSentry } from './utils'; @@ -80,20 +82,19 @@ const createTestDetails = () => { }; export default async function initPipelineDetailsBundle() { - createTestDetails(); - createDagApp(apolloProvider); - const canShowNewPipelineDetails = gon.features.graphqlPipelineDetails || gon.features.graphqlPipelineDetailsUsers; const { dataset } = document.querySelector(SELECTORS.PIPELINE_DETAILS); + try { + createPipelineHeaderApp(SELECTORS.PIPELINE_HEADER, apolloProvider, dataset.graphqlResourceEtag); + } catch { + Flash(__('An error occurred while loading a section of this page.')); + } + if (canShowNewPipelineDetails) { try { - const { createPipelinesDetailApp } = await import( - /* webpackChunkName: 'createPipelinesDetailApp' */ './pipeline_details_graph' - ); - createPipelinesDetailApp(SELECTORS.PIPELINE_GRAPH, apolloProvider, dataset); } catch { Flash(__('An error occurred while loading the pipeline.')); @@ -108,12 +109,6 @@ export default async function initPipelineDetailsBundle() { createLegacyPipelinesDetailApp(mediator); } - try { - const { createPipelineHeaderApp } = await import( - /* webpackChunkName: 'createPipelineHeaderApp' */ './pipeline_details_header' - ); - createPipelineHeaderApp(SELECTORS.PIPELINE_HEADER, apolloProvider, dataset.graphqlResourceEtag); - } catch { - Flash(__('An error occurred while loading a section of this page.')); - } + createDagApp(apolloProvider); + createTestDetails(); } diff --git a/app/assets/javascripts/projects/compare/components/repo_dropdown.vue b/app/assets/javascripts/projects/compare/components/repo_dropdown.vue index 822dfc09d81..cb9d8b64b33 100644 --- a/app/assets/javascripts/projects/compare/components/repo_dropdown.vue +++ b/app/assets/javascripts/projects/compare/components/repo_dropdown.vue @@ -46,14 +46,7 @@ export default { this.emitTargetProject(repo.name); }, setDefaultRepo() { - if (this.isSourceRevision) { - this.selectedRepo = this.projectTo; - return; - } - - const [defaultTargetProject] = this.projectsFrom; - this.emitTargetProject(defaultTargetProject.name); - this.selectedRepo = defaultTargetProject; + this.selectedRepo = this.projectTo; }, emitTargetProject(name) { if (!this.isSourceRevision) { diff --git a/app/assets/javascripts/sidebar/components/assignees/assignee_avatar_link.vue b/app/assets/javascripts/sidebar/components/assignees/assignee_avatar_link.vue index d0a65b48522..98fc0b0a783 100644 --- a/app/assets/javascripts/sidebar/components/assignees/assignee_avatar_link.vue +++ b/app/assets/javascripts/sidebar/components/assignees/assignee_avatar_link.vue @@ -103,10 +103,10 @@ export default { v-gl-tooltip="tooltipOption" :href="assigneeUrl" :title="tooltipTitle" - class="d-inline-block" + class="gl-display-inline-block" > <!-- use d-flex so that slot can be appropriately styled --> - <span class="d-flex"> + <span class="gl-display-flex"> <assignee-avatar :user="user" :img-size="32" :issuable-type="issuableType" /> <slot></slot> </span> diff --git a/app/assets/javascripts/sidebar/components/assignees/assignees_realtime.vue b/app/assets/javascripts/sidebar/components/assignees/assignees_realtime.vue index ca86d6c6c3e..f98798582c1 100644 --- a/app/assets/javascripts/sidebar/components/assignees/assignees_realtime.vue +++ b/app/assets/javascripts/sidebar/components/assignees/assignees_realtime.vue @@ -1,7 +1,7 @@ <script> import actionCable from '~/actioncable_consumer'; import { getIdFromGraphQLId } from '~/graphql_shared/utils'; -import query from '~/issuable_sidebar/queries/issue_sidebar.query.graphql'; +import { assigneesQueries } from '~/sidebar/constants'; export default { subscription: null, @@ -9,7 +9,8 @@ export default { props: { mediator: { type: Object, - required: true, + required: false, + default: null, }, issuableIid: { type: String, @@ -19,10 +20,16 @@ export default { type: String, required: true, }, + issuableType: { + type: String, + required: true, + }, }, apollo: { - project: { - query, + workspace: { + query() { + return assigneesQueries[this.issuableType].query; + }, variables() { return { iid: this.issuableIid, @@ -30,7 +37,9 @@ export default { }; }, result(data) { - this.handleFetchResult(data); + if (this.mediator) { + this.handleFetchResult(data); + } }, }, }, @@ -43,7 +52,7 @@ export default { methods: { received(data) { if (data.event === 'updated') { - this.$apollo.queries.project.refetch(); + this.$apollo.queries.workspace.refetch(); } }, initActionCablePolling() { @@ -57,7 +66,7 @@ export default { ); }, handleFetchResult({ data }) { - const { nodes } = data.project.issue.assignees; + const { nodes } = data.workspace.issuable.assignees; const assignees = nodes.map((n) => ({ ...n, @@ -69,7 +78,7 @@ export default { }, }, render() { - return this.$slots.default; + return null; }, }; </script> diff --git a/app/assets/javascripts/sidebar/components/assignees/issuable_assignees.vue b/app/assets/javascripts/sidebar/components/assignees/issuable_assignees.vue index b53b7039018..e93aced12f3 100644 --- a/app/assets/javascripts/sidebar/components/assignees/issuable_assignees.vue +++ b/app/assets/javascripts/sidebar/components/assignees/issuable_assignees.vue @@ -18,6 +18,11 @@ export default { required: false, default: 'issue', }, + signedIn: { + type: Boolean, + required: false, + default: false, + }, }, computed: { assigneesText() { @@ -34,20 +39,28 @@ export default { <div class="gl-display-flex gl-flex-direction-column issuable-assignees"> <div v-if="emptyUsers" - class="gl-display-flex gl-align-items-center gl-text-gray-500" + class="gl-display-flex gl-align-items-center gl-text-gray-500 gl-mt-2 hide-collapsed" data-testid="none" > - <span> {{ __('None') }} -</span> - <gl-button - data-testid="assign-yourself" - category="tertiary" - variant="link" - class="gl-ml-2" - @click="$emit('assign-self')" - > - <span class="gl-text-gray-500 gl-hover-text-blue-800">{{ __('assign yourself') }}</span> - </gl-button> + <span> {{ __('None') }}</span> + <template v-if="signedIn"> + <span class="gl-ml-2">-</span> + <gl-button + data-testid="assign-yourself" + category="tertiary" + variant="link" + class="gl-ml-2" + @click="$emit('assign-self')" + > + <span class="gl-text-gray-500 gl-hover-text-blue-800">{{ __('assign yourself') }}</span> + </gl-button> + </template> </div> - <uncollapsed-assignee-list v-else :users="users" :issuable-type="issuableType" /> + <uncollapsed-assignee-list + v-else + :users="users" + :issuable-type="issuableType" + class="gl-mt-2 hide-collapsed" + /> </div> </template> diff --git a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue index 6595debf9a5..e15ea595190 100644 --- a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue +++ b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue @@ -123,6 +123,7 @@ export default { v-if="shouldEnableRealtime" :issuable-iid="issuableIid" :project-path="projectPath" + :issuable-type="issuableType" :mediator="mediator" /> <assignee-title 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..34bc5cf5c2b 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; @@ -134,6 +146,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 +171,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 +216,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 +247,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 +265,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 +293,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 +321,9 @@ export default { collapseWidget() { this.$refs.toggle.collapse(); }, + expandWidget() { + this.$refs.toggle.expand(); + }, showDivider(list) { return list.length > 0 && this.isSearchEmpty; }, @@ -299,121 +332,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> diff --git a/app/assets/javascripts/sidebar/components/assignees/sidebar_invite_members.vue b/app/assets/javascripts/sidebar/components/assignees/sidebar_invite_members.vue new file mode 100644 index 00000000000..9952c6db582 --- /dev/null +++ b/app/assets/javascripts/sidebar/components/assignees/sidebar_invite_members.vue @@ -0,0 +1,51 @@ +<script> +import InviteMemberModal from '~/invite_member/components/invite_member_modal.vue'; +import InviteMemberTrigger from '~/invite_member/components/invite_member_trigger.vue'; +import InviteMembersTrigger from '~/invite_members/components/invite_members_trigger.vue'; +import { __ } from '~/locale'; + +export default { + displayText: __('Invite members'), + dataTrackLabel: 'edit_assignee', + components: { + InviteMemberTrigger, + InviteMemberModal, + InviteMembersTrigger, + }, + inject: { + projectMembersPath: { + default: '', + }, + directlyInviteMembers: { + default: false, + }, + }, + computed: { + trackEvent() { + return this.directlyInviteMembers ? 'click_invite_members' : 'click_invite_members_version_b'; + }, + }, +}; +</script> + +<template> + <div> + <invite-members-trigger + v-if="directlyInviteMembers" + trigger-element="anchor" + :display-text="$options.displayText" + :event="trackEvent" + :label="$options.dataTrackLabel" + classes="gl-display-block gl-pl-6 gl-hover-text-decoration-none gl-hover-text-blue-800!" + /> + <template v-else> + <invite-member-trigger + :display-text="$options.displayText" + :event="trackEvent" + :label="$options.dataTrackLabel" + class="gl-display-block gl-pl-6 gl-hover-text-decoration-none gl-hover-text-blue-800!" + /> + <invite-member-modal :members-path="projectMembersPath" /> + </template> + </div> +</template> diff --git a/app/assets/javascripts/sidebar/components/assignees/sidebar_participant.vue b/app/assets/javascripts/sidebar/components/assignees/sidebar_participant.vue new file mode 100644 index 00000000000..e2a38a100b9 --- /dev/null +++ b/app/assets/javascripts/sidebar/components/assignees/sidebar_participant.vue @@ -0,0 +1,39 @@ +<script> +import { GlAvatarLabeled, GlAvatarLink } from '@gitlab/ui'; +import { s__, sprintf } from '~/locale'; + +export default { + components: { + GlAvatarLabeled, + GlAvatarLink, + }, + props: { + user: { + type: Object, + required: true, + }, + }, + computed: { + userLabel() { + if (!this.user.status) { + return this.user.name; + } + return sprintf(s__('UserAvailability|%{author} (Busy)'), { + author: this.user.name, + }); + }, + }, +}; +</script> + +<template> + <gl-avatar-link> + <gl-avatar-labeled + :size="32" + :label="userLabel" + :sub-label="user.username" + :src="user.avatarUrl || user.avatar || user.avatar_url" + class="gl-align-items-center" + /> + </gl-avatar-link> +</template> diff --git a/app/assets/javascripts/sidebar/components/assignees/uncollapsed_assignee_list.vue b/app/assets/javascripts/sidebar/components/assignees/uncollapsed_assignee_list.vue index d0da4a9c75a..b7080bb05b8 100644 --- a/app/assets/javascripts/sidebar/components/assignees/uncollapsed_assignee_list.vue +++ b/app/assets/javascripts/sidebar/components/assignees/uncollapsed_assignee_list.vue @@ -1,4 +1,5 @@ <script> +import { IssuableType } from '~/issue_show/constants'; import { __, sprintf } from '~/locale'; import AssigneeAvatarLink from './assignee_avatar_link.vue'; import UserNameWithStatus from './user_name_with_status.vue'; @@ -58,7 +59,10 @@ export default { this.showLess = !this.showLess; }, userAvailability(u) { - return u?.availability || ''; + if (this.issuableType === IssuableType.MergeRequest) { + return u?.availability || ''; + } + return u?.status?.availability || ''; }, }, }; diff --git a/app/assets/javascripts/sidebar/components/due_date/sidebar_due_date_widget.vue b/app/assets/javascripts/sidebar/components/due_date/sidebar_due_date_widget.vue new file mode 100644 index 00000000000..141c2b3aae9 --- /dev/null +++ b/app/assets/javascripts/sidebar/components/due_date/sidebar_due_date_widget.vue @@ -0,0 +1,203 @@ +<script> +import { GlButton, GlIcon, GlDatepicker, GlTooltipDirective } from '@gitlab/ui'; +import createFlash from '~/flash'; +import { IssuableType } from '~/issue_show/constants'; +import { dateInWords, formatDate, parsePikadayDate } from '~/lib/utils/datetime_utility'; +import { __, sprintf } from '~/locale'; +import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue'; +import { dueDateQueries } from '~/sidebar/constants'; + +const hideDropdownEvent = new CustomEvent('hiddenGlDropdown', { + bubbles: true, +}); + +export default { + tracking: { + event: 'click_edit_button', + label: 'right_sidebar', + property: 'dueDate', + }, + directives: { + GlTooltip: GlTooltipDirective, + }, + components: { + GlButton, + GlIcon, + GlDatepicker, + SidebarEditableItem, + }, + inject: ['fullPath', 'iid', 'canUpdate'], + props: { + issuableType: { + required: true, + type: String, + }, + }, + data() { + return { + dueDate: null, + loading: false, + }; + }, + apollo: { + dueDate: { + query() { + return dueDateQueries[this.issuableType].query; + }, + variables() { + return { + fullPath: this.fullPath, + iid: String(this.iid), + }; + }, + update(data) { + return data.workspace?.issuable?.dueDate || null; + }, + result({ data }) { + this.$emit('dueDateUpdated', data.workspace?.issuable?.dueDate); + }, + error() { + createFlash({ + message: sprintf(__('Something went wrong while setting %{issuableType} due date.'), { + issuableType: this.issuableType, + }), + }); + }, + }, + }, + computed: { + isLoading() { + return this.$apollo.queries.dueDate.loading || this.loading; + }, + hasDueDate() { + return this.dueDate !== null; + }, + parsedDueDate() { + if (!this.hasDueDate) { + return null; + } + + return parsePikadayDate(this.dueDate); + }, + formattedDueDate() { + if (!this.hasDueDate) { + return this.$options.i18n.noDueDate; + } + + return dateInWords(this.parsedDueDate, true); + }, + workspacePath() { + return this.issuableType === IssuableType.Issue + ? { + projectPath: this.fullPath, + } + : { + groupPath: this.fullPath, + }; + }, + }, + methods: { + closeForm() { + this.$refs.editable.collapse(); + this.$el.dispatchEvent(hideDropdownEvent); + this.$emit('closeForm'); + }, + openDatePicker() { + this.$refs.datePicker.calendar.show(); + }, + setDueDate(date) { + this.loading = true; + this.$refs.editable.collapse(); + this.$apollo + .mutate({ + mutation: dueDateQueries[this.issuableType].mutation, + variables: { + input: { + ...this.workspacePath, + iid: this.iid, + dueDate: date ? formatDate(date, 'yyyy-mm-dd') : null, + }, + }, + }) + .then( + ({ + data: { + issuableSetDueDate: { errors }, + }, + }) => { + if (errors.length) { + createFlash({ + message: errors[0], + }); + } else { + this.$emit('closeForm'); + } + }, + ) + .catch(() => { + createFlash({ + message: sprintf(__('Something went wrong while setting %{issuableType} due date.'), { + issuableType: this.issuableType, + }), + }); + }) + .finally(() => { + this.loading = false; + }); + }, + }, + i18n: { + dueDate: __('Due date'), + noDueDate: __('None'), + removeDueDate: __('remove due date'), + }, +}; +</script> + +<template> + <sidebar-editable-item + ref="editable" + :title="$options.i18n.dueDate" + :tracking="$options.tracking" + :loading="isLoading" + class="block" + data-testid="due-date" + @open="openDatePicker" + > + <template #collapsed> + <div v-gl-tooltip :title="$options.i18n.dueDate" class="sidebar-collapsed-icon"> + <gl-icon :size="16" name="calendar" /> + <span class="collapse-truncated-title">{{ formattedDueDate }}</span> + </div> + <div class="gl-display-flex gl-align-items-center hide-collapsed"> + <span + :class="hasDueDate ? 'gl-text-gray-900 gl-font-weight-bold' : 'gl-text-gray-500'" + data-testid="sidebar-duedate-value" + > + {{ formattedDueDate }} + </span> + <div v-if="hasDueDate && canUpdate" class="gl-display-flex"> + <span class="gl-px-2">-</span> + <gl-button + variant="link" + class="gl-text-gray-500!" + data-testid="reset-button" + :disabled="isLoading" + @click="setDueDate(null)" + > + {{ $options.i18n.removeDueDate }} + </gl-button> + </div> + </div> + </template> + <template #default> + <gl-datepicker + ref="datePicker" + :value="parsedDueDate" + show-clear-button + @input="setDueDate" + @clear="setDueDate(null)" + /> + </template> + </sidebar-editable-item> +</template> diff --git a/app/assets/javascripts/sidebar/components/sidebar_editable_item.vue b/app/assets/javascripts/sidebar/components/sidebar_editable_item.vue index 4ab4606ac1c..caf1c92c28a 100644 --- a/app/assets/javascripts/sidebar/components/sidebar_editable_item.vue +++ b/app/assets/javascripts/sidebar/components/sidebar_editable_item.vue @@ -1,5 +1,6 @@ <script> import { GlButton, GlLoadingIcon } from '@gitlab/ui'; +import { __ } from '~/locale'; export default { components: { GlButton, GlLoadingIcon }, @@ -20,6 +21,16 @@ export default { required: false, default: false, }, + initialLoading: { + type: Boolean, + required: false, + default: false, + }, + isDirty: { + type: Boolean, + required: false, + default: false, + }, tracking: { type: Object, required: false, @@ -35,6 +46,11 @@ export default { edit: false, }; }, + computed: { + editButtonText() { + return this.isDirty ? __('Apply') : __('Edit'); + }, + }, destroyed() { window.removeEventListener('click', this.collapseWhenOffClick); window.removeEventListener('keyup', this.collapseOnEscape); @@ -86,15 +102,15 @@ export default { <template> <div> <div class="gl-display-flex gl-align-items-center" @click.self="collapse"> - <span class="hide-collapsed" data-testid="title">{{ title }}</span> - <gl-loading-icon v-if="loading" inline class="gl-ml-2 hide-collapsed" /> + <span class="hide-collapsed" data-testid="title" @click="collapse">{{ title }}</span> + <gl-loading-icon v-if="loading || initialLoading" inline class="gl-ml-2 hide-collapsed" /> <gl-loading-icon v-if="loading && isClassicSidebar" inline class="gl-mx-auto gl-my-0 hide-expanded" /> <gl-button - v-if="canUpdate" + v-if="canUpdate && !initialLoading" variant="link" class="gl-text-gray-900! gl-hover-text-blue-800! gl-ml-auto hide-collapsed" data-testid="edit-button" @@ -105,14 +121,16 @@ export default { @keyup.esc="toggle" @click="toggle" > - {{ __('Edit') }} + {{ editButtonText }} </gl-button> </div> - <div v-show="!edit" data-testid="collapsed-content"> - <slot name="collapsed">{{ __('None') }}</slot> - </div> - <div v-show="edit" data-testid="expanded-content" :class="{ 'gl-mt-3': !isClassicSidebar }"> - <slot :edit="edit"></slot> - </div> + <template v-if="!initialLoading"> + <div v-show="!edit" data-testid="collapsed-content"> + <slot name="collapsed">{{ __('None') }}</slot> + </div> + <div v-show="edit" data-testid="expanded-content" :class="{ 'gl-mt-3': !isClassicSidebar }"> + <slot :edit="edit"></slot> + </div> + </template> </div> </template> diff --git a/app/assets/javascripts/sidebar/constants.js b/app/assets/javascripts/sidebar/constants.js index a0e636488f4..80e07d556bf 100644 --- a/app/assets/javascripts/sidebar/constants.js +++ b/app/assets/javascripts/sidebar/constants.js @@ -1,10 +1,12 @@ import { IssuableType } from '~/issue_show/constants'; import epicConfidentialQuery from '~/sidebar/queries/epic_confidential.query.graphql'; import issueConfidentialQuery from '~/sidebar/queries/issue_confidential.query.graphql'; +import issueDueDateQuery from '~/sidebar/queries/issue_due_date.query.graphql'; import issueReferenceQuery from '~/sidebar/queries/issue_reference.query.graphql'; import mergeRequestReferenceQuery from '~/sidebar/queries/merge_request_reference.query.graphql'; import updateEpicMutation from '~/sidebar/queries/update_epic_confidential.mutation.graphql'; import updateIssueConfidentialMutation from '~/sidebar/queries/update_issue_confidential.mutation.graphql'; +import updateIssueDueDateMutation from '~/sidebar/queries/update_issue_due_date.mutation.graphql'; import getIssueParticipants from '~/vue_shared/components/sidebar/queries/get_issue_participants.query.graphql'; import getMergeRequestParticipants from '~/vue_shared/components/sidebar/queries/get_mr_participants.query.graphql'; import updateAssigneesMutation from '~/vue_shared/components/sidebar/queries/update_issue_assignees.mutation.graphql'; @@ -42,3 +44,10 @@ export const referenceQueries = { query: mergeRequestReferenceQuery, }, }; + +export const dueDateQueries = { + [IssuableType.Issue]: { + query: issueDueDateQuery, + mutation: updateIssueDueDateMutation, + }, +}; diff --git a/app/assets/javascripts/sidebar/mount_sidebar.js b/app/assets/javascripts/sidebar/mount_sidebar.js index b98c8a8c5dc..f5c8ed6bd61 100644 --- a/app/assets/javascripts/sidebar/mount_sidebar.js +++ b/app/assets/javascripts/sidebar/mount_sidebar.js @@ -10,7 +10,10 @@ import { parseBoolean, } from '~/lib/utils/common_utils'; import { __ } from '~/locale'; +import CollapsedAssigneeList from '~/sidebar/components/assignees/collapsed_assignee_list.vue'; +import SidebarAssigneesWidget from '~/sidebar/components/assignees/sidebar_assignees_widget.vue'; import SidebarConfidentialityWidget from '~/sidebar/components/confidential/sidebar_confidentiality_widget.vue'; +import SidebarDueDateWidget from '~/sidebar/components/due_date/sidebar_due_date_widget.vue'; import SidebarReferenceWidget from '~/sidebar/components/reference/sidebar_reference_widget.vue'; import { apolloProvider } from '~/sidebar/graphql'; import Translate from '../vue_shared/translate'; @@ -32,15 +35,6 @@ function getSidebarOptions(sidebarOptEl = document.querySelector('.js-sidebar-op return JSON.parse(sidebarOptEl.innerHTML); } -/** - * Extracts the list of assignees with availability information from a hidden input - * field and converts to a key:value pair for use in the sidebar assignees component. - * The assignee username is used as the key and their busy status is the value - * - * e.g { root: 'busy', admin: '' } - * - * @returns {Object} - */ function getSidebarAssigneeAvailabilityData() { const sidebarAssigneeEl = document.querySelectorAll('.js-sidebar-assignee-data input'); return Array.from(sidebarAssigneeEl) @@ -54,7 +48,7 @@ function getSidebarAssigneeAvailabilityData() { ); } -function mountAssigneesComponent(mediator) { +function mountAssigneesComponentDeprecated(mediator) { const el = document.getElementById('js-vue-sidebar-assignees'); if (!el) return; @@ -86,6 +80,51 @@ function mountAssigneesComponent(mediator) { }); } +function mountAssigneesComponent() { + const el = document.getElementById('js-vue-sidebar-assignees'); + + if (!el) return; + + const { iid, fullPath, editable, projectMembersPath } = getSidebarOptions(); + // eslint-disable-next-line no-new + new Vue({ + el, + apolloProvider, + components: { + SidebarAssigneesWidget, + }, + provide: { + canUpdate: editable, + projectMembersPath, + directlyInviteMembers: el.hasAttribute('data-directly-invite-members'), + indirectlyInviteMembers: el.hasAttribute('data-indirectly-invite-members'), + }, + render: (createElement) => + createElement('sidebar-assignees-widget', { + props: { + iid: String(iid), + fullPath, + issuableType: + isInIssuePage() || isInIncidentPage() || isInDesignPage() + ? IssuableType.Issue + : IssuableType.MergeRequest, + multipleAssignees: !el.dataset.maxAssignees, + }, + scopedSlots: { + collapsed: ({ users, onClick }) => + createElement(CollapsedAssigneeList, { + props: { + users, + }, + nativeOn: { + click: onClick, + }, + }), + }, + }), + }); +} + function mountReviewersComponent(mediator) { const el = document.getElementById('js-vue-sidebar-reviewers'); @@ -168,6 +207,36 @@ function mountConfidentialComponent() { }); } +function mountDueDateComponent() { + const el = document.getElementById('js-due-date-entry-point'); + if (!el) { + return; + } + + const { fullPath, iid, editable } = getSidebarOptions(); + + // eslint-disable-next-line no-new + new Vue({ + el, + apolloProvider, + components: { + SidebarDueDateWidget, + }, + provide: { + iid: String(iid), + fullPath, + canUpdate: editable, + }, + + render: (createElement) => + createElement('sidebar-due-date-widget', { + props: { + issuableType: IssuableType.Issue, + }, + }), + }); +} + function mountReferenceComponent() { const el = document.getElementById('js-reference-entry-point'); if (!el) { @@ -342,9 +411,14 @@ function mountCopyEmailComponent() { } export function mountSidebar(mediator) { - mountAssigneesComponent(mediator); + if (isInIssuePage() || isInDesignPage()) { + mountAssigneesComponent(); + } else { + mountAssigneesComponentDeprecated(mediator); + } mountReviewersComponent(mediator); mountConfidentialComponent(mediator); + mountDueDateComponent(mediator); mountReferenceComponent(mediator); mountLockComponent(); mountParticipantsComponent(mediator); diff --git a/app/assets/javascripts/sidebar/queries/issue_due_date.query.graphql b/app/assets/javascripts/sidebar/queries/issue_due_date.query.graphql new file mode 100644 index 00000000000..6d3f782bd0a --- /dev/null +++ b/app/assets/javascripts/sidebar/queries/issue_due_date.query.graphql @@ -0,0 +1,10 @@ +query issueDueDate($fullPath: ID!, $iid: String) { + workspace: project(fullPath: $fullPath) { + __typename + issuable: issue(iid: $iid) { + __typename + id + dueDate + } + } +} diff --git a/app/assets/javascripts/sidebar/queries/update_issue_due_date.mutation.graphql b/app/assets/javascripts/sidebar/queries/update_issue_due_date.mutation.graphql new file mode 100644 index 00000000000..cf7eccd61c7 --- /dev/null +++ b/app/assets/javascripts/sidebar/queries/update_issue_due_date.mutation.graphql @@ -0,0 +1,9 @@ +mutation updateIssueDueDate($input: UpdateIssueInput!) { + issuableSetDueDate: updateIssue(input: $input) { + issuable: issue { + id + dueDate + } + errors + } +} diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/author_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/author_token.vue index d53c829a48e..aeb698a3adb 100644 --- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/author_token.vue +++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/author_token.vue @@ -45,6 +45,9 @@ export default { activeAuthor() { return this.authors.find((author) => author.username.toLowerCase() === this.currentValue); }, + activeAuthorAvatar() { + return this.avatarUrl(this.activeAuthor); + }, }, watch: { active: { @@ -74,6 +77,9 @@ export default { this.loading = false; }); }, + avatarUrl(author) { + return author.avatarUrl || author.avatar_url; + }, searchAuthors: debounce(function debouncedSearch({ data }) { this.fetchAuthorBySearchTerm(data); }, DEBOUNCE_DELAY), @@ -92,7 +98,7 @@ export default { <gl-avatar v-if="activeAuthor" :size="16" - :src="activeAuthor.avatar_url" + :src="activeAuthorAvatar" shape="circle" class="gl-mr-2" /> @@ -115,7 +121,7 @@ export default { :value="author.username" > <div class="d-flex"> - <gl-avatar :size="32" :src="author.avatar_url" /> + <gl-avatar :size="32" :src="avatarUrl(author)" /> <div> <div>{{ author.name }}</div> <div>@{{ author.username }}</div> diff --git a/app/assets/javascripts/vue_shared/components/sidebar/multiselect_dropdown.vue b/app/assets/javascripts/vue_shared/components/sidebar/multiselect_dropdown.vue index ef5f052527b..17904f20341 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/multiselect_dropdown.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/multiselect_dropdown.vue @@ -30,5 +30,8 @@ export default { <gl-dropdown-form> <slot name="items"></slot> </gl-dropdown-form> + <template #footer> + <slot name="footer"></slot> + </template> </gl-dropdown> </template> diff --git a/app/assets/javascripts/vue_shared/components/sidebar/queries/get_issue_participants.query.graphql b/app/assets/javascripts/vue_shared/components/sidebar/queries/get_issue_participants.query.graphql index 459ea27e9cd..3885127fa8e 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/queries/get_issue_participants.query.graphql +++ b/app/assets/javascripts/vue_shared/components/sidebar/queries/get_issue_participants.query.graphql @@ -1,4 +1,5 @@ #import "~/graphql_shared/fragments/user.fragment.graphql" +#import "~/graphql_shared/fragments/user_availability.fragment.graphql" query issueParticipants($fullPath: ID!, $iid: String!) { workspace: project(fullPath: $fullPath) { @@ -9,11 +10,13 @@ query issueParticipants($fullPath: ID!, $iid: String!) { participants { nodes { ...User + ...UserAvailability } } assignees { nodes { ...User + ...UserAvailability } } } diff --git a/app/assets/javascripts/vue_shared/components/sidebar/queries/get_mr_participants.query.graphql b/app/assets/javascripts/vue_shared/components/sidebar/queries/get_mr_participants.query.graphql index 43bd9f17e9a..63482873b69 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/queries/get_mr_participants.query.graphql +++ b/app/assets/javascripts/vue_shared/components/sidebar/queries/get_mr_participants.query.graphql @@ -1,4 +1,5 @@ #import "~/graphql_shared/fragments/user.fragment.graphql" +#import "~/graphql_shared/fragments/user_availability.fragment.graphql" query getMrParticipants($fullPath: ID!, $iid: String!) { workspace: project(fullPath: $fullPath) { @@ -7,11 +8,13 @@ query getMrParticipants($fullPath: ID!, $iid: String!) { participants { nodes { ...User + ...UserAvailability } } assignees { nodes { ...User + ...UserAvailability } } } diff --git a/app/assets/javascripts/vue_shared/components/sidebar/queries/update_issue_assignees.mutation.graphql b/app/assets/javascripts/vue_shared/components/sidebar/queries/update_issue_assignees.mutation.graphql index 8ee8de2cb5c..3f40c0368d7 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/queries/update_issue_assignees.mutation.graphql +++ b/app/assets/javascripts/vue_shared/components/sidebar/queries/update_issue_assignees.mutation.graphql @@ -1,4 +1,5 @@ #import "~/graphql_shared/fragments/user.fragment.graphql" +#import "~/graphql_shared/fragments/user_availability.fragment.graphql" mutation issueSetAssignees($iid: String!, $assigneeUsernames: [String!]!, $fullPath: ID!) { issuableSetAssignees: issueSetAssignees( @@ -9,11 +10,13 @@ mutation issueSetAssignees($iid: String!, $assigneeUsernames: [String!]!, $fullP assignees { nodes { ...User + ...UserAvailability } } participants { nodes { ...User + ...UserAvailability } } } diff --git a/app/assets/javascripts/vue_shared/components/sidebar/queries/update_mr_assignees.mutation.graphql b/app/assets/javascripts/vue_shared/components/sidebar/queries/update_mr_assignees.mutation.graphql index a0f15a07692..77140ea36d8 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/queries/update_mr_assignees.mutation.graphql +++ b/app/assets/javascripts/vue_shared/components/sidebar/queries/update_mr_assignees.mutation.graphql @@ -1,4 +1,5 @@ #import "~/graphql_shared/fragments/user.fragment.graphql" +#import "~/graphql_shared/fragments/user_availability.fragment.graphql" mutation mergeRequestSetAssignees($iid: String!, $assigneeUsernames: [String!]!, $fullPath: ID!) { mergeRequestSetAssignees( @@ -9,11 +10,13 @@ mutation mergeRequestSetAssignees($iid: String!, $assigneeUsernames: [String!]!, assignees { nodes { ...User + ...UserAvailability } } participants { nodes { ...User + ...UserAvailability } } } |