Commit 3da6019d authored by Florie Guibert's avatar Florie Guibert Committed by Natalia Tepluhina

Epic boards - Display epic participants in sidebar

parent 6f0bab97
...@@ -95,7 +95,7 @@ export default { ...@@ -95,7 +95,7 @@ export default {
<gl-loading-icon v-if="loading" /> <gl-loading-icon v-if="loading" />
<span v-else data-testid="collapsed-count"> {{ participantCount }} </span> <span v-else data-testid="collapsed-count"> {{ participantCount }} </span>
</div> </div>
<div v-if="showParticipantLabel" class="title hide-collapsed"> <div v-if="showParticipantLabel" class="title hide-collapsed gl-mb-2">
<gl-loading-icon v-if="loading" :inline="true" /> <gl-loading-icon v-if="loading" :inline="true" />
{{ participantLabel }} {{ participantLabel }}
</div> </div>
...@@ -105,10 +105,10 @@ export default { ...@@ -105,10 +105,10 @@ export default {
:key="participant.id" :key="participant.id"
class="participants-author" class="participants-author"
> >
<a :href="participant.web_url" class="author-link"> <a :href="participant.web_url || participant.webUrl" class="author-link">
<user-avatar-image <user-avatar-image
:lazy="true" :lazy="true"
:img-src="participant.avatar_url" :img-src="participant.avatar_url || participant.avatarUrl"
:size="24" :size="24"
:tooltip-text="participant.name" :tooltip-text="participant.name"
css-classes="avatar-inline" css-classes="avatar-inline"
......
<script>
import { __ } from '~/locale';
import { participantsQueries } from '~/sidebar/constants';
import Participants from './participants.vue';
export default {
i18n: {
fetchingError: __('An error occurred while fetching participants'),
},
components: {
Participants,
},
props: {
iid: {
type: String,
required: true,
},
fullPath: {
type: String,
required: true,
},
issuableType: {
required: true,
type: String,
},
},
data() {
return {
participants: [],
};
},
apollo: {
participants: {
query() {
return participantsQueries[this.issuableType].query;
},
variables() {
return {
fullPath: this.fullPath,
iid: this.iid,
};
},
update(data) {
return data.workspace?.issuable?.participants.nodes || [];
},
error(error) {
this.$emit('fetch-error', {
message: this.$options.i18n.fetchingError,
error,
});
},
},
},
computed: {
isLoading() {
return this.$apollo.queries.participants.loading;
},
},
};
</script>
<template>
<participants
:loading="isLoading"
:participants="participants"
:number-of-less-participants="7"
/>
</template>
import { IssuableType } from '~/issue_show/constants'; import { IssuableType } from '~/issue_show/constants';
import epicConfidentialQuery from '~/sidebar/queries/epic_confidential.query.graphql'; import epicConfidentialQuery from '~/sidebar/queries/epic_confidential.query.graphql';
import epicDueDateQuery from '~/sidebar/queries/epic_due_date.query.graphql'; import epicDueDateQuery from '~/sidebar/queries/epic_due_date.query.graphql';
import epicParticipantsQuery from '~/sidebar/queries/epic_participants.query.graphql';
import epicStartDateQuery from '~/sidebar/queries/epic_start_date.query.graphql'; import epicStartDateQuery from '~/sidebar/queries/epic_start_date.query.graphql';
import epicSubscribedQuery from '~/sidebar/queries/epic_subscribed.query.graphql'; import epicSubscribedQuery from '~/sidebar/queries/epic_subscribed.query.graphql';
import issuableAssigneesSubscription from '~/sidebar/queries/issuable_assignees.subscription.graphql'; import issuableAssigneesSubscription from '~/sidebar/queries/issuable_assignees.subscription.graphql';
...@@ -46,6 +47,9 @@ export const participantsQueries = { ...@@ -46,6 +47,9 @@ export const participantsQueries = {
[IssuableType.MergeRequest]: { [IssuableType.MergeRequest]: {
query: getMergeRequestParticipants, query: getMergeRequestParticipants,
}, },
[IssuableType.Epic]: {
query: epicParticipantsQuery,
},
}; };
export const confidentialityQueries = { export const confidentialityQueries = {
......
#import "~/graphql_shared/fragments/user.fragment.graphql"
#import "~/graphql_shared/fragments/user_availability.fragment.graphql"
query epicParticipants($fullPath: ID!, $iid: ID) {
workspace: group(fullPath: $fullPath) {
__typename
issuable: epic(iid: $iid) {
__typename
id
participants {
nodes {
...User
...UserAvailability
}
}
}
}
}
...@@ -217,7 +217,6 @@ ...@@ -217,7 +217,6 @@
.title { .title {
color: $gl-text-color; color: $gl-text-color;
margin-bottom: $gl-padding-4;
line-height: $gl-line-height-20; line-height: $gl-line-height-20;
.avatar { .avatar {
......
...@@ -7,6 +7,7 @@ import { ISSUABLE } from '~/boards/constants'; ...@@ -7,6 +7,7 @@ import { ISSUABLE } from '~/boards/constants';
import { contentTop } from '~/lib/utils/common_utils'; import { contentTop } from '~/lib/utils/common_utils';
import SidebarConfidentialityWidget from '~/sidebar/components/confidential/sidebar_confidentiality_widget.vue'; import SidebarConfidentialityWidget from '~/sidebar/components/confidential/sidebar_confidentiality_widget.vue';
import SidebarDateWidget from '~/sidebar/components/date/sidebar_date_widget.vue'; import SidebarDateWidget from '~/sidebar/components/date/sidebar_date_widget.vue';
import SidebarParticipantsWidget from '~/sidebar/components/participants/sidebar_participants_widget.vue';
import SidebarSubscriptionsWidget from '~/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue'; import SidebarSubscriptionsWidget from '~/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue';
export default { export default {
...@@ -17,6 +18,7 @@ export default { ...@@ -17,6 +18,7 @@ export default {
BoardSidebarTitle, BoardSidebarTitle,
SidebarConfidentialityWidget, SidebarConfidentialityWidget,
SidebarDateWidget, SidebarDateWidget,
SidebarParticipantsWidget,
SidebarSubscriptionsWidget, SidebarSubscriptionsWidget,
}, },
computed: { computed: {
...@@ -63,6 +65,11 @@ export default { ...@@ -63,6 +65,11 @@ export default {
:can-inherit="true" :can-inherit="true"
/> />
<board-sidebar-labels-select class="labels" /> <board-sidebar-labels-select class="labels" />
<sidebar-participants-widget
:iid="activeBoardItem.iid"
:full-path="fullPath"
issuable-type="epic"
/>
<sidebar-confidentiality-widget <sidebar-confidentiality-widget
:iid="activeBoardItem.iid" :iid="activeBoardItem.iid"
:full-path="fullPath" :full-path="fullPath"
......
...@@ -4,8 +4,11 @@ import Vuex from 'vuex'; ...@@ -4,8 +4,11 @@ import Vuex from 'vuex';
import EpicBoardContentSidebar from 'ee_component/boards/components/epic_board_content_sidebar.vue'; import EpicBoardContentSidebar from 'ee_component/boards/components/epic_board_content_sidebar.vue';
import { stubComponent } from 'helpers/stub_component'; import { stubComponent } from 'helpers/stub_component';
import BoardSidebarLabelsSelect from '~/boards/components/sidebar/board_sidebar_labels_select.vue'; import BoardSidebarLabelsSelect from '~/boards/components/sidebar/board_sidebar_labels_select.vue';
import BoardSidebarTitle from '~/boards/components/sidebar/board_sidebar_title.vue';
import { ISSUABLE } from '~/boards/constants'; import { ISSUABLE } from '~/boards/constants';
import SidebarConfidentialityWidget from '~/sidebar/components/confidential/sidebar_confidentiality_widget.vue'; import SidebarConfidentialityWidget from '~/sidebar/components/confidential/sidebar_confidentiality_widget.vue';
import SidebarDateWidget from '~/sidebar/components/date/sidebar_date_widget.vue';
import SidebarParticipantsWidget from '~/sidebar/components/participants/sidebar_participants_widget.vue';
import SidebarSubscriptionsWidget from '~/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue'; import SidebarSubscriptionsWidget from '~/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue';
import { mockEpic } from '../mock_data'; import { mockEpic } from '../mock_data';
...@@ -59,30 +62,42 @@ describe('EpicBoardContentSidebar', () => { ...@@ -59,30 +62,42 @@ describe('EpicBoardContentSidebar', () => {
}); });
it('confirms we render GlDrawer', () => { it('confirms we render GlDrawer', () => {
expect(wrapper.find(GlDrawer).exists()).toBe(true); expect(wrapper.findComponent(GlDrawer).exists()).toBe(true);
}); });
it('does not render GlDrawer when isSidebarOpen is false', () => { it('does not render GlDrawer when isSidebarOpen is false', () => {
createStore({ mockGetters: { isSidebarOpen: () => false } }); createStore({ mockGetters: { isSidebarOpen: () => false } });
createComponent(); createComponent();
expect(wrapper.find(GlDrawer).exists()).toBe(false); expect(wrapper.findComponent(GlDrawer).exists()).toBe(false);
}); });
it('applies an open attribute', () => { it('applies an open attribute', () => {
expect(wrapper.find(GlDrawer).props('open')).toBe(true); expect(wrapper.findComponent(GlDrawer).props('open')).toBe(true);
}); });
it('renders BoardSidebarLabelsSelect', () => { it('renders BoardSidebarLabelsSelect', () => {
expect(wrapper.find(BoardSidebarLabelsSelect).exists()).toBe(true); expect(wrapper.findComponent(BoardSidebarLabelsSelect).exists()).toBe(true);
});
it('renders BoardSidebarTitle', () => {
expect(wrapper.findComponent(BoardSidebarTitle).exists()).toBe(true);
}); });
it('renders SidebarConfidentialityWidget', () => { it('renders SidebarConfidentialityWidget', () => {
expect(wrapper.find(SidebarConfidentialityWidget).exists()).toBe(true); expect(wrapper.findComponent(SidebarConfidentialityWidget).exists()).toBe(true);
});
it('renders 2 SidebarDateWidget', () => {
expect(wrapper.findAll(SidebarDateWidget)).toHaveLength(2);
});
it('renders SidebarParticipantsWidget', () => {
expect(wrapper.findComponent(SidebarParticipantsWidget).exists()).toBe(true);
}); });
it('renders SidebarSubscriptionsWidget', () => { it('renders SidebarSubscriptionsWidget', () => {
expect(wrapper.find(SidebarSubscriptionsWidget).exists()).toBe(true); expect(wrapper.findComponent(SidebarSubscriptionsWidget).exists()).toBe(true);
}); });
describe('when we emit close', () => { describe('when we emit close', () => {
......
...@@ -3490,6 +3490,9 @@ msgstr "" ...@@ -3490,6 +3490,9 @@ msgstr ""
msgid "An error occurred while fetching markdown preview" msgid "An error occurred while fetching markdown preview"
msgstr "" msgstr ""
msgid "An error occurred while fetching participants"
msgstr ""
msgid "An error occurred while fetching participants." msgid "An error occurred while fetching participants."
msgstr "" msgstr ""
......
import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import Participants from '~/sidebar/components/participants/participants.vue';
import SidebarParticipantsWidget from '~/sidebar/components/participants/sidebar_participants_widget.vue';
import epicParticipantsQuery from '~/sidebar/queries/epic_participants.query.graphql';
import { epicParticipantsResponse } from '../../mock_data';
Vue.use(VueApollo);
describe('Sidebar Participants Widget', () => {
let wrapper;
let fakeApollo;
const findParticipants = () => wrapper.findComponent(Participants);
const createComponent = ({
participantsQueryHandler = jest.fn().mockResolvedValue(epicParticipantsResponse()),
} = {}) => {
fakeApollo = createMockApollo([[epicParticipantsQuery, participantsQueryHandler]]);
wrapper = shallowMount(SidebarParticipantsWidget, {
apolloProvider: fakeApollo,
propsData: {
fullPath: 'group',
iid: '1',
issuableType: 'epic',
},
stubs: {
Participants,
},
});
};
afterEach(() => {
wrapper.destroy();
fakeApollo = null;
});
it('passes a `loading` prop as true to child component when query is loading', () => {
createComponent();
expect(findParticipants().props('loading')).toBe(true);
});
describe('when participants are loaded', () => {
beforeEach(() => {
createComponent({
participantsQueryHandler: jest.fn().mockResolvedValue(epicParticipantsResponse()),
});
return waitForPromises();
});
it('passes a `loading` prop as false to editable item', () => {
expect(findParticipants().props('loading')).toBe(false);
});
it('passes participants to child component', () => {
expect(findParticipants().props('participants')).toEqual(
epicParticipantsResponse().data.workspace.issuable.participants.nodes,
);
});
});
describe('when error occurs', () => {
it('emits error event with correct parameters', async () => {
const mockError = new Error('mayday');
createComponent({
participantsQueryHandler: jest.fn().mockRejectedValue(mockError),
});
await waitForPromises();
const [
[
{
message,
error: { networkError },
},
],
] = wrapper.emitted('fetch-error');
expect(message).toBe(wrapper.vm.$options.i18n.fetchingError);
expect(networkError).toEqual(mockError);
});
});
});
...@@ -262,6 +262,31 @@ export const issuableStartDateResponse = (startDate = null) => ({ ...@@ -262,6 +262,31 @@ export const issuableStartDateResponse = (startDate = null) => ({
}, },
}); });
export const epicParticipantsResponse = () => ({
data: {
workspace: {
__typename: 'Group',
issuable: {
__typename: 'Epic',
id: 'gid://gitlab/Epic/4',
participants: {
nodes: [
{
id: 'gid://gitlab/User/2',
avatarUrl:
'https://www.gravatar.com/avatar/a95e5b71488f4b9d69ce5ff58bfd28d6?s=80\u0026d=identicon',
name: 'Jacki Kub',
username: 'francina.skiles',
webUrl: '/franc',
status: null,
},
],
},
},
},
},
});
export const issueReferenceResponse = (reference) => ({ export const issueReferenceResponse = (reference) => ({
data: { data: {
workspace: { workspace: {
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment