Commit b3c5b30c authored by Eric Eastwood's avatar Eric Eastwood

Add IssueToken

See https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/1797

Conflicts:
	app/assets/stylesheets/pages/issues.scss
parent 2e760d20
<script>
import eventHub from '../event_hub';
import {
FETCHING_STATUS,
FETCH_SUCCESS_STATUS,
FETCH_ERROR_STATUS,
} from '../constants';
export default {
name: 'IssueToken',
props: {
reference: {
type: String,
required: true,
},
displayReference: {
type: String,
required: true,
},
eventNamespace: {
type: String,
required: false,
default: '',
},
title: {
type: String,
required: false,
default: '',
},
path: {
type: String,
required: false,
default: '',
},
state: {
type: String,
required: false,
default: '',
},
fetchStatus: {
type: String,
required: false,
default: FETCH_SUCCESS_STATUS,
},
canRemove: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
isFetching() {
return this.fetchStatus === FETCHING_STATUS;
},
hasFetchingError() {
return this.fetchStatus === FETCH_ERROR_STATUS;
},
removeButtonLabel() {
return `Remove related issue ${this.reference}`;
},
hasState() {
return this.state && this.state.length > 0;
},
hasTitle() {
return this.title.length > 0 || this.isFetching;
}
},
methods: {
onRemoveRequest() {
let namespacePrefix = '';
if (this.eventNamespace && this.eventNamespace.length > 0) {
namespacePrefix = `${this.eventNamespace}-`;
}
eventHub.$emit(`${namespacePrefix}removeRequest`, this.reference);
},
},
updated() {
const removeButton = this.$refs.removeButton;
if (removeButton) {
$(this.$refs.removeButton).tooltip('fixTitle');
}
},
};
</script>
<template>
<div
class="issue-token"
:class="{ 'issue-token-error': hasFetchingError }">
<a
ref="link"
class="issue-token-link"
:href="path">
<span
ref="reference"
class="issue-token-reference">
<i
ref="stateIcon"
v-if="hasState"
class="fa"
:class="{
'issue-token-state-icon-open fa-circle-o': state === 'opened',
'issue-token-state-icon-closed fa-minus': state === 'closed',
}"
:aria-label="state">
</i>
{{ displayReference }}
</span>
<span
v-if="hasTitle"
ref="title"
class="issue-token-title">
<i
ref="fetchStatusIcon"
v-if="isFetching"
class="fa fa-spinner fa-spin"
aria-label="Fetching info">
</i>
{{ title }}
</span>
</a>
<button
ref="removeButton"
v-if="canRemove"
type="button"
class="issue-token-remove-button"
:class="{ 'issue-token-remove-button-standalone': !hasTitle }"
:title="removeButtonLabel"
data-toggle="tooltip"
@click="onRemoveRequest">
<i
class="fa fa-times"
aria-hidden="true">
</i>
</button>
</div>
</template>
export const FETCHING_STATUS = 'FETCHING';
export const FETCH_SUCCESS_STATUS = 'FETCH_SUCCESS';
export const FETCH_ERROR_STATUS = 'FETCH_ERROR';
......@@ -64,6 +64,7 @@ $orange-900: #853b00;
$red-25: #fef7f6;
$red-50: #fbe7e4;
$red-75: #f8d5d0;
$red-100: #f4c4bc;
$red-200: #ed9d90;
$red-300: #e67664;
......
......@@ -275,3 +275,121 @@ ul.related-merge-requests > li {
}
}
}
.issue-token {
display: inline-flex;
align-items: stretch;
line-height: 1.75;
white-space: nowrap;
}
.issue-token-link {
display: inline-flex;
&:hover,
&:focus {
outline: none;
text-decoration: none;
}
}
.issue-token-reference {
display: flex;
align-items: baseline;
margin-right: 1px;
padding-left: 0.5em;
padding-right: 0.5em;
background-color: $gray-lighter;
transition: background $general-hover-transition-duration $general-hover-transition-curve, color $general-hover-transition-duration $general-hover-transition-curve;
.issue-token-error & {
background-color: $red-50;
}
.issue-token:hover &,
.issue-token-link:focus > & {
background-color: issue-token-reference-hover-background-color;
color: $gl-link-hover-color;
text-decoration: none;
}
.issue-token-error:hover &,
.issue-token-link:focus > & {
background-color: $red-75;
}
}
@mixin issue-token-state-icon {
margin-right: 0.35em;
font-size: 0.9em;
}
.issue-token-state-icon-open {
@include issue-token-state-icon;
color: $green-600;
}
.issue-token-state-icon-closed {
@include issue-token-state-icon;
color: $red-600;
}
.issue-token-title {
display: flex;
align-items: baseline;
padding-left: 0.5em;
padding-right: 0.5em;
background-color: $gray-normal;
color: $gl-text-color-secondary;
transition: background $general-hover-transition-duration $general-hover-transition-curve;
.issue-token-error & {
background-color: $red-75;
}
.issue-token:hover &,
.issue-token-link:focus > & {
background-color: $border-gray-normal;
}
.issue-token-error:hover &,
.issue-token-link:focus > & {
background-color: $red-100;
}
& > .fa {
line-height: inherit;
}
}
.issue-token-remove-button {
padding: 0 0.5em 0 0;
background-color: $gray-normal;
border: 0;
color: $gl-text-color-secondary;
transition: background $general-hover-transition-duration $general-hover-transition-curve;
.issue-token-error & {
background-color: $red-75;
}
.issue-token:hover &,
.issue-token-link:focus + & {
background-color: $border-gray-normal;
}
.issue-token-error:hover &,
.issue-token-link:focus > & {
background-color: $red-100;
}
& > .fa {
font-size: 0.9em;
}
}
// When there isn't a title
.issue-token-remove-button-standalone {
padding-left: 0.5em;
}
import Vue from 'vue';
import eventHub from '~/issuable/related_issues/event_hub';
import RelatedIssuesService from '~/issuable/related_issues/services/related_issues_service';
import issueToken from '~/issuable/related_issues/components/issue_token.vue';
describe('IssueToken', () => {
const reference = 'foo/bar#123';
const title = 'some title';
let IssueToken;
let vm;
beforeEach(() => {
IssueToken = Vue.extend(issueToken);
});
afterEach(() => {
if (vm) {
vm.$destroy();
}
});
describe('with reference supplied', () => {
beforeEach(() => {
vm = new IssueToken({
propsData: {
reference,
},
}).$mount();
});
it('shows reference', () => {
expect(vm.$el.textContent.trim()).toEqual(reference);
});
});
describe('with reference and title supplied', () => {
beforeEach(() => {
vm = new IssueToken({
propsData: {
reference,
title,
},
}).$mount();
});
it('shows reference and title', () => {
expect(vm.$refs.reference.textContent.trim()).toEqual(reference);
expect(vm.$refs.title.textContent.trim()).toEqual(title);
});
});
describe('with path supplied', () => {
const path = '/foo/bar/issues/123';
beforeEach(() => {
vm = new IssueToken({
propsData: {
reference,
title,
path,
},
}).$mount();
});
it('links reference and title', () => {
expect(vm.$refs.link.getAttribute('href')).toEqual(path);
});
});
describe('with state supplied', () => {
describe('`state: \'opened\'`', () => {
beforeEach(() => {
vm = new IssueToken({
propsData: {
reference,
state: 'opened',
},
}).$mount();
});
it('shows green circle icon', () => {
expect(vm.$el.querySelector('.issue-token-state-icon-open.fa.fa-circle-o')).toBeDefined();
});
});
describe('`state: \'closed\'`', () => {
beforeEach(() => {
vm = new IssueToken({
propsData: {
reference,
state: 'closed',
},
}).$mount();
});
it('shows red minus icon', () => {
expect(vm.$el.querySelector('.issue-token-state-icon-closed.fa.fa-minus')).toBeDefined();
});
});
});
describe('with reference, title, state', () => {
const state = 'opened';
beforeEach(() => {
vm = new IssueToken({
propsData: {
reference,
title,
state,
},
}).$mount();
});
it('shows reference, title, and state', () => {
expect(vm.$refs.stateIcon.getAttribute('aria-label')).toEqual(state);
expect(vm.$refs.reference.textContent.trim()).toEqual(reference);
expect(vm.$refs.title.textContent.trim()).toEqual(title);
});
});
describe('with fetchStatus', () => {
describe('`canRemove: RelatedIssuesService.FETCHING_STATUS`', () => {
beforeEach(() => {
vm = new IssueToken({
propsData: {
reference,
fetchStatus: RelatedIssuesService.FETCHING_STATUS,
},
}).$mount();
});
it('shows loading indicator/spinner', () => {
expect(vm.$refs.fetchStatusIcon).toBeDefined();
});
});
describe('`canRemove: RelatedIssuesService.FETCH_ERROR_STATUS`', () => {
beforeEach(() => {
vm = new IssueToken({
propsData: {
reference,
fetchStatus: RelatedIssuesService.FETCH_ERROR_STATUS,
},
}).$mount();
});
it('tints the token red', () => {
expect(vm.$el.classList.contains('issue-token-error')).toEqual(true);
});
});
});
describe('with canRemove', () => {
describe('`canRemove: false` (default)', () => {
beforeEach(() => {
vm = new IssueToken({
propsData: {
reference,
},
}).$mount();
});
it('does not have remove button', () => {
expect(vm.$el.querySelector('.issue-token-remove-button')).toBeNull();
});
});
describe('`canRemove: true`', () => {
beforeEach(() => {
vm = new IssueToken({
propsData: {
reference,
canRemove: true,
},
}).$mount();
});
it('has remove button', () => {
expect(vm.$el.querySelector('.issue-token-remove-button')).toBeDefined();
});
});
});
describe('methods', () => {
let removeRequestSpy;
beforeEach(() => {
vm = new IssueToken({
propsData: {
reference,
},
}).$mount();
removeRequestSpy = jasmine.createSpy('spy');
eventHub.$on('removeRequest', removeRequestSpy);
});
afterEach(() => {
eventHub.$off('removeRequest', removeRequestSpy);
});
it('when getting checked', () => {
expect(removeRequestSpy).not.toHaveBeenCalled();
vm.onRemoveRequest();
expect(removeRequestSpy).toHaveBeenCalled();
});
});
});
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