Commit 03ad81e0 authored by Tom Quirk's avatar Tom Quirk Committed by Kushal Pandya

Add jira issue assignees to sidebar

- uses gitlab ui components to show Jira
issue assignee
- add specs
- move sidebar to own directory
parent 677c52f6
<script> <script>
import { GlAlert, GlSprintf, GlLink } from '@gitlab/ui'; import { GlAlert, GlSprintf, GlLink } from '@gitlab/ui';
import { fetchIssue } from 'ee/integrations/jira/issues_show/api'; import { fetchIssue } from 'ee/integrations/jira/issues_show/api';
import Sidebar from 'ee/integrations/jira/issues_show/components/sidebar.vue'; import JiraIssueSidebar from 'ee/integrations/jira/issues_show/components/sidebar/jira_issues_sidebar_root.vue';
import { issueStates, issueStateLabels } from 'ee/integrations/jira/issues_show/constants'; import { issueStates, issueStateLabels } from 'ee/integrations/jira/issues_show/constants';
import IssuableShow from '~/issuable_show/components/issuable_show_root.vue'; import IssuableShow from '~/issuable_show/components/issuable_show_root.vue';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
export default { export default {
...@@ -14,7 +13,7 @@ export default { ...@@ -14,7 +13,7 @@ export default {
GlSprintf, GlSprintf,
GlLink, GlLink,
IssuableShow, IssuableShow,
Sidebar, JiraIssueSidebar,
}, },
inject: { inject: {
issuesShowPath: { issuesShowPath: {
...@@ -81,7 +80,7 @@ export default { ...@@ -81,7 +80,7 @@ export default {
<template #status-badge>{{ statusBadgeText }}</template> <template #status-badge>{{ statusBadgeText }}</template>
<template #right-sidebar-items="{ sidebarExpanded }"> <template #right-sidebar-items="{ sidebarExpanded }">
<sidebar :sidebar-expanded="sidebarExpanded" :selected-labels="issue.labels" /> <jira-issue-sidebar :sidebar-expanded="sidebarExpanded" :issue="issue" />
</template> </template>
</issuable-show> </issuable-show>
</div> </div>
......
<script>
import { GlAvatarLabeled, GlAvatarLink, GlAvatar, GlIcon, GlTooltipDirective } from '@gitlab/ui';
import { __ } from '~/locale';
import AssigneeTitle from '~/sidebar/components/assignees/assignee_title.vue';
export default {
name: 'JiraIssuesSidebarAssignee',
components: {
GlAvatarLabeled,
GlAvatarLink,
GlAvatar,
GlIcon,
AssigneeTitle,
},
directives: {
GlTooltip: GlTooltipDirective,
},
props: {
assignee: {
type: Object,
required: false,
default: null,
},
},
computed: {
tooltipTitle() {
return this.assignee?.name || __('No assignee');
},
numberOfAssignees() {
return this.assignee ? 1 : 0;
},
},
tooltipOptions: {
container: 'body',
placement: 'left',
boundary: 'viewport',
},
};
</script>
<template>
<div>
<div class="hide-collapsed">
<assignee-title
:number-of-assignees="numberOfAssignees"
:editable="false"
:show-toggle="false"
:changing="false"
/>
<gl-avatar-link
v-if="assignee"
v-gl-tooltip="$options.tooltipOptions"
target="_blank"
:href="assignee.webUrl"
:title="tooltipTitle"
>
<gl-avatar-labeled
:size="32"
:src="assignee.avatarUrl"
:alt="assignee.name"
:entity-name="assignee.name"
:label="assignee.name"
:sub-label="__('Jira user')"
/>
</gl-avatar-link>
<span v-else class="gl-text-gray-500" data-testid="no-assignee-text">{{ __('None') }}</span>
</div>
<div
v-gl-tooltip="$options.tooltipOptions"
class="sidebar-collapsed-icon"
:title="tooltipTitle"
data-testid="sidebar-collapsed-icon-wrapper"
>
<gl-avatar
v-if="assignee"
:size="24"
:src="assignee.avatarUrl"
:alt="assignee.name"
:entity-name="assignee.name"
/>
<gl-icon v-else name="user" data-testid="no-assignee-icon" />
</div>
</div>
</template>
<script> <script>
import LabelsSelect from '~/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue'; import LabelsSelect from '~/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue';
import Assignee from './assignee.vue';
export default { export default {
name: 'JiraIssuesSidebar',
components: { components: {
Assignee,
LabelsSelect, LabelsSelect,
}, },
props: { props: {
...@@ -10,19 +13,30 @@ export default { ...@@ -10,19 +13,30 @@ export default {
type: Boolean, type: Boolean,
required: true, required: true,
}, },
selectedLabels: { issue: {
type: Array, type: Object,
required: true, required: true,
}, },
}, },
computed: {
assignee() {
// Jira issues have at most 1 assignee
return (this.issue?.assignees || [])[0];
},
},
}; };
</script> </script>
<template> <template>
<div>
<assignee class="block" :assignee="assignee" />
<labels-select <labels-select
:selected-labels="selectedLabels" :selected-labels="issue.labels"
variant="sidebar" variant="sidebar"
class="block labels js-labels-block" class="block labels js-labels-block"
>{{ __('None') }}</labels-select
> >
{{ __('None') }}
</labels-select>
</div>
</template> </template>
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`JiraIssuesSidebarAssignee with assignee template renders avatar components 1`] = `
<div>
<div
class="hide-collapsed"
>
<assignee-title-stub
numberofassignees="1"
/>
<gl-avatar-link-stub
href="http://127.0.0.1:3000/root"
target="_blank"
title="Justin Ho"
>
<gl-avatar-labeled-stub
alt="Justin Ho"
entity-name="Justin Ho"
label="Justin Ho"
labellink=""
size="32"
src="http://127.0.0.1:3000/uploads/-/system/user/avatar/1/avatar.png?width=90"
sublabel="Jira user"
sublabellink=""
/>
</gl-avatar-link-stub>
</div>
<div
class="sidebar-collapsed-icon"
data-testid="sidebar-collapsed-icon-wrapper"
title="Justin Ho"
>
<gl-avatar-stub
alt="Justin Ho"
entityid="0"
entityname="Justin Ho"
shape="circle"
size="24"
src="http://127.0.0.1:3000/uploads/-/system/user/avatar/1/avatar.png?width=90"
/>
</div>
</div>
`;
exports[`JiraIssuesSidebarAssignee with no assignee template renders template without avatar components (the "None" state) 1`] = `
<div>
<div
class="hide-collapsed"
>
<assignee-title-stub
numberofassignees="0"
/>
<span
class="gl-text-gray-500"
data-testid="no-assignee-text"
>
None
</span>
</div>
<div
class="sidebar-collapsed-icon"
data-testid="sidebar-collapsed-icon-wrapper"
title="No assignee"
>
<gl-icon-stub
data-testid="no-assignee-icon"
name="user"
size="16"
/>
</div>
</div>
`;
import { GlAvatarLabeled, GlAvatarLink, GlAvatar } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Assignee from 'ee/integrations/jira/issues_show/components/sidebar/assignee.vue';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import AssigneeTitle from '~/sidebar/components/assignees/assignee_title.vue';
import { mockJiraIssue } from '../../mock_data';
const mockAssignee = convertObjectPropsToCamelCase(mockJiraIssue.assignees[0], { deep: true });
describe('JiraIssuesSidebarAssignee', () => {
let wrapper;
const findNoAssigneeText = () => wrapper.findByTestId('no-assignee-text');
const findNoAssigneeIcon = () => wrapper.findByTestId('no-assignee-text');
const findAvatar = () => wrapper.find(GlAvatar);
const findAvatarLabeled = () => wrapper.find(GlAvatarLabeled);
const findAvatarLink = () => wrapper.find(GlAvatarLink);
const findSidebarCollapsedIconWrapper = () =>
wrapper.findByTestId('sidebar-collapsed-icon-wrapper');
const createComponent = ({ assignee } = {}) => {
wrapper = extendedWrapper(
shallowMount(Assignee, {
propsData: {
assignee,
},
}),
);
};
afterEach(() => {
if (wrapper) {
wrapper.destroy();
wrapper = null;
}
});
describe('with assignee', () => {
beforeEach(() => {
createComponent({ assignee: mockAssignee });
});
describe('template', () => {
it('renders avatar components', () => {
expect(wrapper.element).toMatchSnapshot();
});
it('renders GlAvatarLink with correct props', () => {
const avatarLink = findAvatarLink();
expect(avatarLink.exists()).toBe(true);
expect(avatarLink.attributes()).toMatchObject({
href: mockAssignee.webUrl,
title: mockAssignee.name,
});
});
it('renders GlAvatarLabeled with correct props', () => {
const avatarLabeled = findAvatarLabeled();
expect(avatarLabeled.exists()).toBe(true);
expect(avatarLabeled.attributes()).toMatchObject({
src: mockAssignee.avatarUrl,
alt: mockAssignee.name,
'entity-name': mockAssignee.name,
});
expect(avatarLabeled.props('label')).toBe(mockAssignee.name);
});
it('renders GlAvatar with correct props', () => {
const avatar = findAvatar();
expect(avatar.exists()).toBe(true);
expect(avatar.attributes()).toMatchObject({
src: mockAssignee.avatarUrl,
alt: mockAssignee.name,
});
expect(avatar.props('entityName')).toBe(mockAssignee.name);
});
it('renders AssigneeTitle with correct props', () => {
const title = wrapper.find(AssigneeTitle);
expect(title.exists()).toBe(true);
expect(title.props('numberOfAssignees')).toBe(1);
});
it('does not render "No assignee" text', () => {
expect(findNoAssigneeText().exists()).toBe(false);
});
it('does not render "No assignee" icon', () => {
expect(findNoAssigneeIcon().exists()).toBe(false);
});
it('sets `title` attribute of collapsed sidebar wrapper correctly', () => {
const iconWrapper = findSidebarCollapsedIconWrapper();
expect(iconWrapper.attributes('title')).toBe(mockAssignee.name);
});
});
});
describe('with no assignee', () => {
beforeEach(() => {
createComponent({ assignee: undefined });
});
describe('template', () => {
it('renders template without avatar components (the "None" state)', () => {
expect(wrapper.element).toMatchSnapshot();
});
it('sets `title` attribute of collapsed sidebar wrapper correctly', () => {
const iconWrapper = findSidebarCollapsedIconWrapper();
expect(iconWrapper.attributes('title')).toBe('No assignee');
});
});
});
});
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import Assignee from 'ee/integrations/jira/issues_show/components/sidebar/assignee.vue';
import Sidebar from 'ee/integrations/jira/issues_show/components/sidebar.vue'; import Sidebar from 'ee/integrations/jira/issues_show/components/sidebar/jira_issues_sidebar_root.vue';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import LabelsSelect from '~/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue'; import LabelsSelect from '~/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue';
import { mockJiraIssue as mockJiraIssueData } from '../../mock_data';
import { mockJiraIssue } from '../mock_data'; const mockJiraIssue = convertObjectPropsToCamelCase(mockJiraIssueData, { deep: true });
describe('Sidebar', () => { describe('JiraIssuesSidebar', () => {
let wrapper; let wrapper;
const defaultProps = { const defaultProps = {
sidebarExpanded: false, sidebarExpanded: false,
selectedLabels: mockJiraIssue.labels, issue: mockJiraIssue,
}; };
const createComponent = () => { const createComponent = () => {
...@@ -27,12 +29,20 @@ describe('Sidebar', () => { ...@@ -27,12 +29,20 @@ describe('Sidebar', () => {
}); });
const findLabelsSelect = () => wrapper.findComponent(LabelsSelect); const findLabelsSelect = () => wrapper.findComponent(LabelsSelect);
const findAssignee = () => wrapper.findComponent(Assignee);
it('renders LabelsSelect', async () => { it('renders Labels block', async () => {
createComponent(); createComponent();
await wrapper.vm.$nextTick();
expect(findLabelsSelect().exists()).toBe(true); expect(findLabelsSelect().exists()).toBe(true);
expect(findLabelsSelect().props('selectedLabels')).toEqual(mockJiraIssue.labels);
});
it('renders Assignee block', async () => {
createComponent();
const assignee = findAssignee();
expect(assignee.exists()).toBe(true);
expect(assignee.props('assignee')).toEqual(mockJiraIssue.assignees[0]);
}); });
}); });
...@@ -5,12 +5,17 @@ export const mockJiraIssue = { ...@@ -5,12 +5,17 @@ export const mockJiraIssue = {
'<a href="https://jira.reali.sh:8080/projects/FE/issues/FE-2">FE-2</a> The second FE issue on Jira', '<a href="https://jira.reali.sh:8080/projects/FE/issues/FE-2">FE-2</a> The second FE issue on Jira',
created_at: '"2021-02-01T04:04:40.833Z"', created_at: '"2021-02-01T04:04:40.833Z"',
author: { author: {
id: 2,
username: 'justin_ho',
name: 'Justin Ho', name: 'Justin Ho',
web_url: 'http://127.0.0.1:3000/root', web_url: 'http://127.0.0.1:3000/root',
avatar_url: 'http://127.0.0.1:3000/uploads/-/system/user/avatar/1/avatar.png?width=90', avatar_url: 'http://127.0.0.1:3000/uploads/-/system/user/avatar/1/avatar.png?width=90',
}, },
assignees: [
{
name: 'Justin Ho',
web_url: 'http://127.0.0.1:3000/root',
avatar_url: 'http://127.0.0.1:3000/uploads/-/system/user/avatar/1/avatar.png?width=90',
},
],
labels: [ labels: [
{ {
title: 'In Progress', title: 'In Progress',
......
...@@ -16807,6 +16807,9 @@ msgstr "" ...@@ -16807,6 +16807,9 @@ msgstr ""
msgid "Jira service not configured." msgid "Jira service not configured."
msgstr "" msgstr ""
msgid "Jira user"
msgstr ""
msgid "Jira users have been imported from the configured Jira instance. They can be mapped by selecting a GitLab user from the dropdown in the \"GitLab username\" column. When the form appears, the dropdown defaults to the user conducting the import." msgid "Jira users have been imported from the configured Jira instance. They can be mapped by selecting a GitLab user from the dropdown in the \"GitLab username\" column. When the form appears, the dropdown defaults to the user conducting the import."
msgstr "" msgstr ""
...@@ -20079,6 +20082,9 @@ msgstr "" ...@@ -20079,6 +20082,9 @@ msgstr ""
msgid "No application_settings found" msgid "No application_settings found"
msgstr "" msgstr ""
msgid "No assignee"
msgstr ""
msgid "No authentication methods configured." msgid "No authentication methods configured."
msgstr "" msgstr ""
......
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