Commit e228b0bd authored by Natalia Tepluhina's avatar Natalia Tepluhina

Merge branch '231401-epic-boards-participants-sidebar-widget' into 'master'

Epic boards - Display epic participants in sidebar

See merge request gitlab-org/gitlab!60885
parents 085a0f39 3da6019d
......@@ -95,7 +95,7 @@ export default {
<gl-loading-icon v-if="loading" />
<span v-else data-testid="collapsed-count"> {{ participantCount }} </span>
</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" />
{{ participantLabel }}
</div>
......@@ -105,10 +105,10 @@ export default {
:key="participant.id"
class="participants-author"
>
<a :href="participant.web_url" class="author-link">
<a :href="participant.web_url || participant.webUrl" class="author-link">
<user-avatar-image
:lazy="true"
:img-src="participant.avatar_url"
:img-src="participant.avatar_url || participant.avatarUrl"
:size="24"
:tooltip-text="participant.name"
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 epicConfidentialQuery from '~/sidebar/queries/epic_confidential.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 epicSubscribedQuery from '~/sidebar/queries/epic_subscribed.query.graphql';
import issuableAssigneesSubscription from '~/sidebar/queries/issuable_assignees.subscription.graphql';
......@@ -46,6 +47,9 @@ export const participantsQueries = {
[IssuableType.MergeRequest]: {
query: getMergeRequestParticipants,
},
[IssuableType.Epic]: {
query: epicParticipantsQuery,
},
};
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 @@
.title {
color: $gl-text-color;
margin-bottom: $gl-padding-4;
line-height: $gl-line-height-20;
.avatar {
......
......@@ -7,6 +7,7 @@ import { ISSUABLE } from '~/boards/constants';
import { contentTop } from '~/lib/utils/common_utils';
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';
export default {
......@@ -17,6 +18,7 @@ export default {
BoardSidebarTitle,
SidebarConfidentialityWidget,
SidebarDateWidget,
SidebarParticipantsWidget,
SidebarSubscriptionsWidget,
},
computed: {
......@@ -63,6 +65,11 @@ export default {
:can-inherit="true"
/>
<board-sidebar-labels-select class="labels" />
<sidebar-participants-widget
:iid="activeBoardItem.iid"
:full-path="fullPath"
issuable-type="epic"
/>
<sidebar-confidentiality-widget
:iid="activeBoardItem.iid"
:full-path="fullPath"
......
......@@ -4,8 +4,11 @@ import Vuex from 'vuex';
import EpicBoardContentSidebar from 'ee_component/boards/components/epic_board_content_sidebar.vue';
import { stubComponent } from 'helpers/stub_component';
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 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 { mockEpic } from '../mock_data';
......@@ -59,30 +62,42 @@ describe('EpicBoardContentSidebar', () => {
});
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', () => {
createStore({ mockGetters: { isSidebarOpen: () => false } });
createComponent();
expect(wrapper.find(GlDrawer).exists()).toBe(false);
expect(wrapper.findComponent(GlDrawer).exists()).toBe(false);
});
it('applies an open attribute', () => {
expect(wrapper.find(GlDrawer).props('open')).toBe(true);
expect(wrapper.findComponent(GlDrawer).props('open')).toBe(true);
});
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', () => {
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', () => {
expect(wrapper.find(SidebarSubscriptionsWidget).exists()).toBe(true);
expect(wrapper.findComponent(SidebarSubscriptionsWidget).exists()).toBe(true);
});
describe('when we emit close', () => {
......
......@@ -3496,6 +3496,9 @@ msgstr ""
msgid "An error occurred while fetching markdown preview"
msgstr ""
msgid "An error occurred while fetching participants"
msgstr ""
msgid "An error occurred while fetching participants."
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) => ({
},
});
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) => ({
data: {
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