Skip to content
Snippets Groups Projects
Commit 20b7a701 authored by Eric Eastwood's avatar Eric Eastwood
Browse files
parent d8898ea5
No related branches found
No related tags found
2 merge requests!1817WIP: Use store state alongside UI state in `data`,!1739WIP: Related issues FE
Showing
with 550 additions and 35 deletions
<script>
export default {
name: 'AddIssuableForm',
props: {
addButtonLabel: {
type: String,
required: true,
},
},
methods: {
},
};
</script>
<template>
<div>
<div class="add-issuable-form-input-wrapper form-control">
<ul class="add-issuable-form-input-token-list">
<li>some token</li>
</ul>
<input
type="text"
class="add-issuable-form-input"
value="asdf"
placeholder="Search issues..."
@input="onInput" />
</div>
<div class="clearfix prepend-top-10">
<button class="btn btn-new pull-left">
{{ addButtonLabel }}
</button>
<button class="btn btn-default pull-right">
Cancel
</button>
</div>
</div>
</template>
<script>
export default {
name: 'IssueToken',
props: {
reference: {
type: String,
required: true,
},
title: {
type: String,
required: true,
},
path: {
type: String,
required: true,
},
state: {
type: String,
required: false,
default: null,
},
},
computed: {
stateIconClass() {
let iconClass = null;
if (this.state === 'opened') {
iconClass = 'issue-token-state-icon-open fa fa-circle-o';
} else if (this.state === 'closed') {
iconClass = 'issue-token-state-icon-closed fa fa-minus';
}
return iconClass;
},
accessibleLabel() {
return `${this.state} ${this.reference} ${this.title}`;
},
removeButtonLabel() {
return `Remove related issue ${this.reference}`;
},
},
methods: {
},
};
</script>
<template>
<div class="issue-token">
<a
class="issue-token-reference"
:href="path">
<i
v-if="stateIconClass"
:class="stateIconClass"
:aria-label="accessibleLabel" />
{{ reference }}
</a>
<div class="issue-token-title">
<a
class="issue-token-title-link"
:href="path"
aria-hidden="true"
tabindex="-1">
{{ title }}
</a>
<button
class="issue-token-remove-button has-tooltip"
:title="removeButtonLabel">
<i class="fa fa-times" aria-hidden="true" />
</button>
</div>
</div>
</template>
<script>
import eventHub from '../event_hub';
import IssueToken from './issue_token.vue';
import AddIssuableForm from './add_issuable_form.vue';
export default {
name: 'RelatedIssuesBlock',
props: {
relatedIssues: {
type: Array,
required: false,
default: [],
},
fetchError: {
type: Error,
required: false,
default: null,
},
isAddRelatedIssuesFormVisible: {
type: Boolean,
required: false,
default: false,
},
},
components: {
addIssuableForm: AddIssuableForm,
issueToken: IssueToken,
},
computed: {
relatedIssueCount() {
return this.relatedIssues.length;
},
canAddRelatedIssues() {
// TODO:
return true;
},
},
methods: {
showAddRelatedIssuesForm() {
eventHub.$emit('showAddRelatedIssuesForm');
},
},
};
</script>
<template>
<div
class="panel-slim panel-default">
<div class="panel-heading">
<h3 class="panel-title">
Related issues
<div class="issue-count-holder">
<span class="issue-count-holder-count has-btn">
{{ relatedIssueCount }}
</span>
<button
v-if="canAddRelatedIssues"
class="issue-count-holder-add-button btn btn-small btn-default has-tooltip"
type="button"
aria-label="Add an issue"
title="Add an issue"
data-placement="top"
@click="showAddRelatedIssuesForm">
<i class="fa fa-plus" aria-hidden="true" />
</button>
</div>
</h3>
</div>
<div
v-if="isAddRelatedIssuesFormVisible"
class="related-issues-add-related-issues-form panel-body">
<addIssuableForm
add-button-label="Add related issues" />
</div>
<div class="panel-body">
<template v-if="fetchError">
<i class="fa fa-exclamation-circle" aria-hidden="true" />
An error occurred while fetching the related issues
</template>
<ul
v-else-if="relatedIssues"
class="related-issues-token-body">
<li
v-for="issue in relatedIssues"
class="related-issues-token-list-item">
<issueToken
:reference="issue.reference"
:title="issue.title"
:path="issue.path"
:state="issue.state" />
</li>
</ul>
<template v-else>
<i class="fa fa-spinner fa-spin" aria-hidden="true" />
Fetching related issues
</template>
</div>
</div>
</div>
</template>
import Vue from 'vue';
export default new Vue();
import Vue from 'vue';
import eventHub from './event_hub';
import RelatedIssuesBlock from './components/related_issues_block.vue';
import RelatedIssuesStore from './stores/related_issues_store';
import RelatedIssuesService from './services/related_issues_service';
class RelatedIssuesRoot {
constructor(wrapperElement) {
this.wrapperElement = wrapperElement;
const endpoint = this.wrapperElement.dataset.endpoint;
this.store = new RelatedIssuesStore({
});
this.service = new RelatedIssuesService(endpoint);
}
init() {
this.bindEvents();
this.fetchRelatedIssues();
this.render();
window.todoTestingUpdateRelatedIssues = () => {
this.store.setRelatedIssues([{
title: 'This is the title of my issue',
state: 'opened',
reference: '#1222',
path: '/gitlab-org/gitlab-ce/issues/0-fake-issue-id',
}, {
title: 'Another title to my other issue',
state: 'closed',
reference: '#43',
path: '/gitlab-org/gitlab-ce/issues/0-fake-issue-id',
}, {
title: 'One last title to my other issue',
state: 'closed',
reference: '#432',
path: '/gitlab-org/gitlab-ce/issues/0-fake-issue-id',
}, {
title: 'An issue from another project',
state: 'closed',
reference: 'design/#1222',
path: '/gitlab-org/gitlab-ce/issues/0-fake-issue-id',
}]);
};
}
bindEvents() {
this.onShowAddRelatedIssuesFormWrapper = this.onShowAddRelatedIssuesForm.bind(this);
eventHub.$on('showAddRelatedIssuesForm', this.onShowAddRelatedIssuesFormWrapper);
}
unbindEvents() {
eventHub.$off('showAddRelatedIssuesForm', this.onShowAddRelatedIssuesFormWrapper);
}
render() {
this.vm = new Vue({
el: this.wrapperElement,
data: this.store.state,
components: {
relatedIssuesBlock: RelatedIssuesBlock,
},
/* * /
template: `
<relatedIssuesBlock
:related-issues="relatedIssues"
:fetch-error="fetchError"
:is-add-related-issues-form-visible="isAddRelatedIssuesFormVisible" />
`,
/* */
render: createElement => createElement('related-issues-block', {
props: {
relatedIssues: this.store.state.relatedIssues,
fetchError: this.store.state.fetchError,
isAddRelatedIssuesFormVisible: this.store.state.isAddRelatedIssuesFormVisible,
},
}),
});
}
onShowAddRelatedIssuesForm() {
this.store.setIsAddRelatedIssuesFormVisible(true);
}
fetchRelatedIssues() {
this.service.fetchRelatedIssues()
.then((issues) => {
this.store.setRelatedIssues(issues);
})
.catch((err) => {
this.store.setFetchError(err);
});
}
destroy() {
this.unbindEvents();
if (this.vm) {
this.vm.$destroy();
}
}
}
export default RelatedIssuesRoot;
import Vue from 'vue';
import vueResource from 'vue-resource';
Vue.use(vueResource);
class RelatedIssuesService {
constructor(endpoint) {
this.relatedIssuesResource = Vue.resource(`${endpoint}`);
}
fetchRelatedIssues() {
return this.relatedIssuesResource.get()
.then((res) => {
const issues = res.data;
if (!issues) {
throw new Error('Response didn\'t include `service_desk_address`');
}
return issues;
});
}
}
export default RelatedIssuesService;
class RelatedIssuesStore {
constructor(initialState = {}) {
this.state = Object.assign({
relatedIssues: [],
fetchError: null,
isAddRelatedIssuesFormVisible: false,
}, initialState);
}
setRelatedIssues(value) {
this.state.relatedIssues = value;
}
setFetchError(value) {
this.state.fetchError = value;
}
setIsAddRelatedIssuesFormVisible(value) {
this.state.isAddRelatedIssuesFormVisible = value;
}
}
export default RelatedIssuesStore;
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, one-var, no-underscore-dangle, one-var-declaration-per-line, object-shorthand, no-unused-vars, no-new, comma-dangle, consistent-return, quotes, dot-notation, quote-props, prefer-arrow-callback, max-len */
/* global Flash */
 
import RelatedIssuesRoot from './issuable/related_issues/related_issues_root';
require('./flash');
require('~/lib/utils/text_utility');
require('vendor/jquery.waitforimages');
Loading
Loading
@@ -23,6 +25,7 @@ class Issue {
 
Issue.$btnNewBranch = $('#new-branch');
 
Issue.initRelatedIssues();
Issue.initMergeRequests();
Issue.initRelatedBranches();
Issue.initCanCreateBranch();
Loading
Loading
@@ -86,6 +89,12 @@ class Issue {
}
}
 
static initRelatedIssues() {
console.log('initRelatedIssues');
new RelatedIssuesRoot(document.querySelector('.js-related-issues-root'))
.init();
}
static initMergeRequests() {
var $container;
$container = $('#merge-requests');
Loading
Loading
Loading
Loading
@@ -317,30 +317,6 @@
margin: 5px;
}
 
.board-issue-count-holder {
margin-top: -3px;
.btn {
line-height: 12px;
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
}
.board-issue-count {
padding-right: 10px;
padding-left: 10px;
line-height: 21px;
border-radius: $border-radius-base;
border: 1px solid $border-color;
&.has-btn {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
border-width: 1px 0 1px 1px;
}
}
.boards-title-holder {
padding: 25px 13px $gl-padding;
 
Loading
Loading
Loading
Loading
@@ -171,3 +171,155 @@ ul.related-merge-requests > li {
.recaptcha {
margin-bottom: 30px;
}
.issue-count-holder {
display: inline-flex;
align-items: stretch;
height: 24px;
}
.issue-count-holder-count {
display: flex;
align-items: center;
padding-right: 10px;
padding-left: 10px;
border: 1px solid $border-color;
border-radius: $border-radius-base;
line-height: 1;
&.has-btn {
border-right: 0;
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
}
.issue-count-holder-add-button {
display: flex;
align-items: center;
border-top: 1px solid $border-color;
border-left: 1px solid $border-color;
border-bottom: 1px solid $border-color;
border-right: 1px solid $border-color;
border-top-left-radius: 0;
border-bottom-left-radius: 0;
border-top-right-radius: $border-radius-base;
border-bottom-right-radius: $border-radius-base;
line-height: 1;
}
.issue-token {
display: inline-flex;
line-height: 1.5;
}
.issue-token-reference {
display: flex;
align-items: baseline;
margin-right: 1px;
padding: 0 .5em;
background-color: $gray-lighter;
transition: background 0.2s ease, color 0.2s ease;
&:hover,
&:focus,
.issue-token:hover > &,
.issue-token:focus > & {
background-color: $gray-normal;
color: $gl-link-hover-color;
text-decoration: none;
}
}
@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: 0 .5em;
background-color: $gray-normal;
color: $gl-text-color-secondary;
transition: background 0.2s ease, color 0.2s ease;
&:hover,
&:focus,
.issue-token:hover > &,
.issue-token:focus > & {
background-color: darken($gray-normal, 2%);
color: $gl-text-color-secondary;
}
}
.issue-token-title-link {
color: inherit;
&:hover,
&:focus {
text-decoration: none;
color: inherit;
}
}
.issue-token-remove-button {
background-color: transparent;
border: 0;
color: inherit;
font-size: 0.9em;
}
.add-issuable-form-input-wrapper {
display: flex;
align-items: stretch;
}
.add-issuable-form-input-token-list {
display: flex;
list-style: none;
margin-bottom: 0;
padding-left: 0;
}
.add-issuable-form-input {
flex: 1;
border: 0;
&:focus {
outline: none;
}
}
.related-issues-add-related-issues-form {
border-bottom: 1px solid $border-color;
}
.related-issues-token-body {
display: flex;
flex-wrap: wrap;
margin-bottom: 0;
padding-left: 0;
list-style: none;
}
.related-issues-token-list-item {
margin-bottom: 0.5em;
margin-right: 1em;
}
Loading
Loading
@@ -6,11 +6,11 @@
%span.has-tooltip{ ":title" => '(list.label ? list.label.description : "")',
data: { container: "body", placement: "bottom" } }
{{ list.title }}
.board-issue-count-holder.pull-right.clearfix{ "v-if" => 'list.type !== "blank"' }
%span.board-issue-count.pull-left{ ":class" => '{ "has-btn": list.type !== "closed" && !disabled }' }
.issue-count-holder.pull-right.clearfix{ "v-if" => 'list.type !== "blank"' }
%span.issue-count-holder-count{ ":class" => '{ "has-btn": list.type !== "closed" && !disabled }' }
{{ list.issuesSize }}
- if can?(current_user, :admin_issue, @project)
%button.btn.btn-small.btn-default.pull-right.has-tooltip{ type: "button",
%button.issue-count-holder-add-button.btn.btn-small.btn-default.has-tooltip{ type: "button",
"@click" => "showNewIssueForm",
"v-if" => 'list.type !== "closed"',
"aria-label" => "Add an issue",
Loading
Loading
Loading
Loading
@@ -64,6 +64,8 @@
= @issue.description
= edited_time_ago_with_tooltip(@issue, placement: 'bottom', html_class: 'issue_edited_ago')
 
.js-related-issues-root{ data: { endpoint: namespace_project_issue_related_issues_path(@project.namespace, @project, @issue) } }
#merge-requests{ data: { url: referenced_merge_requests_namespace_project_issue_url(@project.namespace, @project, @issue) } }
// This element is filled in using JavaScript.
 
Loading
Loading
Loading
Loading
@@ -22,18 +22,18 @@
end
 
it 'displays new issue button' do
expect(page).to have_selector('.board-issue-count-holder .btn', count: 1)
expect(page).to have_selector('.issue-count-holder-add-button', count: 1)
end
 
it 'does not display new issue button in closed list' do
page.within('.board:nth-child(2)') do
expect(page).not_to have_selector('.board-issue-count-holder .btn')
expect(page).not_to have_selector('.issue-count-holder-add-button')
end
end
 
it 'shows form when clicking button' do
page.within(first('.board')) do
find('.board-issue-count-holder .btn').click
find('.issue-count-holder-add-button').click
 
expect(page).to have_selector('.board-new-issue-form')
end
Loading
Loading
@@ -41,7 +41,7 @@
 
it 'hides form when clicking cancel' do
page.within(first('.board')) do
find('.board-issue-count-holder .btn').click
find('.issue-count-holder-add-button').click
 
expect(page).to have_selector('.board-new-issue-form')
 
Loading
Loading
@@ -53,7 +53,7 @@
 
it 'creates new issue' do
page.within(first('.board')) do
find('.board-issue-count-holder .btn').click
find('.issue-count-holder-add-button').click
end
 
page.within(first('.board-new-issue-form')) do
Loading
Loading
@@ -63,14 +63,14 @@
 
wait_for_vue_resource
 
page.within(first('.board .board-issue-count')) do
page.within(first('.board .issue-count-holder-count')) do
expect(page).to have_content('1')
end
end
 
it 'shows sidebar when creating new issue' do
page.within(first('.board')) do
find('.board-issue-count-holder .btn').click
find('.issue-count-holder-add-button').click
end
 
page.within(first('.board-new-issue-form')) do
Loading
Loading
@@ -91,7 +91,7 @@
end
 
it 'does not display new issue button' do
expect(page).to have_selector('.board-issue-count-holder .btn', count: 0)
expect(page).to have_selector('.issue-count-holder-add-button', count: 0)
end
end
end
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment