Commit 6b7336ac authored by Kushal Pandya's avatar Kushal Pandya

Merge branch 'jira-issues-sidebar-assignees' into 'master'

Jira Issues: Add assignees to sidebar

See merge request gitlab-org/gitlab!54045
parents 5857f7b7 03ad81e0
<script>
import { GlAlert, GlSprintf, GlLink } from '@gitlab/ui';
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 IssuableShow from '~/issuable_show/components/issuable_show_root.vue';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
export default {
......@@ -14,7 +13,7 @@ export default {
GlSprintf,
GlLink,
IssuableShow,
Sidebar,
JiraIssueSidebar,
},
inject: {
issuesShowPath: {
......@@ -81,7 +80,7 @@ export default {
<template #status-badge>{{ statusBadgeText }}</template>
<template #right-sidebar-items="{ sidebarExpanded }">
<sidebar :sidebar-expanded="sidebarExpanded" :selected-labels="issue.labels" />
<jira-issue-sidebar :sidebar-expanded="sidebarExpanded" :issue="issue" />
</template>
</issuable-show>
</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>
import LabelsSelect from '~/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue';
import Assignee from './assignee.vue';
export default {
name: 'JiraIssuesSidebar',
components: {
Assignee,
LabelsSelect,
},
props: {
......@@ -10,19 +13,30 @@ export default {
type: Boolean,
required: true,
},
selectedLabels: {
type: Array,
issue: {
type: Object,
required: true,
},
},
computed: {
assignee() {
// Jira issues have at most 1 assignee
return (this.issue?.assignees || [])[0];
},
},
};
</script>
<template>
<labels-select
:selected-labels="selectedLabels"
variant="sidebar"
class="block labels js-labels-block"
>{{ __('None') }}</labels-select
>
<div>
<assignee class="block" :assignee="assignee" />
<labels-select
:selected-labels="issue.labels"
variant="sidebar"
class="block labels js-labels-block"
>
{{ __('None') }}
</labels-select>
</div>
</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 Sidebar from 'ee/integrations/jira/issues_show/components/sidebar.vue';
import Assignee from 'ee/integrations/jira/issues_show/components/sidebar/assignee.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 { mockJiraIssue as mockJiraIssueData } from '../../mock_data';
import { mockJiraIssue } from '../mock_data';
const mockJiraIssue = convertObjectPropsToCamelCase(mockJiraIssueData, { deep: true });
describe('Sidebar', () => {
describe('JiraIssuesSidebar', () => {
let wrapper;
const defaultProps = {
sidebarExpanded: false,
selectedLabels: mockJiraIssue.labels,
issue: mockJiraIssue,
};
const createComponent = () => {
......@@ -27,12 +29,20 @@ describe('Sidebar', () => {
});
const findLabelsSelect = () => wrapper.findComponent(LabelsSelect);
const findAssignee = () => wrapper.findComponent(Assignee);
it('renders LabelsSelect', async () => {
it('renders Labels block', async () => {
createComponent();
await wrapper.vm.$nextTick();
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 = {
'<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"',
author: {
id: 2,
username: 'justin_ho',
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',
},
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: [
{
title: 'In Progress',
......
......@@ -16849,6 +16849,9 @@ msgstr ""
msgid "Jira service not configured."
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."
msgstr ""
......@@ -20121,6 +20124,9 @@ msgstr ""
msgid "No application_settings found"
msgstr ""
msgid "No assignee"
msgstr ""
msgid "No authentication methods configured."
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