Commit ac9a53fe authored by Mike Greiling's avatar Mike Greiling

Merge branch '2035-add-blocking-issues-to-frontend' into 'master'

Resolve "Blocking issues MVC: (add support for issue dependencies)"

See merge request gitlab-org/gitlab!21415
parents 2ce3b765 c2ca79c4
......@@ -55,6 +55,10 @@
background-color: $gray-light;
}
.bg-white {
background-color: $white;
}
.bg-line-target-blue {
background: $line-target-blue;
}
......
......@@ -29,10 +29,13 @@
}
.border-width-1px { border-width: 1px; }
.border-bottom-width-1px { border-bottom-width: 1px; }
.border-style-dashed { border-style: dashed; }
.border-style-solid { border-style: solid; }
.border-bottom-style-solid { border-bottom-style: solid; }
.border-color-blue-300 { border-color: $blue-300; }
.border-color-default { border-color: $border-color; }
.border-bottom-color-default { border-bottom-color: $border-color; }
.box-shadow-default { box-shadow: 0 2px 4px 0 $black-transparent; }
.mh-50vh { max-height: 50vh; }
......
......@@ -44,6 +44,7 @@ class Projects::IssuesController < Projects::ApplicationController
before_action do
push_frontend_feature_flag(:vue_issuable_sidebar, project.group)
push_frontend_feature_flag(:issue_link_types, project)
end
around_action :allow_gitaly_ref_name_caching, only: [:discussions]
......
<script>
/* eslint-disable @gitlab/vue-i18n/no-bare-strings */
import { GlLoadingIcon } from '@gitlab/ui';
import { GlFormGroup, GlFormRadioGroup, GlLoadingIcon } from '@gitlab/ui';
import { __ } from '~/locale';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import RelatedIssuableInput from './related_issuable_input.vue';
import {
issuableTypesMap,
itemAddFailureTypesMap,
linkedIssueTypesMap,
addRelatedIssueErrorMap,
addRelatedItemErrorMap,
} from '../constants';
......@@ -12,9 +14,12 @@ import {
export default {
name: 'AddIssuableForm',
components: {
GlFormGroup,
GlFormRadioGroup,
GlLoadingIcon,
RelatedIssuableInput,
},
mixins: [glFeatureFlagsMixin()],
props: {
inputValue: {
type: String,
......@@ -55,6 +60,25 @@ export default {
default: itemAddFailureTypesMap.NOT_FOUND,
},
},
data() {
return {
linkedIssueType: linkedIssueTypesMap.RELATES_TO,
linkedIssueTypes: [
{
text: __('relates to'),
value: linkedIssueTypesMap.RELATES_TO,
},
{
text: __('blocks'),
value: linkedIssueTypesMap.BLOCKS,
},
{
text: __('is blocked by'),
value: linkedIssueTypesMap.IS_BLOCKED_BY,
},
],
};
},
computed: {
isSubmitButtonDisabled() {
return (
......@@ -74,7 +98,10 @@ export default {
this.$emit('pendingIssuableRemoveRequest', params);
},
onFormSubmit() {
this.$emit('addIssuableFormSubmit', this.$refs.relatedIssuableInput.$refs.input.value);
this.$emit('addIssuableFormSubmit', {
pendingReferences: this.$refs.relatedIssuableInput.$refs.input.value,
linkedIssueType: this.linkedIssueType,
});
},
onFormCancel() {
this.$emit('addIssuableFormCancel');
......@@ -91,6 +118,24 @@ export default {
<template>
<form @submit.prevent="onFormSubmit">
<template v-if="glFeatures.issueLinkTypes">
<gl-form-group
:label="__('The current issue')"
label-for="linked-issue-type-radio"
label-class="label-bold"
class="mb-2"
>
<gl-form-radio-group
id="linked-issue-type-radio"
v-model="linkedIssueType"
:options="linkedIssueTypes"
:checked="linkedIssueType"
/>
</gl-form-group>
<p class="bold">
{{ __('the following issue(s)') }}
</p>
</template>
<related-issuable-input
ref="relatedIssuableInput"
:focus-on-mount="true"
......@@ -116,7 +161,7 @@ export default {
class="js-add-issuable-form-add-button btn btn-success float-left qa-add-issue-button"
:class="{ disabled: isSubmitButtonDisabled }"
>
Add
{{ __('Add') }}
<gl-loading-icon v-if="isSubmitting" ref="loadingIcon" :inline="true" />
</button>
<button type="button" class="btn btn-default float-right" @click="onFormCancel">
......
<script>
import Sortable from 'sortablejs';
import IssueWeight from 'ee/boards/components/issue_card_weight.vue';
import sortableConfig from 'ee/sortable/sortable_config';
import { GlLoadingIcon } from '@gitlab/ui';
import { __ } from '~/locale';
import tooltip from '~/vue_shared/directives/tooltip';
import Icon from '~/vue_shared/components/icon.vue';
import RelatedIssuableItem from '~/vue_shared/components/issue/related_issuable_item.vue';
import IssueDueDate from '~/boards/components/issue_due_date.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import AddIssuableForm from './add_issuable_form.vue';
import { issuableIconMap, issuableQaClassMap } from '../constants';
import RelatedIssuesList from './related_issues_list.vue';
import {
issuableIconMap,
issuableQaClassMap,
linkedIssueTypesMap,
linkedIssueTypesTextMap,
} from '../constants';
export default {
name: 'RelatedIssuesBlock',
directives: {
tooltip,
},
components: {
Icon,
AddIssuableForm,
RelatedIssuableItem,
GlLoadingIcon,
IssueWeight,
IssueDueDate,
RelatedIssuesList,
},
mixins: [glFeatureFlagsMixin()],
props: {
isFetching: {
type: Boolean,
......@@ -79,11 +74,6 @@ export default {
required: false,
default: () => ({}),
},
title: {
type: String,
required: false,
default: __('Related issues'),
},
issuableType: {
type: String,
required: true,
......@@ -93,6 +83,22 @@ export default {
hasRelatedIssues() {
return this.relatedIssues.length > 0;
},
categorisedIssues() {
if (this.glFeatures.issueLinkTypes) {
return Object.values(linkedIssueTypesMap)
.map(linkType => ({
linkType,
issues: this.relatedIssues.filter(issue => issue.linkType === linkType),
}))
.filter(obj => obj.issues.length > 0);
}
return [
{
linkType: linkedIssueTypesMap.RELATES_TO,
issues: this.relatedIssues,
},
];
},
shouldShowTokenBody() {
return this.hasRelatedIssues || this.isFetching;
},
......@@ -111,57 +117,32 @@ export default {
qaClass() {
return issuableQaClassMap[this.issuableType];
},
validIssueWeight() {
if (this.issue) {
return this.issue.weight >= 0;
cardBodyCssClass() {
return this.glFeatures.issueLinkTypes
? {
'linked-issues-card-body': true,
'bg-gray-light': true,
'gl-p-3': this.isFormVisible || this.shouldShowTokenBody,
}
: {};
},
formCssClass() {
if (this.glFeatures.issueLinkTypes) {
return ['bordered-box', 'bg-white'];
}
return false;
if (this.hasRelatedIssues) {
return [
'border-bottom-width-1px',
'border-bottom-style-solid',
'border-bottom-color-default',
];
}
return [];
},
},
mounted() {
if (this.canReorder) {
this.sortable = Sortable.create(
this.$refs.list,
Object.assign({}, sortableConfig, {
onStart: this.addDraggingCursor,
onEnd: this.reordered,
}),
);
}
},
methods: {
getBeforeAfterId(itemEl) {
const prevItemEl = itemEl.previousElementSibling;
const nextItemEl = itemEl.nextElementSibling;
return {
beforeId: prevItemEl && parseInt(prevItemEl.dataset.orderingId, 0),
afterId: nextItemEl && parseInt(nextItemEl.dataset.orderingId, 0),
};
},
reordered(event) {
this.removeDraggingCursor();
const { beforeId, afterId } = this.getBeforeAfterId(event.item);
const { oldIndex, newIndex } = event;
this.$emit('saveReorder', {
issueId: parseInt(event.item.dataset.key, 10),
oldIndex,
newIndex,
afterId,
beforeId,
});
},
addDraggingCursor() {
document.body.classList.add('is-dragging');
},
removeDraggingCursor() {
document.body.classList.remove('is-dragging');
},
issuableOrderingId({ epic_issue_id: epicIssueId, id }) {
return this.issuableType === 'issue' ? epicIssueId : id;
},
created() {
this.linkedIssueTypesTextMap = linkedIssueTypesTextMap;
this.title = this.glFeatures.issueLinkTypes ? __('Linked issues') : __('Related issues');
},
};
</script>
......@@ -203,88 +184,41 @@ export default {
</div>
</h3>
</div>
<div
v-if="isFormVisible"
:class="{
'related-issues-add-related-issues-form-with-break': hasRelatedIssues,
}"
class="js-add-related-issues-form-area card-body"
>
<add-issuable-form
:is-submitting="isSubmitting"
:issuable-type="issuableType"
:input-value="inputValue"
:pending-references="pendingReferences"
:auto-complete-sources="autoCompleteSources"
:path-id-separator="pathIdSeparator"
@pendingIssuableRemoveRequest="$emit('pendingIssuableRemoveRequest', $event)"
@addIssuableFormInput="$emit('addIssuableFormInput', $event)"
@addIssuableFormBlur="$emit('addIssuableFormBlur', $event)"
@addIssuableFormSubmit="$emit('addIssuableFormSubmit', $event)"
@addIssuableFormCancel="$emit('addIssuableFormCancel', $event)"
/>
</div>
<div
v-if="shouldShowTokenBody"
:class="{ 'sortable-container': canReorder }"
class="related-issues-token-body"
>
<div v-if="isFetching" class="related-issues-loading-icon qa-related-issues-loading-icon">
<gl-loading-icon
ref="loadingIcon"
label="Fetching related issues"
class="prepend-top-5"
<div :class="cardBodyCssClass">
<div
v-if="isFormVisible"
class="js-add-related-issues-form-area card-body"
:class="formCssClass"
>
<add-issuable-form
:is-submitting="isSubmitting"
:issuable-type="issuableType"
:input-value="inputValue"
:pending-references="pendingReferences"
:auto-complete-sources="autoCompleteSources"
:path-id-separator="pathIdSeparator"
@pendingIssuableRemoveRequest="$emit('pendingIssuableRemoveRequest', $event)"
@addIssuableFormInput="$emit('addIssuableFormInput', $event)"
@addIssuableFormBlur="$emit('addIssuableFormBlur', $event)"
@addIssuableFormSubmit="$emit('addIssuableFormSubmit', $event)"
@addIssuableFormCancel="$emit('addIssuableFormCancel', $event)"
/>
</div>
<ul ref="list" :class="{ 'content-list': !canReorder }" class="related-items-list">
<li
v-for="issue in relatedIssues"
:key="issue.id"
:class="{
'user-can-drag': canReorder,
'sortable-row': canReorder,
'card card-slim': canReorder,
}"
:data-key="issue.id"
:data-ordering-id="issuableOrderingId(issue)"
class="js-related-issues-token-list-item list-item pt-0 pb-0"
>
<related-issuable-item
:id-key="issue.id"
:display-reference="issue.reference"
:confidential="issue.confidential"
:title="issue.title"
:path="issue.path"
:state="issue.state"
:milestone="issue.milestone"
:assignees="issue.assignees"
:created-at="issue.created_at"
:closed-at="issue.closed_at"
:can-remove="canAdmin"
:can-reorder="canReorder"
:path-id-separator="pathIdSeparator"
event-namespace="relatedIssue"
class="qa-related-issuable-item"
@relatedIssueRemoveRequest="$emit('relatedIssueRemoveRequest', $event)"
>
<span v-if="validIssueWeight" slot="weight" class="order-md-1">
<issue-weight
:weight="issue.weight"
class="item-weight d-flex align-items-center"
tag-name="span"
/>
</span>
<span v-if="issue.due_date" slot="dueDate" class="order-md-1">
<issue-due-date
:date="issue.due_date"
tooltip-placement="top"
css-class="item-due-date d-flex align-items-center"
/>
</span>
</related-issuable-item>
</li>
</ul>
<template v-if="shouldShowTokenBody">
<related-issues-list
v-for="category in categorisedIssues"
:key="category.linkType"
:heading="linkedIssueTypesTextMap[category.linkType]"
:can-admin="canAdmin"
:can-reorder="canReorder"
:is-fetching="isFetching"
:issuable-type="issuableType"
:path-id-separator="pathIdSeparator"
:related-issues="category.issues"
@relatedIssueRemoveRequest="$emit('relatedIssueRemoveRequest', $event)"
@saveReorder="$emit('saveReorder', $event)"
/>
</template>
</div>
</div>
</div>
......
<script>
import { GlLoadingIcon } from '@gitlab/ui';
import Sortable from 'sortablejs';
import IssueWeight from 'ee/boards/components/issue_card_weight.vue';
import sortableConfig from 'ee/sortable/sortable_config';
import IssueDueDate from '~/boards/components/issue_due_date.vue';
import RelatedIssuableItem from '~/vue_shared/components/issue/related_issuable_item.vue';
import tooltip from '~/vue_shared/directives/tooltip';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default {
name: 'RelatedIssuesList',
directives: {
tooltip,
},
components: {
GlLoadingIcon,
IssueDueDate,
IssueWeight,
RelatedIssuableItem,
},
mixins: [glFeatureFlagsMixin()],
props: {
canAdmin: {
type: Boolean,
required: false,
default: false,
},
canReorder: {
type: Boolean,
required: false,
default: false,
},
heading: {
type: String,
required: false,
default: '',
},
isFetching: {
type: Boolean,
required: false,
default: false,
},
issuableType: {
type: String,
required: true,
},
pathIdSeparator: {
type: String,
required: true,
},
relatedIssues: {
type: Array,
required: false,
default: () => [],
},
},
computed: {
validIssueWeight() {
return this.issue && this.issue.weight >= 0;
},
shouldRenderHeading() {
return this.glFeatures.issueLinkTypes && this.heading;
},
},
mounted() {
if (this.canReorder) {
this.sortable = Sortable.create(
this.$refs.list,
Object.assign({}, sortableConfig, {
onStart: this.addDraggingCursor,
onEnd: this.reordered,
}),
);
}
},
methods: {
getBeforeAfterId(itemEl) {
const prevItemEl = itemEl.previousElementSibling;
const nextItemEl = itemEl.nextElementSibling;
return {
beforeId: prevItemEl && parseInt(prevItemEl.dataset.orderingId, 0),
afterId: nextItemEl && parseInt(nextItemEl.dataset.orderingId, 0),
};
},
reordered(event) {
this.removeDraggingCursor();
const { beforeId, afterId } = this.getBeforeAfterId(event.item);
const { oldIndex, newIndex } = event;
this.$emit('saveReorder', {
issueId: parseInt(event.item.dataset.key, 10),
oldIndex,
newIndex,
afterId,
beforeId,
});
},
addDraggingCursor() {
document.body.classList.add('is-dragging');
},
removeDraggingCursor() {
document.body.classList.remove('is-dragging');
},
issuableOrderingId({ epicIssueId, id }) {
return this.issuableType === 'issue' ? epicIssueId : id;
},
},
};
</script>
<template>
<div>
<h4 v-if="shouldRenderHeading" class="gl-font-size-14 mt-0">{{ heading }}</h4>
<div
class="related-issues-token-body"
:class="{
'sortable-container': canReorder,
'bordered-box': glFeatures.issueLinkTypes,
'bg-white': glFeatures.issueLinkTypes,
}"
>
<div v-if="isFetching" class="related-issues-loading-icon qa-related-issues-loading-icon">
<gl-loading-icon ref="loadingIcon" label="Fetching linked issues" class="prepend-top-5" />
</div>
<ul ref="list" :class="{ 'content-list': !canReorder }" class="related-items-list">
<li
v-for="issue in relatedIssues"
:key="issue.id"
:class="{
'user-can-drag': canReorder,
'sortable-row': canReorder,
'card card-slim': canReorder,
}"
:data-key="issue.id"
:data-ordering-id="issuableOrderingId(issue)"
class="js-related-issues-token-list-item list-item pt-0 pb-0"
>
<related-issuable-item
:id-key="issue.id"
:display-reference="issue.reference"
:confidential="issue.confidential"
:title="issue.title"
:path="issue.path"
:state="issue.state"
:milestone="issue.milestone"
:assignees="issue.assignees"
:created-at="issue.createdAt"
:closed-at="issue.closedAt"
:can-remove="canAdmin"
:can-reorder="canReorder"
:path-id-separator="pathIdSeparator"
event-namespace="relatedIssue"
class="qa-related-issuable-item"
@relatedIssueRemoveRequest="$emit('relatedIssueRemoveRequest', $event)"
>
<span v-if="validIssueWeight" slot="weight" class="order-md-1">
<issue-weight
:weight="issue.weight"
class="item-weight d-flex align-items-center"
tag-name="span"
/>
</span>
<span v-if="issue.dueDate" slot="dueDate" class="order-md-1">
<issue-due-date
:date="issue.dueDate"
tooltip-placement="top"
css-class="item-due-date d-flex align-items-center"
/>
</span>
</related-issuable-item>
</li>
</ul>
</div>
</div>
</template>
......@@ -23,7 +23,6 @@ Your caret can stop touching a `rawReference` can happen in a variety of ways:
and hide the `AddIssuableForm` area.
*/
import _ from 'underscore';
import Flash from '~/flash';
import { __ } from '~/locale';
import RelatedIssuesBlock from './related_issues_block.vue';
......@@ -62,11 +61,6 @@ export default {
required: false,
default: '',
},
title: {
type: String,
required: false,
default: __('Related issues'),
},
issuableType: {
type: String,
required: false,
......@@ -110,11 +104,14 @@ export default {
this.fetchRelatedIssues();
},
methods: {
findRelatedIssueById(id) {
return this.state.relatedIssues.find(issue => issue.id === id);
},
onRelatedIssueRemoveRequest(idToRemove) {
const issueToRemove = _.find(this.state.relatedIssues, issue => issue.id === idToRemove);
const issueToRemove = this.findRelatedIssueById(idToRemove);
if (issueToRemove) {
RelatedIssuesService.remove(issueToRemove.relation_path)
RelatedIssuesService.remove(issueToRemove.relationPath)
.then(({ data }) => {
this.store.setRelatedIssues(data.issuables);
})
......@@ -133,13 +130,13 @@ export default {
onPendingIssueRemoveRequest(indexToRemove) {
this.store.removePendingRelatedIssue(indexToRemove);
},
onPendingFormSubmit(newValue) {
this.processAllReferences(newValue);
onPendingFormSubmit(event) {
this.processAllReferences(event.pendingReferences);
if (this.state.pendingReferences.length > 0) {
this.isSubmitting = true;
this.service
.addRelatedIssues(this.state.pendingReferences)
.addRelatedIssues(this.state.pendingReferences, event.linkedIssueType)
.then(({ data }) => {
// We could potentially lose some pending issues in the interim here
this.store.setPendingReferences([]);
......@@ -179,11 +176,11 @@ export default {
});
},
saveIssueOrder({ issueId, beforeId, afterId, oldIndex, newIndex }) {
const issueToReorder = _.find(this.state.relatedIssues, issue => issue.id === issueId);
const issueToReorder = this.findRelatedIssueById(issueId);
if (issueToReorder) {
RelatedIssuesService.saveOrder({
endpoint: issueToReorder.relation_path,
endpoint: issueToReorder.relationPath,
move_before_id: beforeId,
move_after_id: afterId,
})
......@@ -227,7 +224,6 @@ export default {
:is-form-visible="isFormVisible"
:input-value="inputValue"
:auto-complete-sources="autoCompleteSources"
:title="title"
:issuable-type="issuableType"
:path-id-separator="pathIdSeparator"
@saveReorder="saveIssueOrder"
......
......@@ -6,6 +6,18 @@ export const issuableTypesMap = {
MERGE_REQUEST: 'merge_request',
};
export const linkedIssueTypesMap = {
BLOCKS: 'blocks',
IS_BLOCKED_BY: 'is_blocked_by',
RELATES_TO: 'relates_to',
};
export const linkedIssueTypesTextMap = {
[linkedIssueTypesMap.RELATES_TO]: __('Relates to'),
[linkedIssueTypesMap.BLOCKS]: __('Blocks'),
[linkedIssueTypesMap.IS_BLOCKED_BY]: __('Is blocked by'),
};
export const autoCompleteTextMap = {
true: {
[issuableTypesMap.ISSUE]: __(' or <#issue id>'),
......
import axios from '~/lib/utils/axios_utils';
import { linkedIssueTypesMap } from '../constants';
class RelatedIssuesService {
constructor(endpoint) {
......@@ -9,9 +10,10 @@ class RelatedIssuesService {
return axios.get(this.endpoint);
}
addRelatedIssues(newIssueReferences) {
addRelatedIssues(newIssueReferences, linkType = linkedIssueTypesMap.RELATES_TO) {
return axios.post(this.endpoint, {
issuable_references: newIssueReferences,
link_type: linkType,
});
}
......
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
class RelatedIssuesStore {
constructor() {
this.state = {
......@@ -9,7 +11,7 @@ class RelatedIssuesStore {
}
setRelatedIssues(issues = []) {
this.state.relatedIssues = issues;
this.state.relatedIssues = convertObjectPropsToCamelCase(issues, { deep: true });
}
removeRelatedIssue(idToRemove) {
......
......@@ -118,8 +118,8 @@ export default {
this.addPendingReferences(this.getRawRefs(newValue));
this.setItemInputValue('');
},
handleAddItemFormSubmit(newValue) {
this.handleAddItemFormBlur(newValue);
handleAddItemFormSubmit(event) {
this.handleAddItemFormBlur(event.pendingReferences);
if (this.pendingReferences.length > 0) {
this.addItem();
......
......@@ -13,10 +13,6 @@ $token-spacing-bottom: 0.5em;
margin-left: 0.5em;
}
.related-issues-add-related-issues-form-with-break {
border-bottom: 1px solid $border-color;
}
.related-issues-token-body {
padding: 0;
......@@ -59,3 +55,7 @@ $token-spacing-bottom: 0.5em;
.issue-token-end {
order: 1;
}
.linked-issues-card-body > * + * {
margin-top: $gl-padding;
}
......@@ -420,6 +420,136 @@ describe 'Related issues', :js do
expect(find('.js-related-issues-header-issue-count')).to have_content('2')
end
end
context 'with issue_link_types feature flag enabled' do
def add_linked_issue(issue, radio_input_value)
find('.js-issue-count-badge-add-button').click
find('.js-add-issuable-form-input').set "#{issue.to_reference(project)} "
find("input[name=\"linked-issue-type-radio\"][value=\"#{radio_input_value}\"]").click
find('.js-add-issuable-form-add-button').click
wait_for_requests
end
before do
stub_feature_flags(issue_link_types: true)
visit project_issue_path(project, issue_a)
wait_for_requests
end
context 'when adding a "relates_to" issue' do
before do
add_linked_issue(issue_b, "relates_to")
end
it 'shows "Relates to" heading' do
headings = all('.linked-issues-card-body h4')
expect(headings.count).to eq(1)
expect(headings[0].text).to eq("Relates to")
end
it 'shows the added issue' do
items = all('.item-title a')
expect(items[0].text).to eq(issue_b.title)
expect(find('.js-related-issues-header-issue-count')).to have_content('1')
end
end
context 'when adding a "blocks" issue' do
before do
add_linked_issue(issue_b, "blocks")
end
it 'shows "Blocks" heading' do
headings = all('.linked-issues-card-body h4')
expect(headings.count).to eq(1)
expect(headings[0].text).to eq("Blocks")
end
it 'shows the added issue' do
items = all('.item-title a')
expect(items[0].text).to eq(issue_b.title)
expect(find('.js-related-issues-header-issue-count')).to have_content('1')
end
end
context 'when adding an "is_blocked_by" issue' do
before do
add_linked_issue(issue_b, "is_blocked_by")
end
it 'shows "Is blocked by" heading' do
headings = all('.linked-issues-card-body h4')
expect(headings.count).to eq(1)
expect(headings[0].text).to eq("Is blocked by")
end
it 'shows the added issue' do
items = all('.item-title a')
expect(items[0].text).to eq(issue_b.title)
expect(find('.js-related-issues-header-issue-count')).to have_content('1')
end
end
context 'when adding "relates_to", "blocks", and "is_blocked_by" issues' do
before do
add_linked_issue(issue_b, "relates_to")
add_linked_issue(issue_c, "blocks")
add_linked_issue(issue_d, "is_blocked_by")
end
it 'shows "Blocks", "Is blocked by", and "Relates to" headings' do
headings = all('.linked-issues-card-body h4')
expect(headings.count).to eq(3)
expect(headings[0].text).to eq("Blocks")
expect(headings[1].text).to eq("Is blocked by")
expect(headings[2].text).to eq("Relates to")
end
it 'shows all added issues' do
items = all('.item-title a')
expect(items.count).to eq(3)
expect(find('.js-related-issues-header-issue-count')).to have_content('3')
end
end
end
context 'with issue_link_types feature flag disabled' do
before do
stub_feature_flags(issue_link_types: false)
visit project_issue_path(project, issue_b)
wait_for_requests
end
context 'when adding an issue' do
before do
find('.js-issue-count-badge-add-button').click
find('.js-add-issuable-form-input').set "#{issue_c.to_reference(project)} "
find('.js-add-issuable-form-add-button').click
wait_for_requests
end
it 'does not group it into "Blocks", "Is blocked by", or "Relates to" headings' do
headings = all('.linked-issues-card-body h4')
expect(headings.count).to eq(0)
end
it 'shows the added issue' do
items = all('.item-title a')
expect(items.count).to eq(1)
end
end
end
end
end
end
......@@ -122,10 +122,12 @@ describe('RelatedItemsTreeApp', () => {
describe('handleAddItemFormSubmit', () => {
it('calls `addItem` action when `pendingReferences` prop in state is not empty', () => {
const newValue = '&1 &2';
const emitObj = {
pendingReferences: '&1 &2',
};
jest.spyOn(wrapper.vm, 'addItem').mockImplementation();
wrapper.vm.handleAddItemFormSubmit(newValue);
wrapper.vm.handleAddItemFormSubmit(emitObj);
expect(wrapper.vm.addItem).toHaveBeenCalled();
});
......
import Vue from 'vue';
import { PathIdSeparator } from 'ee/related_issues/constants';
import addIssuableForm from 'ee/related_issues/components/add_issuable_form.vue';
import { mount } from '@vue/test-utils';
import { linkedIssueTypesMap, PathIdSeparator } from 'ee/related_issues/constants';
import AddIssuableForm from 'ee/related_issues/components/add_issuable_form.vue';
const issuable1 = {
id: 200,
......@@ -22,56 +22,49 @@ const issuable2 = {
const pathIdSeparator = PathIdSeparator.Issue;
describe('AddIssuableForm', () => {
let AddIssuableForm;
let vm;
const findFormInput = wrapper => wrapper.find('.js-add-issuable-form-input').element;
beforeEach(() => {
AddIssuableForm = Vue.extend(addIssuableForm);
});
const findRadioInput = (inputs, value) => inputs.filter(input => input.element.value === value)[0];
afterEach(() => {
if (vm) {
// Avoid any NPE errors from `@blur` being called
// after `vm.$destroy` in tests, https://github.com/vuejs/vue/issues/5829
document.activeElement.blur();
describe('AddIssuableForm', () => {
let wrapper;
vm.$destroy();
}
afterEach(() => {
wrapper.destroy();
});
describe('with data', () => {
describe('without references', () => {
describe('without any input text', () => {
beforeEach(() => {
vm = new AddIssuableForm({
wrapper = mount(AddIssuableForm, {
propsData: {
inputValue: '',
pendingReferences: [],
pathIdSeparator,
},
}).$mount();
});
});
it('should have disabled submit button', () => {
expect(vm.$refs.addButton.disabled).toBe(true);
expect(vm.$refs.loadingIcon).toBeUndefined();
expect(wrapper.vm.$refs.addButton.disabled).toBe(true);
expect(wrapper.vm.$refs.loadingIcon).toBeUndefined();
});
});
describe('with input text', () => {
beforeEach(() => {
vm = new AddIssuableForm({
wrapper = mount(AddIssuableForm, {
propsData: {
inputValue: 'foo',
pendingReferences: [],
pathIdSeparator,
},
}).$mount();
});
});
it('should not have disabled submit button', () => {
expect(vm.$refs.addButton.disabled).toBe(false);
expect(wrapper.vm.$refs.addButton.disabled).toBe(false);
});
});
});
......@@ -80,52 +73,142 @@ describe('AddIssuableForm', () => {
const inputValue = 'foo #123';
beforeEach(() => {
vm = new AddIssuableForm({
wrapper = mount(AddIssuableForm, {
propsData: {
inputValue,
pendingReferences: [issuable1.reference, issuable2.reference],
pathIdSeparator,
},
}).$mount();
});
});
it('should put input value in place', () => {
expect(vm.$el.querySelector('.js-add-issuable-form-input').value).toEqual(inputValue);
expect(findFormInput(wrapper).value).toEqual(inputValue);
});
it('should render pending issuables items', () => {
expect(vm.$el.querySelectorAll('.js-add-issuable-form-token-list-item').length).toEqual(2);
expect(wrapper.findAll('.js-add-issuable-form-token-list-item').length).toEqual(2);
});
it('should not have disabled submit button', () => {
expect(vm.$refs.addButton.disabled).toBe(false);
expect(wrapper.vm.$refs.addButton.disabled).toBe(false);
});
});
it('when submitting pending issues', () => {
vm = new AddIssuableForm({
it('should emit the `addIssuableFormSubmit` event when submitting pending issues', () => {
wrapper = mount(AddIssuableForm, {
propsData: {
inputValue: 'foo #123',
pendingReferences: [issuable1.reference, issuable2.reference],
pathIdSeparator,
},
});
vm.$mount();
spyOn(vm, '$emit');
spyOn(wrapper.vm, '$emit');
const newInputValue = 'filling in things';
const inputEl = vm.$el.querySelector('.js-add-issuable-form-input');
const inputEl = findFormInput(wrapper);
inputEl.value = newInputValue;
vm.onFormSubmit();
wrapper.vm.onFormSubmit();
expect(vm.$emit).toHaveBeenCalledWith('addIssuableFormSubmit', newInputValue);
expect(wrapper.vm.$emit).toHaveBeenCalledWith('addIssuableFormSubmit', {
pendingReferences: newInputValue,
linkedIssueType: linkedIssueTypesMap.RELATES_TO,
});
});
it('when canceling form to collapse', () => {
spyOn(vm, '$emit');
vm.onFormCancel();
it('should emit the `addIssuableFormCancel` event when canceling form to collapse', () => {
spyOn(wrapper.vm, '$emit');
wrapper.vm.onFormCancel();
expect(wrapper.vm.$emit).toHaveBeenCalledWith('addIssuableFormCancel');
});
});
expect(vm.$emit).toHaveBeenCalledWith('addIssuableFormCancel');
describe('with :issue_link_types feature flag on', () => {
beforeEach(() => {
wrapper = mount(AddIssuableForm, {
propsData: {
inputValue: '',
pendingReferences: [],
pathIdSeparator,
},
provide: {
glFeatures: {
issueLinkTypes: true,
},
},
});
});
describe('radio buttons', () => {
let radioInputs;
beforeEach(() => {
radioInputs = wrapper.findAll('[name="linked-issue-type-radio"]');
});
it('shows "relates to" option', () => {
expect(findRadioInput(radioInputs, linkedIssueTypesMap.RELATES_TO)).not.toBeNull();
});
it('shows "blocks" option', () => {
expect(findRadioInput(radioInputs, linkedIssueTypesMap.BLOCKS)).not.toBeNull();
});
it('shows "is blocked by" option', () => {
expect(findRadioInput(radioInputs, linkedIssueTypesMap.IS_BLOCKED_BY)).not.toBeNull();
});
it('shows 3 options in total', () => {
expect(radioInputs.length).toBe(3);
});
});
describe('when the form is submitted', () => {
it('emits an event with a "relates_to" link type when the "relates to" radio input selected', done => {
spyOn(wrapper.vm, '$emit');
wrapper.vm.linkedIssueType = linkedIssueTypesMap.RELATES_TO;
wrapper.vm.onFormSubmit();
wrapper.vm.$nextTick(() => {
expect(wrapper.vm.$emit).toHaveBeenCalledWith('addIssuableFormSubmit', {
pendingReferences: '',
linkedIssueType: linkedIssueTypesMap.RELATES_TO,
});
done();
});
});
it('emits an event with a "blocks" link type when the "blocks" radio input selected', done => {
spyOn(wrapper.vm, '$emit');
wrapper.vm.linkedIssueType = linkedIssueTypesMap.BLOCKS;
wrapper.vm.onFormSubmit();
wrapper.vm.$nextTick(() => {
expect(wrapper.vm.$emit).toHaveBeenCalledWith('addIssuableFormSubmit', {
pendingReferences: '',
linkedIssueType: linkedIssueTypesMap.BLOCKS,
});
done();
});
});
it('emits an event with a "is_blocked_by" link type when the "is blocked by" radio input selected', done => {
spyOn(wrapper.vm, '$emit');
wrapper.vm.linkedIssueType = linkedIssueTypesMap.IS_BLOCKED_BY;
wrapper.vm.onFormSubmit();
wrapper.vm.$nextTick(() => {
expect(wrapper.vm.$emit).toHaveBeenCalledWith('addIssuableFormSubmit', {
pendingReferences: '',
linkedIssueType: linkedIssueTypesMap.IS_BLOCKED_BY,
});
done();
});
});
});
});
});
import Vue from 'vue';
import relatedIssuesBlock from 'ee/related_issues/components/related_issues_block.vue';
import { mount } from '@vue/test-utils';
import RelatedIssuesBlock from 'ee/related_issues/components/related_issues_block.vue';
import {
issuable1,
issuable2,
issuable3,
issuable4,
issuable5,
} from 'spec/vue_shared/components/issue/related_issuable_mock_data';
import { PathIdSeparator } from 'ee/related_issues/constants';
import {
linkedIssueTypesMap,
linkedIssueTypesTextMap,
PathIdSeparator,
} from 'ee/related_issues/constants';
describe('RelatedIssuesBlock', () => {
let RelatedIssuesBlock;
let vm;
beforeEach(() => {
RelatedIssuesBlock = Vue.extend(relatedIssuesBlock);
});
let wrapper;
afterEach(() => {
if (vm) {
vm.$destroy();
}
wrapper.destroy();
});
describe('with defaults', () => {
beforeEach(() => {
vm = new RelatedIssuesBlock({
wrapper = mount(RelatedIssuesBlock, {
propsData: {
pathIdSeparator: PathIdSeparator.Issue,
issuableType: 'issue',
},
}).$mount();
});
});
it('unable to add new related issues', () => {
expect(vm.$refs.issueCountBadgeAddButton).toBeUndefined();
expect(wrapper.vm.$refs.issueCountBadgeAddButton).toBeUndefined();
});
it('add related issues form is hidden', () => {
expect(vm.$el.querySelector('.js-add-related-issues-form-area')).toBeNull();
});
it('should not show loading icon', () => {
expect(vm.$refs.loadingIcon).toBeUndefined();
expect(wrapper.contains('.js-add-related-issues-form-area')).toBe(false);
});
});
describe('with isFetching=true', () => {
beforeEach(() => {
vm = new RelatedIssuesBlock({
wrapper = mount(RelatedIssuesBlock, {
propsData: {
pathIdSeparator: PathIdSeparator.Issue,
isFetching: true,
issuableType: 'issue',
},
}).$mount();
});
it('should show loading icon', () => {
expect(vm.$refs.loadingIcon).toBeDefined();
});
});
it('should show `...` badge count', () => {
expect(vm.badgeLabel).toBe('...');
expect(wrapper.vm.badgeLabel).toBe('...');
});
});
describe('with canAddRelatedIssues=true', () => {
beforeEach(() => {
vm = new RelatedIssuesBlock({
wrapper = mount(RelatedIssuesBlock, {
propsData: {
pathIdSeparator: PathIdSeparator.Issue,
canAdmin: true,
issuableType: 'issue',
},
}).$mount();
});
});
it('can add new related issues', () => {
expect(vm.$refs.issueCountBadgeAddButton).toBeDefined();
expect(wrapper.vm.$refs.issueCountBadgeAddButton).toBeDefined();
});
});
describe('with isFormVisible=true', () => {
beforeEach(() => {
vm = new RelatedIssuesBlock({
wrapper = mount(RelatedIssuesBlock, {
propsData: {
pathIdSeparator: PathIdSeparator.Issue,
isFormVisible: true,
issuableType: 'issue',
},
}).$mount();
});
});
it('shows add related issues form', () => {
expect(vm.$el.querySelector('.js-add-related-issues-form-area')).toBeDefined();
expect(wrapper.contains('.js-add-related-issues-form-area')).toBe(true);
});
});
describe('with relatedIssues', () => {
beforeEach(() => {
vm = new RelatedIssuesBlock({
wrapper = mount(RelatedIssuesBlock, {
propsData: {
pathIdSeparator: PathIdSeparator.Issue,
relatedIssues: [issuable1, issuable2],
issuableType: 'issue',
},
}).$mount();
});
});
it('should render issue tokens items', () => {
expect(vm.$el.querySelectorAll('.js-related-issues-token-list-item').length).toEqual(2);
});
});
describe('methods', () => {
beforeEach(() => {
vm = new RelatedIssuesBlock({
propsData: {
pathIdSeparator: PathIdSeparator.Issue,
relatedIssues: [issuable1, issuable2, issuable3, issuable4, issuable5],
issuableType: 'issue',
},
}).$mount();
});
it('reorder item correctly when an item is moved to the top', () => {
const beforeAfterIds = vm.getBeforeAfterId(vm.$el.querySelector('ul li:first-child'));
expect(beforeAfterIds.beforeId).toBeNull();
expect(beforeAfterIds.afterId).toBe(2);
});
it('reorder item correctly when an item is moved to the bottom', () => {
const beforeAfterIds = vm.getBeforeAfterId(vm.$el.querySelector('ul li:last-child'));
expect(beforeAfterIds.beforeId).toBe(4);
expect(beforeAfterIds.afterId).toBeNull();
});
it('reorder item correctly when an item is swapped with adjecent item', () => {
const beforeAfterIds = vm.getBeforeAfterId(vm.$el.querySelector('ul li:nth-child(3)'));
expect(beforeAfterIds.beforeId).toBe(2);
expect(beforeAfterIds.afterId).toBe(4);
});
it('reorder item correctly when an item is moved somewhere in the middle', () => {
const beforeAfterIds = vm.getBeforeAfterId(vm.$el.querySelector('ul li:nth-child(4)'));
expect(beforeAfterIds.beforeId).toBe(3);
expect(beforeAfterIds.afterId).toBe(5);
expect(wrapper.findAll('.js-related-issues-token-list-item').length).toEqual(2);
});
});
......@@ -166,84 +113,61 @@ describe('RelatedIssuesBlock', () => {
},
].forEach(({ issuableType, icon }) => {
it(`issuableType=${issuableType} is passed`, () => {
vm = new RelatedIssuesBlock({
wrapper = mount(RelatedIssuesBlock, {
propsData: {
pathIdSeparator: PathIdSeparator.Issue,
issuableType,
},
}).$mount();
const el = vm.$el.querySelector(`.issue-count-badge-count .ic-${icon}`);
});
expect(el).not.toBeNull();
expect(wrapper.contains(`.issue-count-badge-count .ic-${icon}`)).toBe(true);
});
});
});
describe('issuableOrderingId returns correct issuable order id when', () => {
it('issuableType is epic', () => {
vm = new RelatedIssuesBlock({
describe('with :issue_link_types feature flag on', () => {
beforeEach(() => {
wrapper = mount(RelatedIssuesBlock, {
propsData: {
pathIdSeparator: PathIdSeparator.Issue,
relatedIssues: [issuable1, issuable2, issuable3],
issuableType: 'issue',
},
}).$mount();
const orderId = vm.issuableOrderingId(issuable1);
expect(orderId).toBe(issuable1.epic_issue_id);
});
it('issuableType is issue', () => {
vm = new RelatedIssuesBlock({
propsData: {
pathIdSeparator: PathIdSeparator.Issue,
issuableType: 'epic',
provide: {
glFeatures: {
issueLinkTypes: true,
},
},
}).$mount();
const orderId = vm.issuableOrderingId(issuable1);
expect(orderId).toBe(issuable1.id);
});
});
});
describe('renders correct ordering id when', () => {
let relatedIssues;
beforeAll(() => {
relatedIssues = [issuable1, issuable2, issuable3, issuable4, issuable5];
it('displays "Linked issues" in the header', () => {
expect(wrapper.find('h3').text()).toContain('Linked issues');
});
it('issuableType is epic', () => {
vm = new RelatedIssuesBlock({
propsData: {
pathIdSeparator: PathIdSeparator.Issue,
issuableType: 'epic',
relatedIssues,
},
}).$mount();
describe('categorized headings', () => {
let categorizedHeadings;
beforeEach(() => {
categorizedHeadings = wrapper.findAll('h4');
});
const listItems = vm.$el.querySelectorAll('.list-item');
it('shows "Blocks" heading', () => {
const blocks = linkedIssueTypesTextMap[linkedIssueTypesMap.BLOCKS];
Array.from(listItems).forEach((item, index) => {
expect(Number(item.dataset.orderingId)).toBe(relatedIssues[index].id);
expect(categorizedHeadings.at(0).text()).toBe(blocks);
});
});
it('issuableType is issue', () => {
vm = new RelatedIssuesBlock({
propsData: {
pathIdSeparator: PathIdSeparator.Issue,
issuableType: 'issue',
relatedIssues,
},
}).$mount();
it('shows "Is blocked by" heading', () => {
const isBlockedBy = linkedIssueTypesTextMap[linkedIssueTypesMap.IS_BLOCKED_BY];
expect(categorizedHeadings.at(1).text()).toBe(isBlockedBy);
});
const listItems = vm.$el.querySelectorAll('.list-item');
it('shows "Relates to" heading', () => {
const relatesTo = linkedIssueTypesTextMap[linkedIssueTypesMap.RELATES_TO];
Array.from(listItems).forEach((item, index) => {
expect(Number(item.dataset.orderingId)).toBe(relatedIssues[index].epic_issue_id);
expect(categorizedHeadings.at(2).text()).toBe(relatesTo);
});
});
});
......
import { mount } from '@vue/test-utils';
import RelatedIssuesList from 'ee/related_issues/components/related_issues_list.vue';
import {
issuable1,
issuable2,
issuable3,
issuable4,
issuable5,
} from 'spec/vue_shared/components/issue/related_issuable_mock_data';
import { PathIdSeparator } from 'ee/related_issues/constants';
describe('RelatedIssuesList', () => {
let wrapper;
afterEach(() => {
wrapper.destroy();
});
describe('with defaults', () => {
beforeEach(() => {
wrapper = mount(RelatedIssuesList, {
propsData: {
pathIdSeparator: PathIdSeparator.Issue,
issuableType: 'issue',
},
});
});
it('should not show loading icon', () => {
expect(wrapper.vm.$refs.loadingIcon).toBeUndefined();
});
});
describe('with isFetching=true', () => {
beforeEach(() => {
wrapper = mount(RelatedIssuesList, {
propsData: {
pathIdSeparator: PathIdSeparator.Issue,
isFetching: true,
issuableType: 'issue',
},
});
});
it('should show loading icon', () => {
expect(wrapper.vm.$refs.loadingIcon).toBeDefined();
});
});
describe('methods', () => {
beforeEach(() => {
wrapper = mount(RelatedIssuesList, {
propsData: {
pathIdSeparator: PathIdSeparator.Issue,
relatedIssues: [issuable1, issuable2, issuable3, issuable4, issuable5],
issuableType: 'issue',
},
});
});
it('updates the order correctly when an item is moved to the top', () => {
const beforeAfterIds = wrapper.vm.getBeforeAfterId(
wrapper.vm.$el.querySelector('ul li:first-child'),
);
expect(beforeAfterIds.beforeId).toBeNull();
expect(beforeAfterIds.afterId).toBe(2);
});
it('updates the order correctly when an item is moved to the bottom', () => {
const beforeAfterIds = wrapper.vm.getBeforeAfterId(
wrapper.vm.$el.querySelector('ul li:last-child'),
);
expect(beforeAfterIds.beforeId).toBe(4);
expect(beforeAfterIds.afterId).toBeNull();
});
it('updates the order correctly when an item is swapped with adjacent item', () => {
const beforeAfterIds = wrapper.vm.getBeforeAfterId(
wrapper.vm.$el.querySelector('ul li:nth-child(3)'),
);
expect(beforeAfterIds.beforeId).toBe(2);
expect(beforeAfterIds.afterId).toBe(4);
});
it('updates the order correctly when an item is moved somewhere in the middle', () => {
const beforeAfterIds = wrapper.vm.getBeforeAfterId(
wrapper.vm.$el.querySelector('ul li:nth-child(4)'),
);
expect(beforeAfterIds.beforeId).toBe(3);
expect(beforeAfterIds.afterId).toBe(5);
});
});
describe('issuableOrderingId returns correct issuable order id when', () => {
it('issuableType is epic', () => {
wrapper = mount(RelatedIssuesList, {
propsData: {
pathIdSeparator: PathIdSeparator.Issue,
issuableType: 'issue',
},
});
expect(wrapper.vm.issuableOrderingId(issuable1)).toBe(issuable1.epicIssueId);
});
it('issuableType is issue', () => {
wrapper = mount(RelatedIssuesList, {
propsData: {
pathIdSeparator: PathIdSeparator.Issue,
issuableType: 'epic',
},
});
expect(wrapper.vm.issuableOrderingId(issuable1)).toBe(issuable1.id);
});
});
describe('renders correct ordering id when', () => {
let relatedIssues;
beforeAll(() => {
relatedIssues = [issuable1, issuable2, issuable3, issuable4, issuable5];
});
it('issuableType is epic', () => {
wrapper = mount(RelatedIssuesList, {
propsData: {
pathIdSeparator: PathIdSeparator.Issue,
issuableType: 'epic',
relatedIssues,
},
});
const listItems = wrapper.vm.$el.querySelectorAll('.list-item');
Array.from(listItems).forEach((item, index) => {
expect(Number(item.dataset.orderingId)).toBe(relatedIssues[index].id);
});
});
it('issuableType is issue', () => {
wrapper = mount(RelatedIssuesList, {
propsData: {
pathIdSeparator: PathIdSeparator.Issue,
issuableType: 'issue',
relatedIssues,
},
});
const listItems = wrapper.vm.$el.querySelectorAll('.list-item');
Array.from(listItems).forEach((item, index) => {
expect(Number(item.dataset.orderingId)).toBe(relatedIssues[index].epicIssueId);
});
});
});
describe('with :issue_link_types feature flag on', () => {
it('shows a heading', () => {
const heading = 'Related';
wrapper = mount(RelatedIssuesList, {
propsData: {
pathIdSeparator: PathIdSeparator.Issue,
issuableType: 'issue',
heading,
},
provide: {
glFeatures: {
issueLinkTypes: true,
},
},
});
expect(wrapper.find('h4').text()).toContain(heading);
});
});
});
......@@ -2,6 +2,7 @@ import Vue from 'vue';
import MockAdapter from 'axios-mock-adapter';
import relatedIssuesRoot from 'ee/related_issues/components/related_issues_root.vue';
import relatedIssuesService from 'ee/related_issues/services/related_issues_service';
import { linkedIssueTypesMap } from 'ee/related_issues/constants';
import {
defaultProps,
issuable1,
......@@ -123,16 +124,21 @@ describe('RelatedIssuesRoot', () => {
it('processes references before submitting', () => {
const input = '#123';
const linkedIssueType = linkedIssueTypesMap.RELATES_TO;
const emitObj = {
pendingReferences: input,
linkedIssueType,
};
vm.onPendingFormSubmit(input);
vm.onPendingFormSubmit(emitObj);
expect(vm.processAllReferences).toHaveBeenCalledWith(input);
expect(vm.service.addRelatedIssues).toHaveBeenCalledWith([input]);
expect(vm.service.addRelatedIssues).toHaveBeenCalledWith([input], linkedIssueType);
});
it('submit zero pending issue as related issue', done => {
vm.store.setPendingReferences([]);
vm.onPendingFormSubmit();
vm.onPendingFormSubmit({});
setTimeout(() => {
expect(vm.state.pendingReferences.length).toEqual(0);
......@@ -152,7 +158,7 @@ describe('RelatedIssuesRoot', () => {
});
vm.store.setPendingReferences([issuable1.reference]);
vm.onPendingFormSubmit();
vm.onPendingFormSubmit({});
setTimeout(() => {
expect(vm.state.pendingReferences.length).toEqual(0);
......@@ -173,7 +179,7 @@ describe('RelatedIssuesRoot', () => {
});
vm.store.setPendingReferences([issuable1.reference, issuable2.reference]);
vm.onPendingFormSubmit();
vm.onPendingFormSubmit({});
setTimeout(() => {
expect(vm.state.pendingReferences.length).toEqual(0);
......
......@@ -2690,6 +2690,9 @@ msgstr ""
msgid "Blocked"
msgstr ""
msgid "Blocks"
msgstr ""
msgid "Blog"
msgstr ""
......@@ -10137,6 +10140,9 @@ msgstr ""
msgid "Is"
msgstr ""
msgid "Is blocked by"
msgstr ""
msgid "Is not"
msgstr ""
......@@ -10941,6 +10947,9 @@ msgstr ""
msgid "Linked emails (%{email_count})"
msgstr ""
msgid "Linked issues"
msgstr ""
msgid "LinkedIn"
msgstr ""
......@@ -15104,6 +15113,9 @@ msgstr ""
msgid "Related merge requests"
msgstr ""
msgid "Relates to"
msgstr ""
msgid "Release"
msgid_plural "Releases"
msgstr[0] ""
......@@ -18180,6 +18192,9 @@ msgstr ""
msgid "The content of this page is not encoded in UTF-8. Edits can only be made via the Git repository."
msgstr ""
msgid "The current issue"
msgstr ""
msgid "The data source is connected, but there is no data to display. %{documentationLink}"
msgstr ""
......@@ -21504,6 +21519,9 @@ msgstr ""
msgid "authored"
msgstr ""
msgid "blocks"
msgstr ""
msgid "branch name"
msgstr ""
......@@ -21990,6 +22008,9 @@ msgstr ""
msgid "is an invalid IP address range"
msgstr ""
msgid "is blocked by"
msgstr ""
msgid "is enabled."
msgstr ""
......@@ -22465,6 +22486,9 @@ msgstr ""
msgid "register"
msgstr ""
msgid "relates to"
msgstr ""
msgid "released %{time}"
msgstr ""
......@@ -22581,6 +22605,9 @@ msgstr ""
msgid "tag name"
msgstr ""
msgid "the following issue(s)"
msgstr ""
msgid "this document"
msgstr ""
......
......@@ -19,9 +19,12 @@ module QA
end
view 'ee/app/assets/javascripts/related_issues/components/related_issues_block.vue' do
element :related_issues_plus_button
end
view 'ee/app/assets/javascripts/related_issues/components/related_issues_list.vue' do
element :related_issuable_item
element :related_issues_loading_icon
element :related_issues_plus_button
end
view 'ee/app/assets/javascripts/sidebar/components/weight/weight.vue' do
......
......@@ -6,40 +6,43 @@ export const defaultProps = {
export const issuable1 = {
id: 200,
epic_issue_id: 1,
epicIssueId: 1,
confidential: false,
reference: 'foo/bar#123',
displayReference: '#123',
title: 'some title',
path: '/foo/bar/issues/123',
state: 'opened',
linkType: 'relates_to',
};
export const issuable2 = {
id: 201,
epic_issue_id: 2,
epicIssueId: 2,
confidential: false,
reference: 'foo/bar#124',
displayReference: '#124',
title: 'some other thing',
path: '/foo/bar/issues/124',
state: 'opened',
linkType: 'blocks',
};
export const issuable3 = {
id: 202,
epic_issue_id: 3,
epicIssueId: 3,
confidential: false,
reference: 'foo/bar#125',
displayReference: '#125',
title: 'some other other thing',
path: '/foo/bar/issues/125',
state: 'opened',
linkType: 'is_blocked_by',
};
export const issuable4 = {
id: 203,
epic_issue_id: 4,
epicIssueId: 4,
confidential: false,
reference: 'foo/bar#126',
displayReference: '#126',
......@@ -50,7 +53,7 @@ export const issuable4 = {
export const issuable5 = {
id: 204,
epic_issue_id: 5,
epicIssueId: 5,
confidential: false,
reference: 'foo/bar#127',
displayReference: '#127',
......
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