Commit 529c570c authored by Fatih Acet's avatar Fatih Acet

Move related issues shared components from EE

We will rewrite Related MRs widget in CE with Vue.
It’s pretty much the same with Related Issues in EE.
I made EE only components reusable and this is the CE
backward compatability commit.

Links:

Issue: https://gitlab.com/gitlab-org/gitlab-ce/issues/57662
MR: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/9730
parent 728e8079
<script>
import { GlTooltipDirective } from '@gitlab/ui';
import { __, sprintf } from '~/locale';
import IssueMilestone from '~/vue_shared/components/issue/issue_milestone.vue';
import IssueAssignees from '~/vue_shared/components/issue/issue_assignees.vue';
import relatedIssuableMixin from '~/vue_shared/mixins/related_issuable_mixin';
export default {
name: 'IssueItem',
components: {
IssueMilestone,
IssueAssignees,
},
directives: {
GlTooltip: GlTooltipDirective,
},
mixins: [relatedIssuableMixin],
props: {
canReorder: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
stateTitle() {
return sprintf(
'<span class="bold">%{state}</span> %{timeInWords}<br/><span class="text-tertiary">%{timestamp}</span>',
{
state: this.isOpen ? __('Opened') : __('Closed'),
timeInWords: this.isOpen ? this.createdAtInWords : this.closedAtInWords,
timestamp: this.isOpen ? this.createdAtTimestamp : this.closedAtTimestamp,
},
);
},
},
};
</script>
<template>
<div
:class="{
'issuable-info-container': !canReorder,
'card-body': canReorder,
}"
class="item-body"
>
<div class="item-contents">
<div class="item-title d-flex align-items-center">
<icon
v-if="hasState"
v-tooltip
:css-classes="iconClass"
:name="iconName"
:size="16"
:title="stateTitle"
:aria-label="state"
data-html="true"
/>
<icon
v-if="confidential"
v-gl-tooltip
name="eye-slash"
:size="16"
:title="__('Confidential')"
class="confidential-icon append-right-4"
:aria-label="__('Confidential')"
/>
<a :href="computedPath" class="sortable-link">{{ title }}</a>
</div>
<div class="item-meta">
<div class="d-flex align-items-center item-path-id">
<icon
v-if="hasState"
v-tooltip
:css-classes="iconClass"
:name="iconName"
:size="16"
:title="stateTitle"
:aria-label="state"
data-html="true"
/>
<span v-tooltip :title="itemPath" class="path-id-text">{{ itemPath }}</span>
{{ pathIdSeparator }}{{ itemId }}
</div>
<div class="item-meta-child d-flex align-items-center">
<issue-milestone
v-if="hasMilestone"
:milestone="milestone"
class="d-flex align-items-center item-milestone"
/>
<slot name="dueDate"></slot>
<slot name="weight"></slot>
</div>
<issue-assignees
v-if="assignees.length"
:assignees="assignees"
class="item-assignees d-inline-flex"
/>
</div>
</div>
<button
v-if="canRemove"
ref="removeButton"
v-tooltip
:disabled="removeDisabled"
type="button"
class="btn btn-default btn-svg btn-item-remove js-issue-item-remove-button qa-remove-issue-button"
title="Remove"
aria-label="Remove"
@click="onRemoveRequest"
>
<icon :size="16" class="btn-item-remove-icon" name="close" />
</button>
</div>
</template>
import _ from 'underscore';
import { formatDate } from '~/lib/utils/datetime_utility';
import tooltip from '~/vue_shared/directives/tooltip';
import icon from '~/vue_shared/components/icon.vue';
import timeagoMixin from '~/vue_shared/mixins/timeago';
const mixins = {
data() {
return {
removeDisabled: false,
};
},
props: {
idKey: {
type: Number,
required: true,
},
displayReference: {
type: String,
required: true,
},
pathIdSeparator: {
type: String,
required: true,
},
eventNamespace: {
type: String,
required: false,
default: '',
},
confidential: {
type: Boolean,
required: false,
default: false,
},
title: {
type: String,
required: false,
default: '',
},
path: {
type: String,
required: false,
default: '',
},
state: {
type: String,
required: false,
default: '',
},
createdAt: {
type: String,
required: false,
default: '',
},
closedAt: {
type: String,
required: false,
default: '',
},
milestone: {
type: Object,
required: false,
default: () => ({}),
},
dueDate: {
type: String,
required: false,
default: '',
},
assignees: {
type: Array,
required: false,
default: () => [],
},
weight: {
type: Number,
required: false,
default: 0,
},
canRemove: {
type: Boolean,
required: false,
default: false,
},
},
components: {
icon,
},
directives: {
tooltip,
},
mixins: [timeagoMixin],
computed: {
hasState() {
return this.state && this.state.length > 0;
},
isOpen() {
return this.state === 'opened';
},
isClosed() {
return this.state === 'closed';
},
hasTitle() {
return this.title.length > 0;
},
hasMilestone() {
return !_.isEmpty(this.milestone);
},
iconName() {
return this.isOpen ? 'issue-open-m' : 'issue-close';
},
iconClass() {
return this.isOpen ? 'issue-token-state-icon-open' : 'issue-token-state-icon-closed';
},
computedLinkElementType() {
return this.path.length > 0 ? 'a' : 'span';
},
computedPath() {
return this.path.length ? this.path : null;
},
itemPath() {
return this.displayReference.split(this.pathIdSeparator)[0];
},
itemId() {
return this.displayReference.split(this.pathIdSeparator).pop();
},
createdAtInWords() {
return this.createdAt ? this.timeFormated(this.createdAt) : '';
},
createdAtTimestamp() {
return this.createdAt ? formatDate(new Date(this.createdAt)) : '';
},
closedAtInWords() {
return this.closedAt ? this.timeFormated(this.closedAt) : '';
},
closedAtTimestamp() {
return this.closedAt ? formatDate(new Date(this.closedAt)) : '';
},
},
methods: {
onRemoveRequest() {
let namespacePrefix = '';
if (this.eventNamespace && this.eventNamespace.length > 0) {
namespacePrefix = `${this.eventNamespace}`;
}
this.$emit(`${namespacePrefix}RemoveRequest`, this.idKey);
this.removeDisabled = true;
},
},
};
export default mixins;
import Vue from 'vue';
import { mount, createLocalVue } from '@vue/test-utils';
import RelatedIssuableItem from '~/vue_shared/components/issue/related_issuable_item.vue';
import { defaultMilestone, defaultAssignees } from './related_issuable_mock_data';
describe('RelatedIssuableItem', () => {
let wrapper;
const props = {
idKey: 1,
displayReference: 'gitlab-org/gitlab-test#1',
pathIdSeparator: '#',
path: `${gl.TEST_HOST}/path`,
title: 'title',
confidential: true,
dueDate: '1990-12-31',
weight: 10,
createdAt: '2018-12-01T00:00:00.00Z',
milestone: defaultMilestone,
assignees: defaultAssignees,
eventNamespace: 'relatedIssue',
};
const slots = {
dueDate: '<div class="js-due-date-slot"></div>',
weight: '<div class="js-weight-slot"></div>',
};
beforeEach(() => {
const localVue = createLocalVue();
wrapper = mount(localVue.extend(RelatedIssuableItem), {
localVue,
slots,
sync: false,
propsData: props,
});
});
afterEach(() => {
wrapper.destroy();
});
it('contains issuable-info-container class when canReorder is false', () => {
expect(wrapper.props('canReorder')).toBe(false);
expect(wrapper.find('.issuable-info-container').exists()).toBe(true);
});
it('does not render token state', () => {
expect(wrapper.find('.text-secondary svg').exists()).toBe(false);
});
it('does not render remove button', () => {
expect(wrapper.find({ ref: 'removeButton' }).exists()).toBe(false);
});
describe('token title', () => {
it('links to computedPath', () => {
expect(wrapper.find('.item-title a').attributes('href')).toEqual(wrapper.props('path'));
});
it('renders confidential icon', () => {
expect(wrapper.find('.confidential-icon').exists()).toBe(true);
});
it('renders title', () => {
expect(wrapper.find('.item-title a').text()).toEqual(props.title);
});
});
describe('token state', () => {
let tokenState;
beforeEach(done => {
wrapper.setProps({ state: 'opened' });
Vue.nextTick(() => {
tokenState = wrapper.find('.issue-token-state-icon-open');
done();
});
});
it('renders if hasState', () => {
expect(tokenState.exists()).toBe(true);
});
it('renders state title', () => {
const stateTitle = tokenState.attributes('data-original-title');
expect(stateTitle).toContain('<span class="bold">Opened</span>');
expect(stateTitle).toContain(
'<span class="text-tertiary">Dec 1, 2018 12:00am GMT+0000</span>',
);
});
it('renders aria label', () => {
expect(tokenState.attributes('aria-label')).toEqual('opened');
});
it('renders open icon when open state', () => {
expect(tokenState.classes('issue-token-state-icon-open')).toBe(true);
});
it('renders close icon when close state', done => {
wrapper.setProps({
state: 'closed',
closedAt: '2018-12-01T00:00:00.00Z',
});
Vue.nextTick(() => {
expect(tokenState.classes('issue-token-state-icon-closed')).toBe(true);
done();
});
});
});
describe('token metadata', () => {
let tokenMetadata;
beforeEach(done => {
Vue.nextTick(() => {
tokenMetadata = wrapper.find('.item-meta');
done();
});
});
it('renders item path and ID', () => {
const pathAndID = tokenMetadata.find('.item-path-id').text();
expect(pathAndID).toContain('gitlab-org/gitlab-test');
expect(pathAndID).toContain('#1');
});
it('renders milestone icon and name', () => {
const milestoneIcon = tokenMetadata.find('.item-milestone svg use');
const milestoneTitle = tokenMetadata.find('.item-milestone .milestone-title');
expect(milestoneIcon.attributes('href')).toContain('clock');
expect(milestoneTitle.text()).toContain('Milestone title');
});
it('renders due date component', () => {
expect(tokenMetadata.find('.js-due-date-slot').exists()).toBe(true);
});
it('renders weight component', () => {
expect(tokenMetadata.find('.js-weight-slot').exists()).toBe(true);
});
});
describe('token assignees', () => {
it('renders assignees avatars', () => {
expect(wrapper.findAll('.item-assignees .user-avatar-link').length).toBe(2);
expect(wrapper.find('.item-assignees .avatar-counter').text()).toContain('+2');
});
});
describe('remove button', () => {
let removeBtn;
beforeEach(done => {
wrapper.setProps({ canRemove: true });
Vue.nextTick(() => {
removeBtn = wrapper.find({ ref: 'removeButton' });
done();
});
});
it('renders if canRemove', () => {
expect(removeBtn.exists()).toBe(true);
});
it('renders disabled button when removeDisabled', done => {
wrapper.vm.removeDisabled = true;
Vue.nextTick(() => {
expect(removeBtn.attributes('disabled')).toEqual('disabled');
done();
});
});
it('triggers onRemoveRequest when clicked', () => {
removeBtn.trigger('click');
const { relatedIssueRemoveRequest } = wrapper.emitted();
expect(relatedIssueRemoveRequest.length).toBe(1);
expect(relatedIssueRemoveRequest[0]).toEqual([props.idKey]);
});
});
});
export const defaultProps = {
endpoint: '/foo/bar/issues/1/related_issues',
currentNamespacePath: 'foo',
currentProjectPath: 'bar',
};
export const issuable1 = {
id: 200,
epic_issue_id: 1,
confidential: false,
reference: 'foo/bar#123',
displayReference: '#123',
title: 'some title',
path: '/foo/bar/issues/123',
state: 'opened',
};
export const issuable2 = {
id: 201,
epic_issue_id: 2,
confidential: false,
reference: 'foo/bar#124',
displayReference: '#124',
title: 'some other thing',
path: '/foo/bar/issues/124',
state: 'opened',
};
export const issuable3 = {
id: 202,
epic_issue_id: 3,
confidential: false,
reference: 'foo/bar#125',
displayReference: '#125',
title: 'some other other thing',
path: '/foo/bar/issues/125',
state: 'opened',
};
export const issuable4 = {
id: 203,
epic_issue_id: 4,
confidential: false,
reference: 'foo/bar#126',
displayReference: '#126',
title: 'some other other other thing',
path: '/foo/bar/issues/126',
state: 'opened',
};
export const issuable5 = {
id: 204,
epic_issue_id: 5,
confidential: false,
reference: 'foo/bar#127',
displayReference: '#127',
title: 'some other other other thing',
path: '/foo/bar/issues/127',
state: 'opened',
};
export const defaultMilestone = {
id: 1,
state: 'active',
title: 'Milestone title',
start_date: '2018-01-01',
due_date: '2019-12-31',
};
export const defaultAssignees = [
{
id: 1,
name: 'Administrator',
username: 'root',
state: 'active',
avatar_url: `${gl.TEST_HOST}`,
web_url: `${gl.TEST_HOST}/root`,
status_tooltip_html: null,
path: '/root',
},
{
id: 13,
name: 'Brooks Beatty',
username: 'brynn_champlin',
state: 'active',
avatar_url: `${gl.TEST_HOST}`,
web_url: `${gl.TEST_HOST}/brynn_champlin`,
status_tooltip_html: null,
path: '/brynn_champlin',
},
{
id: 6,
name: 'Bryce Turcotte',
username: 'melynda',
state: 'active',
avatar_url: `${gl.TEST_HOST}`,
web_url: `${gl.TEST_HOST}/melynda`,
status_tooltip_html: null,
path: '/melynda',
},
{
id: 20,
name: 'Conchita Eichmann',
username: 'juliana_gulgowski',
state: 'active',
avatar_url: `${gl.TEST_HOST}`,
web_url: `${gl.TEST_HOST}/juliana_gulgowski`,
status_tooltip_html: null,
path: '/juliana_gulgowski',
},
];
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