Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • rspeicher/gitlab
  • reprazent/gitlab
  • mwasilewski-gitlab/gitlab
  • mlapierre/gitlab
  • gl-gitaly/gitlab
  • gitlab-org/gitlab
  • 1and1_ebusiness/gitlab-ee
  • marin/gitlab-ee
  • max-group/gitlab-ee
  • cirosantilli/gitlab-ee
  • cernvcs/gitlab-ee
  • chrisrohr/gitlab-ee
  • stanhu/gitlab-ee
  • ealcantara/gitlab
  • sijis/gitlab-ee
  • kalibyrn/gitlab-ee
  • bbodenmiller/gitlab-ee
  • cvd/gitlab-ee
  • alexwarren/gitlab-ee
  • j.seto/gitlab
  • cmiskell/gitlab
  • Ashley/gitlab-ee
  • kkm/gitlab-ee
  • haynes/gitlab-ee
  • dblessing/gitlab-ee
  • allison.browne/gitlab
  • singingwolfboy/gitlab-ee
  • coderhugo/gitlab-ee
  • axil/gitlab-ee
  • truongsinh/gitlab-ee
  • tensky/gitlab-ee
  • rymai/gitlab-ee
  • jt-gitlab/gitlab-ee
  • patricio/gitlab-ee
  • b.lashen/gitlab-ee
  • acroitor/gitlab
  • rosiv/gitlab-ee
  • sune-keller/gitlab-ee
  • wiget/gitlab-ee
  • bandel/gitlab-ee
  • e4r7hbug/gitlab-ee
  • chaosaffe/gitlab-ee
  • yannick-croissant/gitlab-ee
  • jameslopez/gitlab-ee
  • xellor/gitlab-ee
  • ymotiongroup/gitlab-ee
  • cwilcox/gitlab-ee
  • daniel.hoffmann/gitlab-ee
  • johnnyeric/gitlab-ee
  • pidge/gitlab-ee
  • Posp/gitlab-ee
  • jonafato/gitlab-ee
  • Quintasan/gitlab
  • shobra/gitlab-ee
  • sleepyknife/gitlab-ee
  • yorickpeterse/gitlab
  • abualy/gitlab-ee
  • ci-test-user-mk/gitlab
  • jogo/gitlab-ee
  • foxnewsnetwork/gitlab-ee
  • riemers/gitlab-ee
  • pacoguzman/gitlab-ee
  • usr01/gitlab-ee
  • samuel.bernard/gitlab-ee
  • codelust/gitlab-ee
  • takitak/gitlab-ee
  • ftasdelen/gitlab-ee
  • qhcthanh/gitlab-ee
  • clairton/gitlab-ee
  • cuongtm/gitlab-ee
  • m5/gitlab-ee
  • Sivapuram/gitlab-ee
  • sbeleidy/gitlab-ee
  • baparici/gitlab-ee
  • Hubbitus/gitlab-ee
  • lbennett/gitlab-ee
  • klml/gitlab-ee
  • jaydeland/gitlab-ee
  • lupine/gitlab-ee
  • leftathome/gitlab-ee
  • ianb/gitlab-ee
  • mumayank/gitlab-ee
  • nagoyamavps/gitlab-ee
  • pattyhama/gitlab-ee
  • momirza/gitlab-ee
  • BhavaniM/gitlab-ee
  • morefice/gitlab
  • splattael-staging/gitlab
  • toanalien/gitlab-ee
  • clever_usr_name/gitlab-ee
  • oscar-lopez/gitlab-ee
  • svansteenis/gitlab-ee
  • prashtest/gitlab-ee
  • bonsai/gitlab-ee
  • graingert/gitlab-ee
  • niijv/gitlab-ee
  • abdiasshaw/gitlab-ee
  • peter9208/gitlab-ee
  • gforcada/gitlab-ee
  • broftkd/gitlab-ee
  • mkobel/gitlab-ee
  • mikaelz/gitlab-ee
  • OdNairy/gitlab-ee
  • christinebeaubrun/gitlab-ee
  • haakoo/gitlab-ee
  • Rencs/gitlab-ee
  • marcia/gitlab-ee
  • dehvmartins/gitlab-ee
  • rouzbeh84/gitlab-ee
  • gjunker/gitlab-ee
  • nick.thomas/gitlab-ee
  • heijmans/gitlab-ee
  • s.ghafarpour/gitlab-ee
  • markglenfletcher1/gitlab-ee
  • Benno/gitlab-ee
  • s2gluser/gitlab-ee
  • hazelyang/gitlab-ee
  • dewetblomerus/gitlab-ee
  • chrisbelyea/gitlab-ee
  • nick.volynkin/gitlab-ee
  • sevenseacat/gitlab-ee
  • yuanchenxi95/gitlab-ee
  • andyli/gitlab-ee
  • nacredata/gitlab-ee
  • srkaycg_admin/gitlab-ee
  • tagyangyang/gitlab-ee
  • miroslav.meca/gitlab-ee
  • cafed00d/gitlab-ee
  • iOrange/gitlab-ee
  • shackledtodesk/gitlab-ee
  • kalleva/gitlab-ee
  • paulsen.jan/gitlab-ee
  • hu19891110/gitlab-ee
  • gmeans/gitlab-ee
  • nithin2/gitlab-ee
  • dbelova/gitlab-ee
  • GeorgConradi/gitlab-ee
  • iunet/gitlab-ee
  • ephemeric/gitlab-ee
  • wendy0402/gitlab-ee
  • phil7/gitlab-ee
  • Eichi4Eichler/gitlab-ee
  • Munken/gitlab-ee
  • clim/gitlab-ee
  • averyduffin/gitlab-ee
  • DKovel/gitlab-ee
  • katrinleinweber/gitlab-ee
  • tmaier/gitlab-ee
  • Shahriar_Rabbi/gitlab-ee
  • Stretch96/gitlab-ee
  • brennanroberts/gitlab-ee
  • sanyatuning/gitlab-ee
  • hkrutzer/gitlab-ee
  • visualrobots/gitlab-ee
  • jieme/gitlab-ee
  • vansch/gitlab-ee
  • smuthusamy.fivedtech/gitlab-ee
  • almtoolbox/gitlab-ee
  • niketgupta6590/gitlab-ee
  • d.demichelis/gitlab-ee
  • LeclercA/gitlab-ee
  • bpietraga/gitlab-ee
  • ivikash/gitlab-ee
  • TobbenTM/gitlab-ee
  • YarNayar/gitlab-ee
  • dcondrey/gitlab-ee
  • atronah/gitlab-ee
  • mindupper/gitlab-ee
  • zyfran/gitlab-ee
  • savitojs/gitlab-ee
  • vino.v/gitlab-ee
  • athar/gitlab-ee
  • me63/gitlab-ee
  • l00mi/gitlab-ee
  • trentontri/gitlab-ee
  • peterlebrun/gitlab-ee
  • gwawr/gitlab-ee
  • isolation85/gitlab-ee
  • timothyandrew/gitlab-ee
  • shawonsoyket439/gitlab-ee
  • afolson/gitlab-ee
  • ismail-s/gitlab-ee
  • peterramsing/gitlab-ee
  • kason/gitlab-ee
  • al1_andre/gitlab-ee
  • pchojnacki/gitlab-ee
  • juliusv/gitlab-ee
  • scallopedllama/gitlab-ee
  • frech/gitlab-ee
  • z9g9l9/gitlab-ee
  • jamiekaw/gitlab-ee
  • mrburrito/gitlab-ee
  • headcrabmeat/gitlab-ee
  • cristiantmbr/gitlab-ee
  • revuel/gitlab-ee
  • Nnidyu/gitlab-ee
  • MrCirwos/gitlab-ee
  • Gutenevv/gitlab-ee
  • jmeyo/gitlab-ee
  • ollie314/gitlab-ee
  • sang9402/gitlab-ee
  • mickael9/gitlab-ee
  • vsizov/gitlab-ee
  • dosuken123/gitlab-ee
  • geoandri/gitlab-ee
  • afrastgeek/gitlab-ee
  • steveschooncts/gitlab-ee
  • dinsaw/gitlab-ee
  • xqua/gitlab-ee
  • smith-kyle/gitlab-ee
  • Mattlk13/gitlab-ee
  • andy_b_84/gitlab-ee
  • raansari/gitlab-ee
  • abitduck/gitlab-ee
  • mgresko/gitlab-ee
  • sidewinder12s/gitlab-ee
  • andresca/gitlab-ee
  • ccrebolder/gitlab-ee
  • rodrigo.pereira1/gitlab-ee
  • mohideen.rahuman/gitlab-ee
  • hebbet/gitlab-ee
  • smcgivern/gitlab-ee
  • heapifyman/gitlab-ee
  • TeNNoX/gitlab-ee
  • herryLi/gitlab-ee
  • AlexKalinin/gitlab-ee
  • vincedmg/gitlab-ee
  • martflu/gitlab-ee
  • derik/gitlab-ee
  • Ruby-and-Friends/gitlab-ee
  • wojciechlisik/gitlab-ee
  • winniehell-gitlab/gitlab-ee
  • Vaidehee5/gitlab-ee
  • vignesh.ravichandran02/gitlab-ee
  • tanyan2004/gitlab-ee
  • gitlabproject_s/gitlab-ee
  • virth/gitlab-ee
  • tmlee/gitlab-ee
  • hwdegroot/gitlab-ee
  • kybae/gitlab-ee
  • xiaogang_gitlab/gitlab-ee
  • jabber/gitlab-ee
  • eswar.madhira/gitlab-ee
  • alshamiri2/gitlab-ee
  • ba2014sheer/gitlab-ee
  • paulcodiny/gitlab-ee
  • glabio/gitlab-ee
  • lenghan1991/gitlab-ee
  • peterl/gitlab-ee
  • lucianomx/gitlab-ee
  • jbrandhorst/gitlab-ee
  • abushoeb/gitlab-ee
  • shellthor/gitlab-ee
  • jameshclrk/gitlab-ee
  • jontdelorme/gitlab-ee
  • tzc_007/gitlab-ee
  • indobits/gitlab-ee
  • dturner_ts/gitlab-ee
  • gauravkk22/gitlab-ee
  • andy9775/gitlab-ee
  • vladel/gitlab-ee
  • hakulatata99/gitlab-ee
  • lee0824/gitlab-ee
  • pk-codebox-evo/gitlab-ee
  • feshu/gitlab-ee
  • techguru/gitlab-ee
  • ei-grad/gitlab-ee
  • mccomput3rfr3ak/gitlab-ee
  • winnetou/gitlab-ee
  • julian-poidevin/gitlab-ee
  • arihantar/gitlab-ee
  • Trilom/gitlab-ee
  • Waysb1/gitlab-ee
  • jjung/gitlab-ee
  • xiaole/gitlab-ee
  • kikipq/gitlab-ee
  • rpadovani/gitlab-ee
  • zsturgess/gitlab-ee
  • mishunov/gitlab
  • nodeable/gitlab-ee
  • m1guelpiedrafita/gitlab-ee
  • jonas1/gitlab-ee
  • imranh/gitlab-ee
  • vitor.tyburski/gitlab-ee
  • pavel-voronin/gitlab-ee
  • alizadeh/gitlab-ee
  • aaaaaaq/gitlab-ee
  • JaKXz/gitlab-ee
  • umirra/gitlab-ee
  • jjcarstens/gitlab-ee
  • jenibella/gitlab-ee
  • hnk/gitlab-ee
  • roccolangeweg/gitlab-ee
  • batok/gitlab-ee
  • rajgomase24/gitlab-ee
  • qmnguyen0711/gitlab
  • 413063135/gitlab-ee
  • josephmarty/gitlab-ee
  • rdumont/gitlab-ee
  • gucong3000/gitlab-ee
  • ckatanda/gitlab-ee
  • overflowsith/gitlab-ee
  • TheJaredWilcurt/gitlab-ee
  • Riktos/gitlab-ee
  • losingle/gitlab-ee
  • mlushpenko/gitlab-ee
  • ericy_ts/gitlab-ee
  • fadb/gitlab-ee
  • dbvid/gitlab-ee
  • thejacer87/gitlab-ee
  • eli.boyarski/gitlab-ee
  • baldwinSPC/gitlab-ee
  • baldwinmathew/gitlab-ee
  • gsmethells/gitlab-ee
  • funkypenguin/gitlab-ee
  • rrentfro/gitlab-ee
  • M1TKO/gitlab-ee
  • SuriyaaKudoIsc/gitlab-ee
  • lmsurprenant/gitlab-ee
  • kay54088/gitlab-ee
  • pablo.catalina/gitlab-ee
  • xji/gitlab-ee
  • iscorer/gitlab-ee
  • kenny-evitt/gitlab-ee
  • Francis_Jude/gitlab-ee
  • xukaierwen/gitlab-ee
  • mustafayildirim/gitlab-ee
  • hh1/gitlab-ee
  • clamp27/gitlab-ee
  • UIPCO/gitlab-ee
  • chaase/gitlab-ee
  • matt.faure/gitlab-ee
  • wstomv/gitlab-ee
  • childNode/gitlab-ee
  • LockiStrike/gitlab-ee
  • Habiballah786/gitlab-ee
  • natebird/gitlab-ee
  • tamcv/gitlab-ee
  • adligithub/gitlab-ee
  • SuperDragon317/gitlab-ee
  • pkoretic/gitlab-ee
  • schoh/gitlab-ee
  • kpankonen/gitlab-ee
  • madnut_ua/gitlab-ee
  • smoothsailing/gitlab-ee
  • TimoSolo/gitlab-ee
  • collen/gitlab-ee
  • 0_0_0/gitlab-ee
  • innerwhisper/gitlab-ee
  • issue-reproduce-forks/gitlab-ee
  • szpak/gitlab-ee
  • zullusa/gitlab-ee
  • jamatute/gitlab-ee
  • mauriciomeirelles/gitlab-ee
  • anarcat/gitlab-ee
  • Shura/gitlab-ee
  • polarise/gitlab-ee
  • ecbrodie/gitlab-ee
  • odromark/gitlab-ee
  • senk/gitlab-ee
  • hiroponz/gitlab-ee
  • rawkode/gitlab-ee
  • HaPPyWaLLaCe/gitlab-ee
  • FeditskiyRoman/gitlab-ee
  • slrz/gitlab-ee
  • realsobek/gitlab-ee
  • Eternal21/gitlab-ee
  • leungpeng/gitlab-ee
  • nprabhu02/gitlab-ee
  • CodingGroup/gitlab-ee
  • smhoekstra/gitlab-ee
  • zeih/gitlab-ee
  • biancaghiurutan/gitlab-ee
  • bstrong/gitlab-ee
  • nap/gitlab-ee
  • mollybeth/gitlab-ee
  • Jeismeier/gitlab-ee
  • bouland/gitlab-ee
  • poneytruand/gitlab-ee
  • robotmay/gitlab
380 results
Show changes
Commits on Source (33)
Showing
with 1351 additions and 249 deletions
Loading
Loading
@@ -51,6 +51,7 @@ import BlobForkSuggestion from './blob/blob_fork_suggestion';
import UserCallout from './user_callout';
import { ProtectedTagCreate, ProtectedTagEditList } from './protected_tags';
import ShortcutsWiki from './shortcuts_wiki';
import GfmAutoComplete from './gfm_auto_complete';
 
import GeoNodes from './geo_nodes';
import ServiceDeskRoot from './projects/settings_service_desk/service_desk_root';
Loading
Loading
@@ -80,6 +81,8 @@ const ShortcutsBlob = require('./shortcuts_blob');
path = page.split(':');
shortcut_handler = null;
 
new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources).setup();
function initBlob() {
new LineHighlighter();
 
Loading
Loading
Loading
Loading
@@ -3,6 +3,8 @@
/* global DropzoneInput */
/* global autosize */
 
import GfmAutoComplete from './gfm_auto_complete';
window.gl = window.gl || {};
 
function GLForm(form) {
Loading
Loading
@@ -31,7 +33,7 @@ GLForm.prototype.setupForm = function() {
// remove notify commit author checkbox for non-commit notes
gl.utils.disableButtonIfEmptyField(this.form.find('.js-note-text'), this.form.find('.js-comment-button, .js-note-new-discussion'));
 
gl.GfmAutoComplete.setup(this.form.find('.js-gfm-input'));
new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources).setup(this.form.find('.js-gfm-input'));
new DropzoneInput(this.form);
autosize(this.textarea);
// form and textarea event listeners
Loading
Loading
require('./time_tracking/time_tracking_bundle');
import Vue from 'vue';
import RelatedIssuesRoot from './related_issues/components/related_issues_root.vue';
import './time_tracking/time_tracking_bundle';
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('relatedIssuesRoot', {
props: {
endpoint: relatedIssuesRootElement.dataset.endpoint,
currentNamespacePath: relatedIssuesRootElement.dataset.namespace,
currentProjectPath: relatedIssuesRootElement.dataset.project,
canAddRelatedIssues: typeof relatedIssuesRootElement.dataset.canAddRelatedIssues !== 'undefined' &&
relatedIssuesRootElement.dataset.canAddRelatedIssues !== 'false',
},
}),
});
}
});
<script>
import eventHub from '../event_hub';
import IssueToken from './issue_token.vue';
import GfmAutoComplete from '../../../gfm_auto_complete';
export default {
name: 'AddIssuableForm',
props: {
inputValue: {
type: String,
required: true,
},
addButtonLabel: {
type: String,
required: true,
},
pendingIssuables: {
type: Array,
required: false,
default: () => [],
},
},
components: {
issueToken: IssueToken,
},
methods: {
onInput() {
const value = this.$refs.input.value;
eventHub.$emit('addIssuableFormInput', value, $(this.$refs.input).caret('pos'));
},
onBlur() {
const value = this.$refs.input.value;
eventHub.$emit('addIssuableFormBlur', value);
},
onInputWrapperClick() {
this.$refs.input.focus();
},
onPendingIssuableRemoveRequest(reference) {
eventHub.$emit('addIssuableFormIssuableRemoveRequest', reference);
},
onFormSubmit() {
eventHub.$emit('addIssuableFormSubmit');
},
onFormCancel() {
eventHub.$emit('addIssuableFormCancel');
},
},
mounted() {
const $input = $(this.$refs.input);
new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources).setup($input, {
issues: true,
});
$input.on('inserted-issues.atwho', this.onInput);
},
beforeDestroy() {
const $input = $(this.$refs.input);
$input.off('inserted-issues.atwho', this.onInput);
},
};
</script>
<template>
<div>
<div
ref="issuable-form-wrapper"
class="add-issuable-form-input-wrapper form-control"
@click="onInputWrapperClick">
<ul class="add-issuable-form-input-token-list">
<li
:key="issuable.reference"
v-for="issuable in pendingIssuables"
class="add-issuable-form-input-token-list-item">
<issue-token
:reference="issuable.reference"
:title="issuable.title"
:path="issuable.path"
:state="issuable.state"
:can-remove="true"
@removeRequest="onPendingIssuableRemoveRequest(issuable.reference)" />
</li>
<li class="add-issuable-form-input-token-list-input-item">
<input
ref="input"
type="text"
class="add-issuable-form-input"
:value="inputValue"
placeholder="Search issues..."
@input="onInput"
@blur="onBlur" />
</li>
</ul>
</div>
<div class="clearfix prepend-top-10">
<button
type="button"
class="btn btn-new pull-left"
@click="onFormSubmit">
{{ addButtonLabel }}
</button>
<button
type="button"
class="btn btn-default pull-right"
@click="onFormCancel">
Cancel
</button>
</div>
</div>
</template>
<script>
export default {
name: 'IssueToken',
props: {
reference: {
type: String,
required: true,
},
title: {
type: String,
required: false,
default: '',
},
path: {
type: String,
required: false,
default: null,
},
state: {
type: String,
required: false,
default: null,
},
canRemove: {
type: Boolean,
required: false,
default: false,
},
},
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: {
onRemoveRequest() {
this.$emit('removeRequest');
},
},
};
</script>
<template>
<div class="issue-token">
<a
class="issue-token-reference"
:href="path"
:aria-label="accessibleLabel">
<i
v-if="stateIconClass"
:class="stateIconClass"
aria-hidden="true" />
{{ reference }}
</a>
<div class="issue-token-title">
<a
v-if="title"
class="issue-token-title-link"
:href="path"
aria-hidden="true"
tabindex="-1">
{{ title }}
</a>
<button
v-if="canRemove"
type="button"
class="issue-token-remove-button has-tooltip"
:title="removeButtonLabel"
@click="onRemoveRequest">
<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: () => [],
},
requestError: {
type: String,
required: false,
default: null,
},
canAddRelatedIssues: {
type: Boolean,
required: false,
default: false,
},
isAddRelatedIssuesFormVisible: {
type: Boolean,
required: false,
default: false,
},
pendingRelatedIssues: {
type: Array,
required: false,
default: () => [],
},
addRelatedIssuesFormInputValue: {
type: String,
required: false,
default: '',
},
},
components: {
addIssuableForm: AddIssuableForm,
issueToken: IssueToken,
},
computed: {
hasRelatedIssues() {
return this.relatedIssues.length > 0;
},
relatedIssueCount() {
return this.relatedIssues.length;
},
panelHeadingClass() {
return `panel-heading ${!this.hasRelatedIssues ? 'panel-empty-heading' : ''}`;
},
issueCountHolderCountClass() {
return `issue-count-holder-count ${this.canAddRelatedIssues ? 'has-btn' : ''}`;
},
},
methods: {
showAddRelatedIssuesForm() {
eventHub.$emit('showAddRelatedIssuesForm');
},
onRelatedIssueRemoveRequest(reference) {
eventHub.$emit('relatedIssueRemoveRequest', reference);
},
},
};
</script>
<template>
<div class="related-issues-block">
<div
v-if="requestError"
class="alert alert-danger">
<i class="fa fa-exclamation-circle" aria-hidden="true" />
{{ requestError }}
</div>
<div
class="panel-slim panel-default">
<div :class="panelHeadingClass">
<h3 class="panel-title">
Related issues
<a
href="TODO"
aria-label="Read more about related issues">
<i
class="related-issues-header-help-icon fa fa-question-circle"
aria-hidden="true" />
</a>
<div class="related-issues-header-issue-count issue-count-holder">
<span :class="issueCountHolderCountClass">
{{ relatedIssueCount }}
</span>
<button
ref="issue-count-holder-add-button"
v-if="canAddRelatedIssues"
type="button"
class="issue-count-holder-add-button btn btn-small btn-default has-tooltip"
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
ref="related-issues-add-related-issues-form"
v-if="isAddRelatedIssuesFormVisible"
class="related-issues-add-related-issues-form panel-body">
<add-issuable-form
:input-value="addRelatedIssuesFormInputValue"
:pending-issuables="pendingRelatedIssues"
add-button-label="Add related issues" />
</div>
<div
v-if="hasRelatedIssues"
class="panel-body">
<ul
class="related-issues-token-body">
<li
:key="issue.reference"
v-for="issue in relatedIssues"
class="related-issues-token-list-item">
<issue-token
:reference="issue.reference"
:title="issue.title"
:path="issue.path"
:state="issue.state"
:can-remove="issue.canRemove"
@removeRequest="onRelatedIssueRemoveRequest(issue.reference)" />
</li>
</ul>
</div>
</div>
</div>
</div>
</template>
<script>
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';
import {
ISSUABLE_REFERENCE_RE,
getReferencePieces,
assembleFullIssuableReference,
} from '../../../lib/utils/issuable_reference_utils';
export default {
name: 'RelatedIssuesRoot',
props: {
endpoint: {
type: String,
required: true,
},
currentNamespacePath: {
type: String,
required: true,
},
currentProjectPath: {
type: String,
required: true,
},
canAddRelatedIssues: {
type: Boolean,
required: false,
default: false,
},
},
data() {
this.store = new RelatedIssuesStore();
return {
state: this.store.state,
isAddRelatedIssuesFormVisible: false,
};
},
components: {
relatedIssuesBlock: RelatedIssuesBlock,
},
computed: {
computedRelatedIssues() {
return this.store.getIssuesFromReferences(
this.state.relatedIssues,
this.currentNamespacePath,
this.currentProjectPath,
);
},
computedPendingRelatedIssues() {
return this.store.getIssuesFromReferences(
this.state.pendingRelatedIssues,
this.currentNamespacePath,
this.currentProjectPath,
);
},
},
methods: {
onRelatedIssueRemoveRequest(reference) {
const fullReference = assembleFullIssuableReference(
reference,
this.currentNamespacePath,
this.currentProjectPath,
);
this.store.setRelatedIssues(this.state.relatedIssues.filter(ref => ref !== fullReference));
// Reset request error
this.store.setRequestError(null);
RelatedIssuesService.removeRelatedIssue(
this.state.issueMap[fullReference].destroy_relation_path,
)
.catch((err) => {
// Restore issue we were unable to delete
this.store.setRelatedIssues(this.state.relatedIssues.concat(fullReference));
this.store.setRequestError('An error occurred while removing related issues.');
throw err;
});
},
onShowAddRelatedIssuesForm() {
this.isAddRelatedIssuesFormVisible = true;
},
onAddIssuableFormInput(newValue, caretPos) {
const rawReferences = newValue.split(/\s/);
let touchedReference;
let iteratingPos = 0;
const untouchedReferences = rawReferences.filter((reference) => {
let isTouched = false;
if (caretPos >= iteratingPos && caretPos <= (iteratingPos + reference.length)) {
touchedReference = reference;
isTouched = true;
}
iteratingPos = iteratingPos + reference.length + 1;
return !isTouched;
});
const results = this.processIssuableReferences(untouchedReferences);
if (results.fullReferences.length > 0) {
this.store.setPendingRelatedIssues(
_.uniq(this.state.pendingRelatedIssues.concat(results.fullReferences)),
);
this.store.setAddRelatedIssuesFormInputValue(`${results.unprocessableReferences.map(ref => `${ref} `).join('')}${touchedReference}`);
}
},
onAddIssuableFormBlur(newValue) {
const rawReferences = newValue.split(/\s+/);
const results = this.processIssuableReferences(rawReferences);
this.store.setPendingRelatedIssues(
_.uniq(this.state.pendingRelatedIssues.concat(results.fullReferences)),
);
this.store.setAddRelatedIssuesFormInputValue(`${results.unprocessableReferences.join(' ')}`);
},
onAddIssuableFormIssuableRemoveRequest(reference) {
const fullReference = assembleFullIssuableReference(
reference,
this.currentNamespacePath,
this.currentProjectPath,
);
this.store.setPendingRelatedIssues(
this.state.pendingRelatedIssues.filter(ref => ref !== fullReference),
);
},
onAddIssuableFormSubmit() {
// Reset request error
this.store.setRequestError(null);
const currentPendingIssues = this.state.pendingRelatedIssues;
this.service.addRelatedIssues(currentPendingIssues)
.then(() => {
this.store.setRelatedIssues(this.state.relatedIssues.concat(currentPendingIssues));
})
.catch((err) => {
// Restore issues we were unable to submit
this.store.setPendingRelatedIssues(
_.uniq(this.state.pendingRelatedIssues.concat(currentPendingIssues)),
);
this.store.setRequestError('An error occurred while submitting related issues.');
throw err;
});
this.store.setPendingRelatedIssues([]);
},
onAddIssuableFormCancel() {
this.isAddRelatedIssuesFormVisible = false;
this.store.setPendingRelatedIssues([]);
this.store.setAddRelatedIssuesFormInputValue('');
},
fetchRelatedIssues() {
// Reset request error
this.store.setRequestError(null);
this.service.fetchRelatedIssues()
.then((issues) => {
const relatedIssueReferences = issues.map((issue) => {
const referenceKey = assembleFullIssuableReference(
issue.reference,
this.currentNamespacePath,
this.currentProjectPath,
);
this.store.addToIssueMap(referenceKey, issue);
return referenceKey;
});
this.store.setRelatedIssues(relatedIssueReferences);
})
.catch((err) => {
this.store.setRequestError('An error occurred while fetching related issues.');
throw err;
});
},
processIssuableReferences(rawReferences) {
const unprocessableReferences = [];
const fullReferences = rawReferences
.filter((reference) => {
const isValidReference = ISSUABLE_REFERENCE_RE.test(reference);
if (!isValidReference) {
unprocessableReferences.push(reference);
}
return isValidReference;
})
.map(reference => assembleFullIssuableReference(
reference,
this.currentNamespacePath,
this.currentProjectPath,
));
// Add some temporary placeholders to lookup
fullReferences.forEach((reference) => {
if (!this.state.issueMap[reference]) {
this.store.addToIssueMap(reference, {
reference,
fetchStatus: RelatedIssuesService.FETCHING_STATUS,
});
// Reset request error
this.store.setRequestError(null);
const referencePieces = getReferencePieces(reference);
const baseIssueEndpoint = `/${referencePieces.namespace}/${referencePieces.project}/issues/${referencePieces.issue}`;
RelatedIssuesService.fetchIssueInfo(`${baseIssueEndpoint}.json`)
.then((issue) => {
this.store.addToIssueMap(reference, {
path: baseIssueEndpoint,
reference,
state: issue.state,
title: issue.title,
});
})
.catch((err) => {
this.store.setRequestError('An error occurred while fetching issue info.');
throw err;
});
}
});
return {
unprocessableReferences,
fullReferences,
};
},
},
created() {
eventHub.$on('relatedIssueRemoveRequest', this.onRelatedIssueRemoveRequest);
eventHub.$on('showAddRelatedIssuesForm', this.onShowAddRelatedIssuesForm);
eventHub.$on('addIssuableFormInput', this.onAddIssuableFormInput);
eventHub.$on('addIssuableFormBlur', this.onAddIssuableFormBlur);
eventHub.$on('addIssuableFormIssuableRemoveRequest', this.onAddIssuableFormIssuableRemoveRequest);
eventHub.$on('addIssuableFormSubmit', this.onAddIssuableFormSubmit);
eventHub.$on('addIssuableFormCancel', this.onAddIssuableFormCancel);
this.service = new RelatedIssuesService(this.endpoint);
this.fetchRelatedIssues();
},
beforeDestroy() {
eventHub.$off('relatedIssueRemoveRequest', this.onRelatedIssueRemoveRequest);
eventHub.$off('showAddRelatedIssuesForm', this.onShowAddRelatedIssuesForm);
eventHub.$off('addIssuableFormInput', this.onAddIssuableFormInput);
eventHub.$off('addIssuableFormBlur', this.onAddIssuableFormBlur);
eventHub.$off('addIssuableFormIssuableRemoveRequest', this.onAddIssuableFormIssuableRemoveRequest);
eventHub.$off('addIssuableFormSubmit', this.onAddIssuableFormSubmit);
eventHub.$off('addIssuableFormCancel', this.onAddIssuableFormCancel);
},
};
</script>
<template>
<related-issues-block
:related-issues="computedRelatedIssues"
:request-error="state.requestError"
:canAddRelatedIssues="canAddRelatedIssues"
:is-add-related-issues-form-visible="isAddRelatedIssuesFormVisible"
:pending-related-issues="computedPendingRelatedIssues"
:add-related-issues-form-input-value="state.addRelatedIssuesFormInputValue" />
</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);
}
static fetchIssueInfo(endpoint) {
const issueResource = Vue.resource(endpoint);
return issueResource.get()
.then((res) => {
const issue = res.json();
return issue;
});
}
fetchRelatedIssues() {
return this.relatedIssuesResource.get()
.then((res) => {
const issues = res.json();
return issues;
});
}
addRelatedIssues(newIssueReferences) {
return this.relatedIssuesResource.save({}, {
issue_references: newIssueReferences,
})
.then((res) => {
const resData = res.json();
return resData;
});
}
static removeRelatedIssue(endpoint) {
const relatedIssueResource = Vue.resource(endpoint);
return relatedIssueResource.remove()
.then((res) => {
const resData = res.json();
return resData;
});
}
}
RelatedIssuesService.FETCHING_STATUS = 'FETCHING';
export default RelatedIssuesService;
import { assembleFullIssuableReference, assembleNecessaryIssuableReference } from '../../../lib/utils/issuable_reference_utils';
class RelatedIssuesStore {
constructor(initialState = {}) {
this.state = Object.assign({
issueMap: {},
relatedIssues: [],
pendingRelatedIssues: [],
requestError: null,
addRelatedIssuesFormInputValue: '',
}, initialState);
}
getIssuesFromReferences(references, namespacePath, projectPath) {
return references.map((reference) => {
const referenceKey = assembleFullIssuableReference(
reference,
namespacePath,
projectPath,
);
const displayReference = assembleNecessaryIssuableReference(
reference,
namespacePath,
projectPath,
);
const issueEntry = this.state.issueMap[referenceKey];
return {
path: issueEntry.path,
reference: displayReference,
title: issueEntry.title,
state: issueEntry.state,
canRemove: !!issueEntry.destroy_relation_path,
};
});
}
addToIssueMap(reference, issue) {
this.state.issueMap = {
...this.state.issueMap,
[reference]: issue,
};
}
setRelatedIssues(value) {
this.state.relatedIssues = value;
}
setPendingRelatedIssues(issues) {
this.state.pendingRelatedIssues = issues;
}
setRequestError(value) {
this.state.requestError = value;
}
setAddRelatedIssuesFormInputValue(value) {
this.state.addRelatedIssuesFormInputValue = value;
}
}
export default RelatedIssuesStore;
Loading
Loading
@@ -7,6 +7,8 @@
/* global dateFormat */
/* global Pikaday */
 
import GfmAutoComplete from './gfm_auto_complete';
(function() {
var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
 
Loading
Loading
@@ -22,7 +24,7 @@
this.renderWipExplanation = bind(this.renderWipExplanation, this);
this.resetAutosave = bind(this.resetAutosave, this);
this.handleSubmit = bind(this.handleSubmit, this);
gl.GfmAutoComplete.setup();
new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources).setup();
new UsersSelect();
new GroupsSelect();
new ZenMode();
Loading
Loading
const ISSUABLE_REFERENCE_RE = /^((?:[^\s/]+(?:\/(?!#))?)*)#(\d+)$/i;
function getReferencePieces(partialReference, namespacePath, projectPath) {
const [
,
fullNamespace = '',
resultantIssue,
] = partialReference.match(ISSUABLE_REFERENCE_RE);
const namespacePieces = fullNamespace.split('/');
const resultantNamespace = namespacePieces.length > 1 ? namespacePieces.slice(0, -1).join('/') : namespacePath;
const resultantProject = namespacePieces.slice(-1)[0] || projectPath;
return {
namespace: resultantNamespace,
project: resultantProject,
issue: resultantIssue,
};
}
function assembleNecessaryIssuableReference(
partialReference,
currentNamespacePath,
currentProjectPath,
) {
const {
namespace,
project,
issue,
} = getReferencePieces(partialReference, currentNamespacePath, currentProjectPath);
let necessaryReference = `#${issue}`;
if (currentProjectPath !== project) {
necessaryReference = project + necessaryReference;
}
if (currentNamespacePath !== namespace) {
necessaryReference = `${namespace}/${necessaryReference}`;
}
return necessaryReference;
}
function assembleFullIssuableReference(partialReference, currentNamespacePath, currentProjectPath) {
const {
namespace,
project,
issue,
} = getReferencePieces(partialReference, currentNamespacePath, currentProjectPath);
return `${namespace}/${project}#${issue}`;
}
export {
ISSUABLE_REFERENCE_RE,
getReferencePieces,
assembleNecessaryIssuableReference,
assembleFullIssuableReference,
};
Loading
Loading
@@ -97,7 +97,6 @@ import './dropzone_input';
import './due_date_select';
import './files_comment_button';
import './flash';
import './gfm_auto_complete';
import './gl_dropdown';
import './gl_field_error';
import './gl_field_errors';
Loading
Loading
Loading
Loading
@@ -11,7 +11,6 @@ require('./autosave');
window.autosize = require('vendor/autosize');
window.Dropzone = require('dropzone');
require('./dropzone_input');
require('./gfm_auto_complete');
require('vendor/jquery.caret'); // required by jquery.atwho
require('vendor/jquery.atwho');
require('./task_list');
Loading
Loading
Loading
Loading
@@ -32,6 +32,10 @@
}
}
 
.panel-empty-heading {
border-bottom: 0;
}
.panel-body {
padding: $gl-padding;
 
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,184 @@ 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;
white-space: nowrap;
}
.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 {
height: auto;
padding-top: $gl-vert-padding;
padding-left: $gl-input-padding;
padding-bottom: 0;
padding-right: $gl-input-padding;
}
.add-issuable-form-input-token-list {
display: flex;
flex-wrap: wrap;
list-style: none;
margin-bottom: 0;
padding-left: 0;
}
.add-issuable-form-input-token-list-item {
margin-bottom: $gl-vert-padding;
margin-right: 1em;
}
.add-issuable-form-input-token-list-input-item {
flex: 1;
min-width: 200px;
margin-bottom: $gl-vert-padding;
}
.add-issuable-form-input {
width: 100%;
border: 0;
&:focus {
outline: none;
}
}
.related-issues-block {
margin-top: $gl-vert-padding;
}
.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 {
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;
}
module Projects
class RelatedIssuesController < ApplicationController
before_action :authorize_read_related_issue!
before_action :authorize_admin_related_issue!, only: [:create, :destroy]
def index
render json: RelatedIssues::ListService.new(issue, current_user).execute
end
def create
opts = { issue_references: params[:issue_references] }
result = RelatedIssues::CreateService.new(issue, current_user, opts).execute
render json: result, status: result['http_status']
end
def destroy
related_issue = RelatedIssue.find(params[:id])
# In order to remove a given relation, one must be allowed to admin_related_issue both the current
# project and on the related issue project.
return render_404 unless can?(current_user, :admin_related_issue, related_issue.related_issue.project)
result = RelatedIssues::DestroyService.new(related_issue, current_user).execute
render json: result
end
private
def authorize_admin_related_issue!
return render_404 unless can?(current_user, :admin_related_issue, @project)
end
def authorize_read_related_issue!
return render_404 unless can?(current_user, :read_related_issue, @project)
end
def issue
@issue ||=
IssuesFinder.new(current_user, project_id: project.id)
.execute
.where(iid: params[:issue_id])
.first!
end
end
end
class RelatedIssue < ActiveRecord::Base
belongs_to :issue
belongs_to :related_issue, class_name: 'Issue'
validates :issue, presence: true
validates :related_issue, presence: true
validates :issue, uniqueness: { scope: :related_issue_id, message: 'is already related' }
validate :check_self_relation
private
def check_self_relation
return unless issue || related_issue
if issue == related_issue
errors.add(:issue, 'cannot be related to itself')
end
end
end