Skip to content
Snippets Groups Projects
Commit 6656d92a authored by Jacob Schatz's avatar Jacob Schatz
Browse files

Merge branch '2001-related-issues-fe-base' into 'master'

Related issues FE Base

Closes #2001

See merge request !1797
parents 2a9a366c f504fc77
No related branches found
No related tags found
1 merge request!1797Related issues FE Base
Pipeline #
Showing
with 1067 additions and 77 deletions
import Vue from 'vue';
import RelatedIssuesRoot from './related_issues/components/related_issues_root.vue';
document.addEventListener('DOMContentLoaded', () => {
const relatedIssuesRootElement = document.querySelector('.js-related-issues-root');
if (relatedIssuesRootElement) {
// eslint-disable-next-line no-new
new Vue({
el: relatedIssuesRootElement,
components: {
relatedIssuesRoot: RelatedIssuesRoot,
},
render: createElement => createElement('related-issues-root', {
props: {
endpoint: relatedIssuesRootElement.dataset.endpoint,
canAddRelatedIssues: gl.utils.convertPermissionToBoolean(
relatedIssuesRootElement.dataset.canAddRelatedIssues,
),
helpPath: relatedIssuesRootElement.dataset.helpPath,
},
}),
});
}
});
<script>
import GfmAutoComplete from '~/gfm_auto_complete';
import eventHub from '../event_hub';
import IssueToken from './issue_token.vue';
export default {
name: 'AddIssuableForm',
props: {
inputValue: {
type: String,
required: true,
},
addButtonLabel: {
type: String,
required: true,
},
pendingReferences: {
type: Array,
required: false,
default: () => [],
},
autoCompleteSources: {
type: Object,
required: false,
default: () => ({}),
},
isSubmitting: {
type: Boolean,
required: false,
default: false,
},
},
data() {
return {
isInputFocused: false,
isAutoCompleteOpen: false,
};
},
components: {
issueToken: IssueToken,
},
computed: {
isSubmitButtonDisabled() {
return this.pendingReferences.length === 0 || this.isSubmitting;
},
},
methods: {
onInput() {
const value = this.$refs.input.value;
eventHub.$emit('addIssuableFormInput', value, $(this.$refs.input).caret('pos'));
},
onFocus() {
this.isInputFocused = true;
},
onBlur() {
this.isInputFocused = false;
// Avoid tokenizing partial input when clicking an autocomplete item
if (!this.isAutoCompleteOpen) {
const value = this.$refs.input.value;
eventHub.$emit('addIssuableFormBlur', value);
}
},
onAutoCompleteToggled(isOpen) {
this.isAutoCompleteOpen = isOpen;
},
onInputWrapperClick() {
this.$refs.input.focus();
},
onFormSubmit() {
eventHub.$emit('addIssuableFormSubmit');
},
onFormCancel() {
eventHub.$emit('addIssuableFormCancel');
},
},
mounted() {
const $input = $(this.$refs.input);
new GfmAutoComplete(this.autoCompleteSources).setup($input, {
issues: true,
});
$input.on('shown-issues.atwho', this.onAutoCompleteToggled.bind(this, true));
$input.on('hidden-issues.atwho', this.onAutoCompleteToggled.bind(this, false));
$input.on('inserted-issues.atwho', this.onInput);
},
beforeDestroy() {
const $input = $(this.$refs.input);
$input.off('shown-issues.atwho');
$input.off('hidden-issues.atwho');
$input.off('inserted-issues.atwho', this.onInput);
},
};
</script>
<template>
<div>
<div
ref="issuableFormWrapper"
class="add-issuable-form-input-wrapper form-control"
:class="{ focus: isInputFocused }"
role="button"
@click="onInputWrapperClick">
<ul class="add-issuable-form-input-token-list">
<li
:key="reference"
v-for="(reference, index) in pendingReferences"
class="js-add-issuable-form-token-list-item add-issuable-form-token-list-item">
<issue-token
event-namespace="pendingIssuable"
:id-key="index"
:display-reference="reference"
:can-remove="true" />
</li>
<li class="add-issuable-form-input-list-item">
<input
ref="input"
type="text"
class="js-add-issuable-form-input add-issuable-form-input"
:value="inputValue"
placeholder="Search issues..."
@input="onInput"
@focus="onFocus"
@blur="onBlur" />
</li>
</ul>
</div>
<div class="add-issuable-form-actions clearfix">
<button
ref="addButton"
type="button"
class="js-add-issuable-form-add-button btn btn-new pull-left"
@click="onFormSubmit"
:disabled="isSubmitButtonDisabled">
{{ addButtonLabel }}
</button>
<button
type="button"
class="btn btn-default pull-right"
@click="onFormCancel">
Cancel
</button>
</div>
</div>
</template>
<script>
import eventHub from '../event_hub';
export default {
name: 'IssueToken',
props: {
idKey: {
type: Number,
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: '',
},
canRemove: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
removeButtonLabel() {
return `Remove related issue ${this.displayReference}`;
},
hasState() {
return this.state && this.state.length > 0;
},
isOpen() {
return this.state === 'opened' || this.state === 'reopened';
},
isClosed() {
return this.state === 'closed';
},
hasTitle() {
return this.title.length > 0;
},
computedLinkElementType() {
return this.path.length > 0 ? 'a' : 'span';
},
computedPath() {
return this.path.length ? this.path : null;
},
},
methods: {
onRemoveRequest() {
let namespacePrefix = '';
if (this.eventNamespace && this.eventNamespace.length > 0) {
namespacePrefix = `${this.eventNamespace}-`;
}
eventHub.$emit(`${namespacePrefix}removeRequest`, this.idKey);
},
},
updated() {
const link = this.$refs.link;
const removeButton = this.$refs.removeButton;
if (link) {
$(link).tooltip('fixTitle');
}
if (removeButton) {
$(removeButton).tooltip('fixTitle');
}
},
};
</script>
<template>
<div class="issue-token">
<component
:is="this.computedLinkElementType"
ref="link"
class="issue-token-link"
:href="computedPath"
:title="title"
data-toggle="tooltip"
data-placement="top">
<span
ref="reference"
class="issue-token-reference">
<i
ref="stateIcon"
v-if="hasState"
class="fa"
:class="{
'issue-token-state-icon-open fa-circle-o': isOpen,
'issue-token-state-icon-closed fa-minus': isClosed,
}"
:aria-label="state">
</i>
{{ displayReference }}
</span>
<span
v-if="hasTitle"
ref="title"
class="js-issue-token-title issue-token-title"
:class="{ 'issue-token-title-standalone': !canRemove }">
<span class="issue-token-title-text">
{{ title }}
</span>
</span>
</component>
<button
ref="removeButton"
v-if="canRemove"
type="button"
class="js-issue-token-remove-button issue-token-remove-button"
:title="removeButtonLabel"
data-toggle="tooltip"
@click="onRemoveRequest">
<i
class="fa fa-times"
aria-hidden="true">
</i>
</button>
</div>
</template>
<script>
import eventHub from '../event_hub';
import loadingIcon from '../../../vue_shared/components/loading_icon.vue';
import issueToken from './issue_token.vue';
import addIssuableForm from './add_issuable_form.vue';
export default {
name: 'RelatedIssuesBlock',
props: {
isFetching: {
type: Boolean,
required: false,
default: false,
},
isSubmitting: {
type: Boolean,
required: false,
default: false,
},
relatedIssues: {
type: Array,
required: false,
default: () => [],
},
canAddRelatedIssues: {
type: Boolean,
required: false,
default: false,
},
isFormVisible: {
type: Boolean,
required: false,
default: false,
},
pendingReferences: {
type: Array,
required: false,
default: () => [],
},
inputValue: {
type: String,
required: false,
default: '',
},
helpPath: {
type: String,
required: false,
default: '',
},
autoCompleteSources: {
type: Object,
required: false,
default: () => ({}),
},
},
components: {
loadingIcon,
addIssuableForm,
issueToken,
},
computed: {
hasRelatedIssues() {
return this.relatedIssues.length > 0;
},
relatedIssueCount() {
return this.relatedIssues.length;
},
hasHelpPath() {
return this.helpPath.length > 0;
},
},
methods: {
toggleAddRelatedIssuesForm() {
eventHub.$emit('toggleAddRelatedIssuesForm');
},
},
updated() {
const addIssueButton = this.$refs.issueCountBadgeAddButton;
if (addIssueButton) {
$(addIssueButton).tooltip('fixTitle');
}
},
};
</script>
<template>
<div class="related-issues-block">
<div
class="panel-slim panel-default">
<div
class="panel-heading"
:class="{ 'panel-empty-heading': !this.hasRelatedIssues }">
<h3 class="panel-title related-issues-panel-title">
<div>
Related issues
<a
v-if="hasHelpPath"
:href="helpPath">
<i
class="related-issues-header-help-icon fa fa-question-circle"
aria-label="Read more about related issues">
</i>
</a>
<div class="js-related-issues-header-issue-count related-issues-header-issue-count issue-count-badge">
<span
class="issue-count-badge-count"
:class="{ 'has-btn': this.canAddRelatedIssues }">
{{ relatedIssueCount }}
</span>
<button
ref="issueCountBadgeAddButton"
v-if="canAddRelatedIssues"
type="button"
class="js-issue-count-badge-add-button issue-count-badge-add-button btn btn-small btn-default"
title="Add an issue"
aria-label="Add an issue"
data-toggle="tooltip"
data-placement="top"
@click="toggleAddRelatedIssuesForm">
<i
class="fa fa-plus"
aria-hidden="true">
</i>
</button>
</div>
</div>
<div>
<loadingIcon
ref="loadingIcon"
v-if="isFetching"
label="Fetching related issues" />
</div>
</h3>
</div>
<div
v-if="isFormVisible"
class="js-add-related-issues-form-area panel-body"
:class="{
'related-issues-add-related-issues-form-with-break': hasRelatedIssues
}">
<add-issuable-form
:is-submitting="isSubmitting"
:input-value="inputValue"
:pending-references="pendingReferences"
add-button-label="Add related issues"
:auto-complete-sources="autoCompleteSources" />
</div>
<div
v-if="hasRelatedIssues"
class="related-issues-token-body panel-body">
<ul
class="related-issues-token-list">
<li
:key="issue.id"
v-for="issue in relatedIssues"
class="js-related-issues-token-list-item related-issues-token-list-item">
<issue-token
event-namespace="relatedIssue"
:id-key="issue.id"
:display-reference="issue.reference"
:title="issue.title"
:path="issue.path"
:state="issue.state"
:can-remove="true" />
</li>
</ul>
</div>
</div>
</div>
</div>
</template>
<script>
/* global Flash */
/*
`rawReferences` are separated by spaces.
Given `abc 123 zxc`, `rawReferences = ['abc', '123', 'zxc']`
Consider you are typing `abc 123 zxc` in the input and your caret position is
at position 4 right before the `123` `rawReference`. Then you type `#` and
it becomes a valid reference, `#123`, but we don't want to jump it straight into
`pendingReferences` because you could still want to type. Say you typed `999`
and now we have `#999123`. Only when you move your caret away from that `rawReference`
do we actually put it in the `pendingReferences`.
Your caret can stop touching a `rawReference` can happen in a variety of ways:
- As you type, we only tokenize after you type a space or move with the arrow keys
- On blur, we consider your caret not touching anything
---
- When you click the "Add related issues"(in the `AddIssuableForm`),
we submit the `pendingReferences` to the server and they come back as actual `relatedIssues`
- When you click the "Cancel"(in the `AddIssuableForm`), we clear out `pendingReferences`
and hide the `AddIssuableForm` area.
*/
import eventHub from '../event_hub';
import RelatedIssuesBlock from './related_issues_block.vue';
import RelatedIssuesStore from '../stores/related_issues_store';
import RelatedIssuesService from '../services/related_issues_service';
const SPACE_FACTOR = 1;
export default {
name: 'RelatedIssuesRoot',
props: {
endpoint: {
type: String,
required: true,
},
canAddRelatedIssues: {
type: Boolean,
required: false,
default: false,
},
helpPath: {
type: String,
required: false,
default: '',
},
},
data() {
this.store = new RelatedIssuesStore();
return {
state: this.store.state,
isFetching: false,
isSubmitting: false,
isFormVisible: false,
inputValue: '',
};
},
components: {
relatedIssuesBlock: RelatedIssuesBlock,
},
computed: {
autoCompleteSources() {
return gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources;
},
},
methods: {
onRelatedIssueRemoveRequest(idToRemove) {
const issueToRemove = _.find(this.state.relatedIssues, issue => issue.id === idToRemove);
if (issueToRemove) {
this.service.removeRelatedIssue(issueToRemove.destroy_relation_path)
.then(res => res.json())
.then((data) => {
this.store.setRelatedIssues(data.issues);
})
.catch((res) => {
if (res && res.status !== 404) {
// eslint-disable-next-line no-new
new Flash('An error occurred while removing related issues.');
}
});
} else {
// eslint-disable-next-line no-new
new Flash('We could not determine the path to remove the related issue');
}
},
onToggleAddRelatedIssuesForm() {
this.isFormVisible = !this.isFormVisible;
},
onPendingIssueRemoveRequest(indexToRemove) {
this.store.removePendingRelatedIssue(indexToRemove);
},
onPendingFormSubmit() {
if (this.state.pendingReferences.length > 0) {
this.isSubmitting = true;
this.service.addRelatedIssues(this.state.pendingReferences)
.then(res => res.json())
.then((data) => {
// We could potentially lose some pending issues in the interim here
this.store.setPendingReferences([]);
this.store.setRelatedIssues(data.issues);
this.isSubmitting = false;
// Close the form on submission
this.isFormVisible = false;
})
.catch((res) => {
this.isSubmitting = false;
// eslint-disable-next-line no-new
new Flash(res.data.message || 'An error occurred while submitting related issues.');
});
}
},
onPendingFormCancel() {
this.isFormVisible = false;
this.store.setPendingReferences([]);
this.inputValue = '';
},
fetchRelatedIssues() {
this.isFetching = true;
this.service.fetchRelatedIssues()
.then(res => res.json())
.then((issues) => {
this.store.setRelatedIssues(issues);
this.isFetching = false;
})
.catch(() => new Flash('An error occurred while fetching related issues.'));
},
onInput(newValue, caretPos) {
const rawReferences = newValue
.split(/\s/);
let touchedReference;
let iteratingPos = 0;
const untouchedRawReferences = rawReferences
.filter((reference) => {
let isTouched = false;
if (caretPos >= iteratingPos && caretPos <= (iteratingPos + reference.length)) {
touchedReference = reference;
isTouched = true;
}
// `+ SPACE_FACTOR` to factor in the missing space we split at earlier
iteratingPos = iteratingPos + reference.length + SPACE_FACTOR;
return !isTouched;
})
.filter(reference => reference.trim().length > 0);
this.store.setPendingReferences(
this.state.pendingReferences.concat(untouchedRawReferences),
);
this.inputValue = `${touchedReference}`;
},
onBlur(newValue) {
const rawReferences = newValue
.split(/\s+/)
.filter(reference => reference.trim().length > 0);
this.store.setPendingReferences(
this.state.pendingReferences.concat(rawReferences),
);
this.inputValue = '';
},
},
created() {
eventHub.$on('relatedIssue-removeRequest', this.onRelatedIssueRemoveRequest);
eventHub.$on('toggleAddRelatedIssuesForm', this.onToggleAddRelatedIssuesForm);
eventHub.$on('pendingIssuable-removeRequest', this.onPendingIssueRemoveRequest);
eventHub.$on('addIssuableFormSubmit', this.onPendingFormSubmit);
eventHub.$on('addIssuableFormCancel', this.onPendingFormCancel);
eventHub.$on('addIssuableFormInput', this.onInput);
eventHub.$on('addIssuableFormBlur', this.onBlur);
this.service = new RelatedIssuesService(this.endpoint);
this.fetchRelatedIssues();
},
beforeDestroy() {
eventHub.$off('relatedIssue-removeRequest', this.onRelatedIssueRemoveRequest);
eventHub.$off('toggleAddRelatedIssuesForm', this.onToggleAddRelatedIssuesForm);
eventHub.$off('pendingIssuable-removeRequest', this.onPendingIssueRemoveRequest);
eventHub.$off('addIssuableFormSubmit', this.onPendingFormSubmit);
eventHub.$off('addIssuableFormCancel', this.onPendingFormCancel);
eventHub.$off('addIssuableFormInput', this.onInput);
eventHub.$off('addIssuableFormBlur', this.onBlur);
},
};
</script>
<template>
<related-issues-block
:help-path="helpPath"
:is-fetching="isFetching"
:is-submitting="isSubmitting"
:related-issues="state.relatedIssues"
:can-add-related-issues="canAddRelatedIssues"
:pending-references="state.pendingReferences"
:is-form-visible="isFormVisible"
:input-value="inputValue"
:auto-complete-sources="autoCompleteSources" />
</template>
import Vue from 'vue';
export default new Vue();
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();
}
addRelatedIssues(newIssueReferences) {
return this.relatedIssuesResource.save({}, {
issue_references: newIssueReferences,
});
}
// eslint-disable-next-line class-methods-use-this
removeRelatedIssue(endpoint) {
return Vue.http.delete(endpoint);
}
}
export default RelatedIssuesService;
class RelatedIssuesStore {
constructor() {
this.state = {
// Stores issue objects of the known related issues
relatedIssues: [],
// Stores references of the "staging area" related issues that are planned to be added
pendingReferences: [],
};
}
setRelatedIssues(issues) {
this.state.relatedIssues = issues;
}
removeRelatedIssue(idToRemove) {
this.state.relatedIssues = this.state.relatedIssues.filter(issue => issue.id !== idToRemove);
}
setPendingReferences(issues) {
this.state.pendingReferences = issues;
}
removePendingRelatedIssue(indexToRemove) {
this.state.pendingReferences =
this.state.pendingReferences.filter((reference, index) => index !== indexToRemove);
}
}
export default RelatedIssuesStore;
@mixin panel {
.panel-heading {
padding: $gl-vert-padding $gl-padding;
line-height: 36px;
.controls {
margin-top: -2px;
float: right;
}
.dropdown-menu-toggle {
line-height: 20px;
}
.badge {
margin-top: -2px;
margin-left: 5px;
}
&.split {
display: flex;
align-items: center;
}
.left {
flex: 1 1 auto;
}
.right {
flex: 0 0 auto;
text-align: right;
}
.panel {
margin-bottom: $gl-padding;
}
.panel-slim {
@extend .panel;
margin-bottom: $gl-vert-padding;
}
.panel-heading {
padding: $gl-vert-padding $gl-padding;
line-height: 36px;
.controls {
margin-top: -2px;
float: right;
}
 
.panel-body {
padding: $gl-padding;
.dropdown-menu-toggle {
line-height: 20px;
}
 
.form-actions {
margin: -$gl-padding;
margin-top: $gl-padding;
}
.badge {
margin-top: -2px;
margin-left: 5px;
}
 
.panel-title {
font-size: inherit;
line-height: inherit;
&.split {
display: flex;
align-items: center;
}
.left {
flex: 1 1 auto;
}
.right {
flex: 0 0 auto;
text-align: right;
}
}
 
.panel {
@include panel;
margin-bottom: $gl-padding;
.panel-empty-heading {
border-bottom: 0;
}
 
.panel-slim {
@extend .panel;
@include panel;
margin-bottom: $gl-vert-padding;
.panel-body {
padding: $gl-padding;
.form-actions {
margin: -$gl-padding;
margin-top: $gl-padding;
}
}
.panel-title {
font-size: inherit;
line-height: inherit;
}
Loading
Loading
@@ -597,4 +597,3 @@ Convdev Index
$color-high-score: $green-400;
$color-average-score: $orange-400;
$color-low-score: $red-400;
@import "./issues/issue_count_badge";
[v-cloak] {
display: none;
}
Loading
Loading
@@ -415,30 +417,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
@@ -727,3 +727,48 @@
}
}
}
.add-issuable-form-input-wrapper {
height: auto;
padding: $gl-vert-padding $gl-vert-padding 0 $gl-input-padding;
&.focus,
&.focus:hover {
border-color: $dropdown-input-focus-border;
box-shadow: 0 0 4px $search-input-focus-shadow-color;
}
}
.add-issuable-form-input-token-list {
display: flex;
flex-wrap: wrap;
align-items: baseline;
list-style: none;
margin-bottom: 0;
padding-left: 0;
}
.add-issuable-form-token-list-item {
max-width: 100%;
margin-bottom: $gl-vert-padding;
margin-right: 5px;
}
.add-issuable-form-input-list-item {
flex: 1;
min-width: 200px;
margin-bottom: $gl-vert-padding;
}
.add-issuable-form-input {
width: 100%;
border: 0;
&:focus {
outline: none;
}
}
.add-issuable-form-actions {
margin-top: $gl-padding;
}
@import "./issues/issue_count_badge";
@import "./issues/related_issues";
.issues-list {
.issue {
padding: 10px 0 10px $gl-padding;
Loading
Loading
@@ -275,3 +278,118 @@ ul.related-merge-requests > li {
}
}
}
.issue-token {
display: inline-flex;
align-items: stretch;
max-width: 100%;
line-height: 1.75;
white-space: nowrap;
}
.issue-token-link {
display: inline-flex;
min-width: 0;
color: $gl-text-color-secondary;
&[href] {
color: $gl-link-color;
}
&:hover,
&:focus {
outline: none;
text-decoration: none;
}
}
.issue-token-reference {
display: flex;
align-items: center;
margin-right: 1px;
padding-left: 0.5em;
padding-right: 0.5em;
background-color: $gray-lighter;
border-top-left-radius: 2px;
border-bottom-left-radius: 2px;
transition: background $general-hover-transition-duration $general-hover-transition-curve, color $general-hover-transition-duration $general-hover-transition-curve;
.issue-token:hover &,
.issue-token-link: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 {
overflow: hidden;
display: flex;
align-items: baseline;
padding-left: 0.5em;
background-color: $gray-normal;
color: $gl-text-color-secondary;
transition: background $general-hover-transition-duration $general-hover-transition-curve;
.issue-token:hover &,
.issue-token-link:focus > & {
background-color: $border-gray-normal;
}
& > .fa {
line-height: inherit;
}
}
.issue-token-title-standalone {
padding-right: 0.5em;
border-top-right-radius: 2px;
border-bottom-right-radius: 2px;
}
.issue-token-title-text {
overflow: hidden;
max-width: 264px;
text-overflow: ellipsis;
}
.issue-token-remove-button {
display: flex;
align-items: center;
padding: 0 0.5em;
background-color: $gray-normal;
border: 0;
border-top-right-radius: 2px;
border-bottom-right-radius: 2px;
color: $gl-text-color-secondary;
transition: background $general-hover-transition-duration $general-hover-transition-curve;
&:hover,
&:focus,
.issue-token:hover &,
.issue-token-link:focus + & {
background-color: $border-gray-normal;
outline: none;
}
& > .fa {
font-size: 0.9em;
}
}
.issue-count-badge {
display: inline-flex;
align-items: stretch;
height: 24px;
}
.issue-count-badge-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-badge-add-button {
display: flex;
align-items: center;
border: 1px solid $border-color;
border-radius: 0 $border-radius-base $border-radius-base 0;
line-height: 1;
}
$token_spacing_bottom: 0.5em;
.related-issues-block {
margin-top: 3 * $gl-vert-padding;
}
.related-issues-panel-title {
display: flex;
justify-content: space-between;
}
.related-issues-header-help-icon {
margin-left: 0.25em;
color: $gl-text-color-secondary;
}
.related-issues-header-issue-count {
margin-left: 0.5em;
}
.related-issues-add-related-issues-form-with-break {
border-bottom: 1px solid $border-color;
}
.related-issues-token-body {
padding-bottom: calc(#{$gl-padding} - #{$token_spacing_bottom});
}
.related-issues-token-list {
display: flex;
flex-wrap: wrap;
margin-bottom: 0;
padding-left: 0;
list-style: none;
}
.related-issues-token-list-item {
max-width: 100%;
margin-bottom: $token_spacing_bottom;
margin-right: 5px;
}
Loading
Loading
@@ -20,7 +20,9 @@ module SystemNoteHelper
'moved' => 'icon_arrow_circle_o_right',
'outdated' => 'icon_edit',
'approved' => 'icon_check',
'unapproved' => 'icon_fa_close'
'unapproved' => 'icon_fa_close',
'relate' => 'icon_anchor',
'unrelate' => 'icon_anchor_broken'
}.freeze
 
def icon_for_system_note(note)
Loading
Loading
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-badge.pull-right.clearfix{ "v-if" => 'list.type !== "blank"' }
%span.issue-count-badge-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-badge-add-button.btn.btn-small.btn-default.has-tooltip{ type: "button",
"@click" => "showNewIssueForm",
"v-if" => 'list.type !== "closed"',
"aria-label" => "New issue",
Loading
Loading
Loading
Loading
@@ -4,6 +4,9 @@
- page_card_attributes @issue.card_attributes
- can_update_issue = can?(current_user, :update_issue, @issue)
- can_report_spam = @issue.submittable_as_spam_by?(current_user)
- content_for :page_specific_javascripts do
= webpack_bundle_tag('common_vue')
= webpack_bundle_tag('issuable')
 
.clearfix.detail-page-header
.issuable-header
Loading
Loading
@@ -65,6 +68,11 @@
 
= edited_time_ago_with_tooltip(@issue, placement: 'bottom', html_class: 'issue-edited-ago js-issue-edited-ago')
 
- if can?(current_user, :read_issue_link, @project)
.js-related-issues-root{ data: { endpoint: namespace_project_issue_links_path(@project.namespace, @project, @issue),
can_add_related_issues: "#{can?(current_user, :update_issue, @issue)}",
help_path: help_page_path('user/project/issues/related_issues') } }
#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
@@ -5,6 +5,7 @@
- content_for :page_specific_javascripts do
= page_specific_javascript_bundle_tag('common_vue')
= page_specific_javascript_bundle_tag('diff_notes')
= webpack_bundle_tag('issuable')
 
.merge-request{ 'data-url' => merge_request_path(@merge_request, format: :json), 'data-project-path' => project_path(@merge_request.project) }
= render "projects/merge_requests/show/mr_title"
Loading
Loading
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" d="m8.419 7.99l-.002.002c-.023-.026-.046-.051-.071-.075-.642-.642-1.678-.651-2.312-.018l-2.432 2.432c-.635.635-.626 1.668.018 2.312.642.642 1.678.651 2.312.018l1.028-1.028c.719.366 1.481.518 2.176.444l-1.753 1.753c-1.344 1.344-3.542 1.326-4.909-.041-1.367-1.367-1.383-3.566-.041-4.909l2.292-2.292c1.344-1.344 3.542-1.326 4.909.041.016.016.032.032.048.049.009.008.017.016.025.024.362.362.367.944.011 1.3-.356.356-.938.351-1.3-.011m-.575.284l.002-.002c.023.026.046.051.071.075.642.642 1.678.651 2.312.018l2.432-2.432c.635-.635.626-1.668-.018-2.312-.642-.642-1.678-.651-2.312-.018l-1.028 1.028c-.719-.366-1.481-.518-2.176-.444l1.753-1.753c1.344-1.344 3.542-1.326 4.909.041 1.367 1.367 1.383 3.566.041 4.909l-2.292 2.292c-1.344 1.344-3.542 1.326-4.909-.041-.016-.016-.032-.032-.048-.049-.009-.008-.017-.016-.025-.024-.362-.362-.367-.944-.011-1.3.356-.356.938-.351 1.3.011"/></svg>
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