Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
G
gitlab-ce
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
1
Merge Requests
1
Analytics
Analytics
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Commits
Issue Boards
Open sidebar
nexedi
gitlab-ce
Commits
6656d92a
Commit
6656d92a
authored
Jun 15, 2017
by
Jacob Schatz
Browse files
Options
Browse Files
Download
Plain Diff
Merge branch '2001-related-issues-fe-base' into 'master'
Related issues FE Base Closes #2001 See merge request !1797
parents
2a9a366c
f504fc77
Changes
34
Hide whitespace changes
Inline
Side-by-side
Showing
34 changed files
with
2429 additions
and
85 deletions
+2429
-85
app/assets/javascripts/issuable/issuable_bundle.js
app/assets/javascripts/issuable/issuable_bundle.js
+24
-0
app/assets/javascripts/issuable/related_issues/components/add_issuable_form.vue
.../issuable/related_issues/components/add_issuable_form.vue
+151
-0
app/assets/javascripts/issuable/related_issues/components/issue_token.vue
...cripts/issuable/related_issues/components/issue_token.vue
+141
-0
app/assets/javascripts/issuable/related_issues/components/related_issues_block.vue
...suable/related_issues/components/related_issues_block.vue
+176
-0
app/assets/javascripts/issuable/related_issues/components/related_issues_root.vue
...ssuable/related_issues/components/related_issues_root.vue
+215
-0
app/assets/javascripts/issuable/related_issues/event_hub.js
app/assets/javascripts/issuable/related_issues/event_hub.js
+3
-0
app/assets/javascripts/issuable/related_issues/services/related_issues_service.js
...ssuable/related_issues/services/related_issues_service.js
+27
-0
app/assets/javascripts/issuable/related_issues/stores/related_issues_store.js
...ts/issuable/related_issues/stores/related_issues_store.js
+30
-0
app/assets/stylesheets/framework/panels.scss
app/assets/stylesheets/framework/panels.scss
+49
-48
app/assets/stylesheets/framework/variables.scss
app/assets/stylesheets/framework/variables.scss
+0
-1
app/assets/stylesheets/pages/boards.scss
app/assets/stylesheets/pages/boards.scss
+2
-24
app/assets/stylesheets/pages/issuable.scss
app/assets/stylesheets/pages/issuable.scss
+45
-0
app/assets/stylesheets/pages/issues.scss
app/assets/stylesheets/pages/issues.scss
+118
-0
app/assets/stylesheets/pages/issues/issue_count_badge.scss
app/assets/stylesheets/pages/issues/issue_count_badge.scss
+29
-0
app/assets/stylesheets/pages/issues/related_issues.scss
app/assets/stylesheets/pages/issues/related_issues.scss
+41
-0
app/helpers/system_note_helper.rb
app/helpers/system_note_helper.rb
+3
-1
app/views/projects/boards/components/_board.html.haml
app/views/projects/boards/components/_board.html.haml
+3
-3
app/views/projects/issues/show.html.haml
app/views/projects/issues/show.html.haml
+8
-0
app/views/projects/merge_requests/_show.html.haml
app/views/projects/merge_requests/_show.html.haml
+1
-0
app/views/shared/icons/_icon_anchor.svg
app/views/shared/icons/_icon_anchor.svg
+1
-0
app/views/shared/icons/_icon_anchor_broken.svg
app/views/shared/icons/_icon_anchor_broken.svg
+1
-0
config/webpack.config.js
config/webpack.config.js
+2
-0
doc/README.md
doc/README.md
+1
-0
doc/user/project/issues/img/related_issues_add.png
doc/user/project/issues/img/related_issues_add.png
+0
-0
doc/user/project/issues/img/related_issues_remove.png
doc/user/project/issues/img/related_issues_remove.png
+0
-0
doc/user/project/issues/related_issues.md
doc/user/project/issues/related_issues.md
+33
-0
spec/features/boards/new_issue_spec.rb
spec/features/boards/new_issue_spec.rb
+8
-8
spec/features/issuables/related_issues_spec.rb
spec/features/issuables/related_issues_spec.rb
+215
-0
spec/javascripts/issuable/related_issues/components/add_issuable_form_spec.js
...uable/related_issues/components/add_issuable_form_spec.js
+165
-0
spec/javascripts/issuable/related_issues/components/issue_token_spec.js
...ts/issuable/related_issues/components/issue_token_spec.js
+204
-0
spec/javascripts/issuable/related_issues/components/related_issues_block_spec.js
...le/related_issues/components/related_issues_block_spec.js
+139
-0
spec/javascripts/issuable/related_issues/components/related_issues_root_spec.js
...ble/related_issues/components/related_issues_root_spec.js
+392
-0
spec/javascripts/issuable/related_issues/services/related_issues_service_spec.js
...le/related_issues/services/related_issues_service_spec.js
+109
-0
spec/javascripts/issuable/related_issues/stores/related_issues_store_spec.js
...suable/related_issues/stores/related_issues_store_spec.js
+93
-0
No files found.
app/assets/javascripts/issuable/issuable_bundle.js
0 → 100644
View file @
6656d92a
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
,
},
}),
});
}
});
app/assets/javascripts/issuable/related_issues/components/add_issuable_form.vue
0 → 100644
View file @
6656d92a
<
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
>
app/assets/javascripts/issuable/related_issues/components/issue_token.vue
0 → 100644
View file @
6656d92a
<
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
>
app/assets/javascripts/issuable/related_issues/components/related_issues_block.vue
0 → 100644
View file @
6656d92a
<
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
>
app/assets/javascripts/issuable/related_issues/components/related_issues_root.vue
0 → 100644
View file @
6656d92a
<
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
>
app/assets/javascripts/issuable/related_issues/event_hub.js
0 → 100644
View file @
6656d92a
import
Vue
from
'
vue
'
;
export
default
new
Vue
();
app/assets/javascripts/issuable/related_issues/services/related_issues_service.js
0 → 100644
View file @
6656d92a
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
;
app/assets/javascripts/issuable/related_issues/stores/related_issues_store.js
0 → 100644
View file @
6656d92a
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
;
app/assets/stylesheets/framework/panels.scss
View file @
6656d92a
@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
;
}
app/assets/stylesheets/framework/variables.scss
View file @
6656d92a
...
...
@@ -597,4 +597,3 @@ Convdev Index
$color-high-score
:
$green-400
;
$color-average-score
:
$orange-400
;
$color-low-score
:
$red-400
;
app/assets/stylesheets/pages/boards.scss
View file @
6656d92a
@import
"./issues/issue_count_badge"
;
[
v-cloak
]
{
display
:
none
;
}
...
...
@@ -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
;
...
...
app/assets/stylesheets/pages/issuable.scss
View file @
6656d92a
...
...
@@ -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
;
}
app/assets/stylesheets/pages/issues.scss
View file @
6656d92a
@import
"./issues/issue_count_badge"
;
@import
"./issues/related_issues"
;
.issues-list
{
.issue
{
padding
:
10px
0
10px
$gl-padding
;
...
...
@@ -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
;
}
}
app/assets/stylesheets/pages/issues/issue_count_badge.scss
0 → 100644
View file @
6656d92a
.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
;
}
app/assets/stylesheets/pages/issues/related_issues.scss
0 → 100644
View file @
6656d92a
$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
;
}
app/helpers/system_note_helper.rb
View file @
6656d92a
...
...
@@ -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
)
...
...
app/views/projects/boards/components/_board.html.haml
View file @
6656d92a
...
...
@@ -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-lef
t
{
":class"
=>
'
{
"has-btn"
:
list
.
type
!==
"closed"
&&
!
disabled
}
'
}
.
issue-count-badge
.pull-right.clearfix
{
"v-if"
=>
'list.type !== "blank"'
}
%span
.
issue-count-badge-coun
t
{
":class"
=>
'
{
"has-btn"
:
list
.
type
!==
"closed"
&&
!
disabled
}
'
}
{{ list.issuesSize }}
-
if
can?
(
current_user
,
:admin_issue
,
@project
)
%button
.
btn.btn-small.btn-default.pull-righ
t.has-tooltip
{
type:
"button"
,
%button
.
issue-count-badge-add-button.btn.btn-small.btn-defaul
t.has-tooltip
{
type:
"button"
,
"@click"
=>
"showNewIssueForm"
,
"v-if"
=>
'list.type !== "closed"'
,
"aria-label"
=>
"New issue"
,
...
...
app/views/projects/issues/show.html.haml
View file @
6656d92a
...
...
@@ -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
...
...
@@ -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.
...
...
app/views/projects/merge_requests/_show.html.haml
View file @
6656d92a
...
...
@@ -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"
...
...
app/views/shared/icons/_icon_anchor.svg
0 → 100644
View file @
6656d92a
<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>
app/views/shared/icons/_icon_anchor_broken.svg
0 → 100644
View file @
6656d92a
<svg
xmlns=
"http://www.w3.org/2000/svg"
viewBox=
"0 0 16 16"
><g
fill-rule=
"evenodd"
><path
fill-rule=
"nonzero"
d=
"M11.3 8.85c.2-.15.4-.3.6-.5l1.4-1.4c1.37-1.38 1.53-3.44.36-4.6-1.17-1.18-3.23-1.02-4.6.35l-1.4 1.4c-.2.2-.36.4-.5.6l1.66.66.04-.04 1.4-1.4c.6-.6 1.48-.66 1.98-.16s.44 1.38-.15 1.97l-1.44 1.4-.04.05.66 1.67zM8.85 11.3c-.15.2-.3.4-.5.6l-1.4 1.4c-1.38 1.37-3.44 1.53-4.6.36-1.18-1.17-1.02-3.23.35-4.6l1.4-1.4c.2-.2.4-.36.6-.5l.66 1.66-.04.04-1.4 1.4c-.6.6-.66 1.48-.16 1.98s1.38.44 1.97-.15l1.4-1.44.05-.04 1.67.66z"
/><path
d=
"M12.66 9.2h2c.27 0 .5.23.5.5v.06c0 .27-.23.5-.5.5h-2c-.28 0-.5-.23-.5-.5V9.7c0-.27.22-.5.5-.5zm-.4 2.12l1.43 1.42c.16.2.16.5 0 .7l-.07.04c-.2.2-.5.2-.7 0l-1.42-1.4c-.2-.2-.2-.53 0-.72l.05-.04c.2-.2.5-.2.7 0zm-2.8 1.1c0-.28.2-.5.5-.5H10c.28 0 .5.22.5.5v2c0 .27-.22.5-.5.5h-.05c-.28 0-.5-.23-.5-.5v-2zM6.7 3.24v-2c0-.27-.22-.5-.5-.5h-.05c-.27 0-.5.23-.5.5v2c0 .28.23.5.5.5h.05c.28 0 .5-.22.5-.5zm-2.1.4L3.16 2.2c-.2-.2-.5-.2-.7 0l-.04.04c-.2.2-.2.5 0 .7l1.4 1.42c.2.2.52.2.72 0l.04-.04c.18-.2.18-.5 0-.7zm-1.1 2.8c.27 0 .5-.2.5-.5V5.9c0-.27-.23-.5-.5-.5h-2c-.28 0-.5.23-.5.5v.06c0 .28.22.5.5.5h2z"
/></g></svg>
config/webpack.config.js
View file @
6656d92a
...
...
@@ -42,6 +42,7 @@ var config = {
graphs
:
'
./graphs/graphs_bundle.js
'
,
group
:
'
./group.js
'
,
groups_list
:
'
./groups_list.js
'
,
issuable
:
'
./issuable/issuable_bundle.js
'
,
issues
:
'
./issues/issues_bundle.js
'
,
issue_show
:
'
./issue_show/index.js
'
,
integrations
:
'
./integrations
'
,
...
...
@@ -159,6 +160,7 @@ var config = {
'
environments
'
,
'
environments_folder
'
,
'
filtered_search
'
,
'
issuable
'
,
'
issue_show
'
,
'
merge_conflicts
'
,
'
notebook_viewer
'
,
...
...
doc/README.md
View file @
6656d92a
...
...
@@ -85,6 +85,7 @@ Manage files and branches from the UI (user interface):
-
[
Cherry-pick
](
user/project/merge_requests/cherry_pick_changes.md
)
-
[
Milestones
](
user/project/milestones/index.md
)
: Organize issues and merge requests into a cohesive group, optionally setting a due date.
-
**(EES/EEP)**
[
Burndown Charts
](
user/project/milestones/index.md#burndown-charts
)
: Watch your project's progress throughout a specific milestone.
-
[
Related issues
](
user/project/issues/related_issues.md
)
-
[
Todos
](
workflow/todos.md
)
: A chronological list of to-dos that are waiting for your input, all in a simple dashboard.
### Git and GitLab
...
...
doc/user/project/issues/img/related_issues_add.png
0 → 100644
View file @
6656d92a
49.4 KB
doc/user/project/issues/img/related_issues_remove.png
0 → 100644
View file @
6656d92a
17.1 KB
doc/user/project/issues/related_issues.md
0 → 100644
View file @
6656d92a
# Related issues
> [Introduced][ee-1797] in [GitLab Enterprise Edition Starter][ee] 9.4.
Related issues are a bi-directional relationship between any two issues
and appear in a block below the issue description. Issues can be across groups
and projects.
The relationship only shows up in the UI if the user has write [permissions]
to see both issues (
`> Guest`
).
## Adding a related issue
You can relate one issue to the other by clicking the issue count badge "+" button
in the header of the related issue block. Then use the input that will appear
where you can type in the issue reference or paste in a link to an issue.
Valid references will be added to a temporary list that you can review.
When ready, click the green "Add related issues" button to submit.
![
Adding a related issue
](
img/related_issues_add.png
)
## Removing a related issue
In the related issues block, click the "x" icon on the right-side of every issue
token. Because of the bi-directional relationship, it will no longer appear in
either issue.
![
Removing a related issue
](
img/related_issues_remove.png
)
[
ee
]:
https://about.gitlab.com/gitlab-ee/
[
ee-1797
]:
https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/1797
[
permissions
]:
../../permissions.md
spec/features/boards/new_issue_spec.rb
View file @
6656d92a
...
...
@@ -19,18 +19,18 @@ describe 'Issue Boards new issue', feature: true, js: true do
end
it
'displays new issue button'
do
expect
(
page
).
to
have_selector
(
'.
board-issue-count-holder .bt
n'
,
count:
1
)
expect
(
page
).
to
have_selector
(
'.
issue-count-badge-add-butto
n'
,
count:
1
)
end
it
'does not display new issue button in closed list'
do
page
.
within
(
'.board:nth-child(2)'
)
do
expect
(
page
).
not_to
have_selector
(
'.
board-issue-count-holder .bt
n'
)
expect
(
page
).
not_to
have_selector
(
'.
issue-count-badge-add-butto
n'
)
end
end
it
'shows form when clicking button'
do
page
.
within
(
first
(
'.board'
))
do
find
(
'.
board-issue-count-holder .bt
n'
).
click
find
(
'.
issue-count-badge-add-butto
n'
).
click
expect
(
page
).
to
have_selector
(
'.board-new-issue-form'
)
end
...
...
@@ -38,7 +38,7 @@ describe 'Issue Boards new issue', feature: true, js: true do
it
'hides form when clicking cancel'
do
page
.
within
(
first
(
'.board'
))
do
find
(
'.
board-issue-count-holder .bt
n'
).
click
find
(
'.
issue-count-badge-add-butto
n'
).
click
expect
(
page
).
to
have_selector
(
'.board-new-issue-form'
)
...
...
@@ -50,7 +50,7 @@ describe 'Issue Boards new issue', feature: true, js: true do
it
'creates new issue'
do
page
.
within
(
first
(
'.board'
))
do
find
(
'.
board-issue-count-holder .bt
n'
).
click
find
(
'.
issue-count-badge-add-butto
n'
).
click
end
page
.
within
(
first
(
'.board-new-issue-form'
))
do
...
...
@@ -60,14 +60,14 @@ describe 'Issue Boards new issue', feature: true, js: true do
wait_for_requests
page
.
within
(
first
(
'.board .
board-issu
e-count'
))
do
page
.
within
(
first
(
'.board .
issue-count-badg
e-count'
))
do
expect
(
page
).
to
have_content
(
'1'
)
end
end
it
'shows sidebar when creating new issue'
do
page
.
within
(
first
(
'.board'
))
do
find
(
'.
board-issue-count-holder .bt
n'
).
click
find
(
'.
issue-count-badge-add-butto
n'
).
click
end
page
.
within
(
first
(
'.board-new-issue-form'
))
do
...
...
@@ -88,7 +88,7 @@ describe 'Issue Boards new issue', feature: true, js: true do
end
it
'does not display new issue button'
do
expect
(
page
).
to
have_selector
(
'.
board-issue-count-holder .bt
n'
,
count:
0
)
expect
(
page
).
to
have_selector
(
'.
issue-count-badge-add-butto
n'
,
count:
0
)
end
end
end
spec/features/issuables/related_issues_spec.rb
0 → 100644
View file @
6656d92a
require
'rails_helper'
describe
'Related issues'
,
feature:
true
,
js:
true
do
let
(
:project
)
{
create
(
:project_empty_repo
,
:public
)
}
let
(
:project_b
)
{
create
(
:project_empty_repo
,
:public
)
}
let
(
:project_unauthorized
)
{
create
(
:project_empty_repo
,
:public
)
}
let
(
:issue_a
)
{
create
(
:issue
,
project:
project
)
}
let
(
:issue_b
)
{
create
(
:issue
,
project:
project
)
}
let
(
:issue_c
)
{
create
(
:issue
,
project:
project
)
}
let
(
:issue_d
)
{
create
(
:issue
,
project:
project
)
}
let
(
:issue_project_b_a
)
{
create
(
:issue
,
project:
project_b
)
}
let
(
:issue_project_unauthorized_a
)
{
create
(
:issue
,
project:
project_unauthorized
)
}
let
(
:user
)
{
create
(
:user
)
}
context
'when user has no permission to update related issues'
do
before
do
login_as
(
user
)
end
context
'with related_issues enabled'
do
before
do
allow_any_instance_of
(
License
).
to
receive
(
:feature_available?
).
and_call_original
allow_any_instance_of
(
License
).
to
receive
(
:feature_available?
).
with
(
:related_issues
)
{
true
}
end
context
'with existing related issues'
do
let!
(
:issue_link_b
)
{
create
:issue_link
,
source:
issue_a
,
target:
issue_b
}
let!
(
:issue_link_c
)
{
create
:issue_link
,
source:
issue_a
,
target:
issue_c
}
context
'visiting issue_a'
do
before
do
visit
namespace_project_issue_path
(
project
.
namespace
,
project
,
issue_a
)
wait_for_requests
end
it
'shows related issues count'
do
expect
(
find
(
'.js-related-issues-header-issue-count'
)).
to
have_content
(
'2'
)
end
it
'does not show add related issue badge button'
do
expect
(
page
).
not_to
have_selector
(
'.js-issue-count-badge-add-button'
)
end
end
context
'visiting issue_b which was targeted by issue_a'
do
before
do
visit
namespace_project_issue_path
(
project
.
namespace
,
project
,
issue_b
)
wait_for_requests
end
it
'shows related issues count'
do
expect
(
find
(
'.js-related-issues-header-issue-count'
)).
to
have_content
(
'1'
)
end
end
end
end
end
context
'when user has permission to update related issues'
do
before
do
project
.
add_master
(
user
)
project_b
.
add_master
(
user
)
login_as
(
user
)
end
context
'with related_issues disabled'
do
let!
(
:issue_link_b
)
{
create
:issue_link
,
source:
issue_a
,
target:
issue_b
}
let!
(
:issue_link_c
)
{
create
:issue_link
,
source:
issue_a
,
target:
issue_c
}
before
do
visit
namespace_project_issue_path
(
project
.
namespace
,
project
,
issue_a
)
wait_for_requests
end
it
'does not show the related issues block'
do
expect
(
page
).
not_to
have_selector
(
'.js-related-issues-root'
)
end
end
context
'with related_issues enabled'
do
before
do
allow_any_instance_of
(
License
).
to
receive
(
:feature_available?
).
and_call_original
allow_any_instance_of
(
License
).
to
receive
(
:feature_available?
).
with
(
:related_issues
)
{
true
}
end
context
'without existing related issues'
do
before
do
visit
namespace_project_issue_path
(
project
.
namespace
,
project
,
issue_a
)
wait_for_requests
end
it
'shows related issues count'
do
expect
(
find
(
'.js-related-issues-header-issue-count'
)).
to
have_content
(
'0'
)
end
it
'shows add related issue badge button'
do
expect
(
page
).
to
have_selector
(
'.js-issue-count-badge-add-button'
)
end
it
'add related issue'
do
find
(
'.js-issue-count-badge-add-button'
).
click
find
(
'.js-add-issuable-form-input'
).
set
"
#{
issue_b
.
to_reference
(
project
)
}
"
find
(
'.js-add-issuable-form-add-button'
).
click
wait_for_requests
items
=
all
(
'.js-related-issues-token-list-item .js-issue-token-title'
)
# Form gets hidden after submission
expect
(
page
).
not_to
have_selector
(
'.js-add-related-issues-form-area'
)
# Check if related issues are present
expect
(
items
.
count
).
to
eq
(
1
)
expect
(
items
[
0
].
text
).
to
eq
(
issue_b
.
title
)
expect
(
find
(
'.js-related-issues-header-issue-count'
)).
to
have_content
(
'1'
)
end
it
'add cross-project related issue'
do
find
(
'.js-issue-count-badge-add-button'
).
click
find
(
'.js-add-issuable-form-input'
).
set
"
#{
issue_project_b_a
.
to_reference
(
project
)
}
"
find
(
'.js-add-issuable-form-add-button'
).
click
wait_for_requests
items
=
all
(
'.js-related-issues-token-list-item .js-issue-token-title'
)
expect
(
items
.
count
).
to
eq
(
1
)
expect
(
items
[
0
].
text
).
to
eq
(
issue_project_b_a
.
title
)
expect
(
find
(
'.js-related-issues-header-issue-count'
)).
to
have_content
(
'1'
)
end
end
context
'with existing related issues'
do
let!
(
:issue_link_b
)
{
create
:issue_link
,
source:
issue_a
,
target:
issue_b
}
let!
(
:issue_link_c
)
{
create
:issue_link
,
source:
issue_a
,
target:
issue_c
}
before
do
visit
namespace_project_issue_path
(
project
.
namespace
,
project
,
issue_a
)
wait_for_requests
end
it
'shows related issues count'
do
expect
(
find
(
'.js-related-issues-header-issue-count'
)).
to
have_content
(
'2'
)
end
it
'shows related issues'
do
items
=
all
(
'.js-related-issues-token-list-item .js-issue-token-title'
)
expect
(
items
.
count
).
to
eq
(
2
)
expect
(
items
[
0
].
text
).
to
eq
(
issue_b
.
title
)
expect
(
items
[
1
].
text
).
to
eq
(
issue_c
.
title
)
end
it
'allows us to remove a related issues'
do
items_before
=
all
(
'.js-related-issues-token-list-item .js-issue-token-title'
)
expect
(
items_before
.
count
).
to
eq
(
2
)
first
(
'.js-issue-token-remove-button'
).
click
wait_for_requests
items_after
=
all
(
'.js-related-issues-token-list-item .js-issue-token-title'
)
expect
(
items_after
.
count
).
to
eq
(
1
)
end
it
'add related issue'
do
find
(
'.js-issue-count-badge-add-button'
).
click
find
(
'.js-add-issuable-form-input'
).
set
"#
#{
issue_d
.
iid
}
"
find
(
'.js-add-issuable-form-add-button'
).
click
wait_for_requests
items
=
all
(
'.js-related-issues-token-list-item .js-issue-token-title'
)
expect
(
items
.
count
).
to
eq
(
3
)
expect
(
items
[
0
].
text
).
to
eq
(
issue_b
.
title
)
expect
(
items
[
1
].
text
).
to
eq
(
issue_c
.
title
)
expect
(
items
[
2
].
text
).
to
eq
(
issue_d
.
title
)
expect
(
find
(
'.js-related-issues-header-issue-count'
)).
to
have_content
(
'3'
)
end
it
'add invalid related issue'
do
find
(
'.js-issue-count-badge-add-button'
).
click
find
(
'.js-add-issuable-form-input'
).
set
"#9999999 "
find
(
'.js-add-issuable-form-add-button'
).
click
wait_for_requests
items
=
all
(
'.js-related-issues-token-list-item .js-issue-token-title'
)
expect
(
items
.
count
).
to
eq
(
2
)
expect
(
items
[
0
].
text
).
to
eq
(
issue_b
.
title
)
expect
(
items
[
1
].
text
).
to
eq
(
issue_c
.
title
)
expect
(
find
(
'.js-related-issues-header-issue-count'
)).
to
have_content
(
'2'
)
end
it
'add unauthorized related issue'
do
find
(
'.js-issue-count-badge-add-button'
).
click
find
(
'.js-add-issuable-form-input'
).
set
"
#{
issue_project_unauthorized_a
.
to_reference
(
project
)
}
"
find
(
'.js-add-issuable-form-add-button'
).
click
wait_for_requests
items
=
all
(
'.js-related-issues-token-list-item .js-issue-token-title'
)
expect
(
items
.
count
).
to
eq
(
2
)
expect
(
items
[
0
].
text
).
to
eq
(
issue_b
.
title
)
expect
(
items
[
1
].
text
).
to
eq
(
issue_c
.
title
)
expect
(
find
(
'.js-related-issues-header-issue-count'
)).
to
have_content
(
'2'
)
end
end
end
end
end
spec/javascripts/issuable/related_issues/components/add_issuable_form_spec.js
0 → 100644
View file @
6656d92a
import
Vue
from
'
vue
'
;
import
eventHub
from
'
~/issuable/related_issues/event_hub
'
;
import
addIssuableForm
from
'
~/issuable/related_issues/components/add_issuable_form.vue
'
;
const
issuable1
=
{
id
:
'
200
'
,
reference
:
'
foo/bar#123
'
,
displayReference
:
'
#123
'
,
title
:
'
some title
'
,
path
:
'
/foo/bar/issues/123
'
,
state
:
'
opened
'
,
};
const
issuable2
=
{
id
:
'
201
'
,
reference
:
'
foo/bar#124
'
,
displayReference
:
'
#124
'
,
title
:
'
some other thing
'
,
path
:
'
/foo/bar/issues/124
'
,
state
:
'
opened
'
,
};
describe
(
'
AddIssuableForm
'
,
()
=>
{
let
AddIssuableForm
;
let
vm
;
beforeEach
(()
=>
{
AddIssuableForm
=
Vue
.
extend
(
addIssuableForm
);
});
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
();
vm
.
$destroy
();
}
});
describe
(
'
with data
'
,
()
=>
{
const
inputValue
=
'
foo #123
'
;
const
addButtonLabel
=
'
Add issuable
'
;
beforeEach
(()
=>
{
vm
=
new
AddIssuableForm
({
propsData
:
{
inputValue
,
addButtonLabel
,
pendingReferences
:
[
issuable1
.
reference
,
issuable2
.
reference
,
],
},
}).
$mount
();
});
it
(
'
should put button label in place
'
,
()
=>
{
expect
(
vm
.
$refs
.
addButton
.
textContent
.
trim
()).
toEqual
(
addButtonLabel
);
});
it
(
'
should put input value in place
'
,
()
=>
{
expect
(
vm
.
$refs
.
input
.
value
).
toEqual
(
inputValue
);
});
it
(
'
should render pending issuables items
'
,
()
=>
{
expect
(
vm
.
$el
.
querySelectorAll
(
'
.js-add-issuable-form-token-list-item
'
).
length
).
toEqual
(
2
);
});
});
describe
(
'
methods
'
,
()
=>
{
let
addIssuableFormInputSpy
;
let
addIssuableFormBlurSpy
;
let
addIssuableFormSubmitSpy
;
let
addIssuableFormCancelSpy
;
beforeEach
(()
=>
{
addIssuableFormInputSpy
=
jasmine
.
createSpy
(
'
spy
'
);
addIssuableFormBlurSpy
=
jasmine
.
createSpy
(
'
spy
'
);
addIssuableFormSubmitSpy
=
jasmine
.
createSpy
(
'
spy
'
);
addIssuableFormCancelSpy
=
jasmine
.
createSpy
(
'
spy
'
);
eventHub
.
$on
(
'
addIssuableFormInput
'
,
addIssuableFormInputSpy
);
eventHub
.
$on
(
'
addIssuableFormBlur
'
,
addIssuableFormBlurSpy
);
eventHub
.
$on
(
'
addIssuableFormSubmit
'
,
addIssuableFormSubmitSpy
);
eventHub
.
$on
(
'
addIssuableFormCancel
'
,
addIssuableFormCancelSpy
);
const
el
=
document
.
createElement
(
'
div
'
);
// We need to append to body to get focus tests working
document
.
body
.
appendChild
(
el
);
vm
=
new
AddIssuableForm
({
propsData
:
{
inputValue
:
''
,
addButtonLabel
:
'
Add issuable
'
,
pendingIssuables
:
[
issuable1
,
],
},
}).
$mount
(
el
);
});
afterEach
(()
=>
{
eventHub
.
$off
(
'
addIssuableFormInput
'
,
addIssuableFormInputSpy
);
eventHub
.
$off
(
'
addIssuableFormBlur
'
,
addIssuableFormBlurSpy
);
eventHub
.
$off
(
'
addIssuableFormSubmit
'
,
addIssuableFormSubmitSpy
);
eventHub
.
$off
(
'
addIssuableFormCancel
'
,
addIssuableFormCancelSpy
);
});
it
(
'
when clicking somewhere on the input wrapper should focus the input
'
,
(
done
)
=>
{
vm
.
onInputWrapperClick
();
setTimeout
(()
=>
{
Vue
.
nextTick
(()
=>
{
expect
(
vm
.
$refs
.
issuableFormWrapper
.
classList
.
contains
(
'
focus
'
)).
toEqual
(
true
);
expect
(
document
.
activeElement
).
toEqual
(
vm
.
$refs
.
input
);
done
();
});
});
});
it
(
'
when filling in the input
'
,
()
=>
{
expect
(
addIssuableFormInputSpy
).
not
.
toHaveBeenCalled
();
const
newInputValue
=
'
filling in things
'
;
vm
.
$refs
.
input
.
value
=
newInputValue
;
vm
.
onInput
();
expect
(
addIssuableFormInputSpy
).
toHaveBeenCalledWith
(
newInputValue
,
newInputValue
.
length
);
});
it
(
'
when blurring the input
'
,
(
done
)
=>
{
expect
(
addIssuableFormInputSpy
).
not
.
toHaveBeenCalled
();
const
newInputValue
=
'
filling in things
'
;
vm
.
$refs
.
input
.
value
=
newInputValue
;
vm
.
onBlur
();
setTimeout
(()
=>
{
Vue
.
nextTick
(()
=>
{
expect
(
vm
.
$refs
.
issuableFormWrapper
.
classList
.
contains
(
'
focus
'
)).
toEqual
(
false
);
expect
(
addIssuableFormBlurSpy
).
toHaveBeenCalledWith
(
newInputValue
);
done
();
});
});
});
it
(
'
when submitting pending issues
'
,
()
=>
{
expect
(
addIssuableFormSubmitSpy
).
not
.
toHaveBeenCalled
();
vm
.
onFormSubmit
();
expect
(
addIssuableFormSubmitSpy
).
toHaveBeenCalled
();
});
it
(
'
when canceling form to collapse
'
,
()
=>
{
expect
(
addIssuableFormCancelSpy
).
not
.
toHaveBeenCalled
();
vm
.
onFormCancel
();
expect
(
addIssuableFormCancelSpy
).
toHaveBeenCalled
();
});
});
});
spec/javascripts/issuable/related_issues/components/issue_token_spec.js
0 → 100644
View file @
6656d92a
import
Vue
from
'
vue
'
;
import
eventHub
from
'
~/issuable/related_issues/event_hub
'
;
import
issueToken
from
'
~/issuable/related_issues/components/issue_token.vue
'
;
describe
(
'
IssueToken
'
,
()
=>
{
const
idKey
=
200
;
const
displayReference
=
'
foo/bar#123
'
;
const
title
=
'
some title
'
;
let
IssueToken
;
let
vm
;
beforeEach
(()
=>
{
IssueToken
=
Vue
.
extend
(
issueToken
);
});
afterEach
(()
=>
{
if
(
vm
)
{
vm
.
$destroy
();
}
});
describe
(
'
with reference supplied
'
,
()
=>
{
beforeEach
(()
=>
{
vm
=
new
IssueToken
({
propsData
:
{
idKey
,
displayReference
,
},
}).
$mount
();
});
it
(
'
shows reference
'
,
()
=>
{
expect
(
vm
.
$el
.
textContent
.
trim
()).
toEqual
(
displayReference
);
});
it
(
'
does not link without path specified
'
,
()
=>
{
expect
(
vm
.
$refs
.
link
.
tagName
.
toLowerCase
()).
toEqual
(
'
span
'
);
expect
(
vm
.
$refs
.
link
.
getAttribute
(
'
href
'
)).
toBeNull
();
});
});
describe
(
'
with reference and title supplied
'
,
()
=>
{
beforeEach
(()
=>
{
vm
=
new
IssueToken
({
propsData
:
{
idKey
,
displayReference
,
title
,
},
}).
$mount
();
});
it
(
'
shows reference and title
'
,
()
=>
{
expect
(
vm
.
$refs
.
reference
.
textContent
.
trim
()).
toEqual
(
displayReference
);
expect
(
vm
.
$refs
.
title
.
textContent
.
trim
()).
toEqual
(
title
);
});
});
describe
(
'
with path supplied
'
,
()
=>
{
const
path
=
'
/foo/bar/issues/123
'
;
beforeEach
(()
=>
{
vm
=
new
IssueToken
({
propsData
:
{
idKey
,
displayReference
,
title
,
path
,
},
}).
$mount
();
});
it
(
'
links reference and title
'
,
()
=>
{
expect
(
vm
.
$refs
.
link
.
getAttribute
(
'
href
'
)).
toEqual
(
path
);
});
});
describe
(
'
with state supplied
'
,
()
=>
{
describe
(
'
`state:
\'
opened
\'
`
'
,
()
=>
{
beforeEach
(()
=>
{
vm
=
new
IssueToken
({
propsData
:
{
idKey
,
displayReference
,
state
:
'
opened
'
,
},
}).
$mount
();
});
it
(
'
shows green circle icon
'
,
()
=>
{
expect
(
vm
.
$el
.
querySelector
(
'
.issue-token-state-icon-open.fa.fa-circle-o
'
)).
toBeDefined
();
});
});
describe
(
'
`state:
\'
reopened
\'
`
'
,
()
=>
{
beforeEach
(()
=>
{
vm
=
new
IssueToken
({
propsData
:
{
idKey
,
displayReference
,
state
:
'
reopened
'
,
},
}).
$mount
();
});
it
(
'
shows green circle icon
'
,
()
=>
{
expect
(
vm
.
$el
.
querySelector
(
'
.issue-token-state-icon-open.fa.fa-circle-o
'
)).
toBeDefined
();
});
});
describe
(
'
`state:
\'
closed
\'
`
'
,
()
=>
{
beforeEach
(()
=>
{
vm
=
new
IssueToken
({
propsData
:
{
idKey
,
displayReference
,
state
:
'
closed
'
,
},
}).
$mount
();
});
it
(
'
shows red minus icon
'
,
()
=>
{
expect
(
vm
.
$el
.
querySelector
(
'
.issue-token-state-icon-closed.fa.fa-minus
'
)).
toBeDefined
();
});
});
});
describe
(
'
with reference, title, state
'
,
()
=>
{
const
state
=
'
opened
'
;
beforeEach
(()
=>
{
vm
=
new
IssueToken
({
propsData
:
{
idKey
,
displayReference
,
title
,
state
,
},
}).
$mount
();
});
it
(
'
shows reference, title, and state
'
,
()
=>
{
expect
(
vm
.
$refs
.
stateIcon
.
getAttribute
(
'
aria-label
'
)).
toEqual
(
state
);
expect
(
vm
.
$refs
.
reference
.
textContent
.
trim
()).
toEqual
(
displayReference
);
expect
(
vm
.
$refs
.
title
.
textContent
.
trim
()).
toEqual
(
title
);
});
});
describe
(
'
with canRemove
'
,
()
=>
{
describe
(
'
`canRemove: false` (default)
'
,
()
=>
{
beforeEach
(()
=>
{
vm
=
new
IssueToken
({
propsData
:
{
idKey
,
displayReference
,
},
}).
$mount
();
});
it
(
'
does not have remove button
'
,
()
=>
{
expect
(
vm
.
$el
.
querySelector
(
'
.issue-token-remove-button
'
)).
toBeNull
();
});
});
describe
(
'
`canRemove: true`
'
,
()
=>
{
beforeEach
(()
=>
{
vm
=
new
IssueToken
({
propsData
:
{
idKey
,
displayReference
,
canRemove
:
true
,
},
}).
$mount
();
});
it
(
'
has remove button
'
,
()
=>
{
expect
(
vm
.
$el
.
querySelector
(
'
.issue-token-remove-button
'
)).
toBeDefined
();
});
});
});
describe
(
'
methods
'
,
()
=>
{
let
removeRequestSpy
;
beforeEach
(()
=>
{
vm
=
new
IssueToken
({
propsData
:
{
idKey
,
displayReference
,
},
}).
$mount
();
removeRequestSpy
=
jasmine
.
createSpy
(
'
spy
'
);
eventHub
.
$on
(
'
removeRequest
'
,
removeRequestSpy
);
});
afterEach
(()
=>
{
eventHub
.
$off
(
'
removeRequest
'
,
removeRequestSpy
);
});
it
(
'
when getting checked
'
,
()
=>
{
expect
(
removeRequestSpy
).
not
.
toHaveBeenCalled
();
vm
.
onRemoveRequest
();
expect
(
removeRequestSpy
).
toHaveBeenCalled
();
});
});
});
spec/javascripts/issuable/related_issues/components/related_issues_block_spec.js
0 → 100644
View file @
6656d92a
import
Vue
from
'
vue
'
;
import
eventHub
from
'
~/issuable/related_issues/event_hub
'
;
import
relatedIssuesBlock
from
'
~/issuable/related_issues/components/related_issues_block.vue
'
;
const
issuable1
=
{
id
:
'
200
'
,
reference
:
'
foo/bar#123
'
,
displayReference
:
'
#123
'
,
title
:
'
some title
'
,
path
:
'
/foo/bar/issues/123
'
,
state
:
'
opened
'
,
};
const
issuable2
=
{
id
:
'
201
'
,
reference
:
'
foo/bar#124
'
,
displayReference
:
'
#124
'
,
title
:
'
some other thing
'
,
path
:
'
/foo/bar/issues/124
'
,
state
:
'
opened
'
,
};
describe
(
'
RelatedIssuesBlock
'
,
()
=>
{
let
RelatedIssuesBlock
;
let
vm
;
beforeEach
(()
=>
{
RelatedIssuesBlock
=
Vue
.
extend
(
relatedIssuesBlock
);
});
afterEach
(()
=>
{
if
(
vm
)
{
vm
.
$destroy
();
}
});
describe
(
'
with defaults
'
,
()
=>
{
beforeEach
(()
=>
{
vm
=
new
RelatedIssuesBlock
().
$mount
();
});
it
(
'
unable to add new related issues
'
,
()
=>
{
expect
(
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
();
});
});
describe
(
'
with isFetching=true
'
,
()
=>
{
beforeEach
(()
=>
{
vm
=
new
RelatedIssuesBlock
({
propsData
:
{
isFetching
:
true
,
},
}).
$mount
();
});
it
(
'
should show loading icon
'
,
()
=>
{
expect
(
vm
.
$refs
.
loadingIcon
).
toBeDefined
();
});
});
describe
(
'
with canAddRelatedIssues=true
'
,
()
=>
{
beforeEach
(()
=>
{
vm
=
new
RelatedIssuesBlock
({
propsData
:
{
canAddRelatedIssues
:
true
,
},
}).
$mount
();
});
it
(
'
can add new related issues
'
,
()
=>
{
expect
(
vm
.
$refs
.
issueCountBadgeAddButton
).
toBeDefined
();
});
});
describe
(
'
with isFormVisible=true
'
,
()
=>
{
beforeEach
(()
=>
{
vm
=
new
RelatedIssuesBlock
({
propsData
:
{
isFormVisible
:
true
,
},
}).
$mount
();
});
it
(
'
shows add related issues form
'
,
()
=>
{
expect
(
vm
.
$el
.
querySelector
(
'
.js-add-related-issues-form-area
'
)).
toBeDefined
();
});
});
describe
(
'
with relatedIssues
'
,
()
=>
{
beforeEach
(()
=>
{
vm
=
new
RelatedIssuesBlock
({
propsData
:
{
relatedIssues
:
[
issuable1
,
issuable2
,
],
},
}).
$mount
();
});
it
(
'
should render issue tokens items
'
,
()
=>
{
expect
(
vm
.
$el
.
querySelectorAll
(
'
.js-related-issues-token-list-item
'
).
length
).
toEqual
(
2
);
});
});
describe
(
'
methods
'
,
()
=>
{
let
toggleAddRelatedIssuesFormSpy
;
beforeEach
(()
=>
{
vm
=
new
RelatedIssuesBlock
({
propsData
:
{
relatedIssues
:
[
issuable1
,
],
},
}).
$mount
();
toggleAddRelatedIssuesFormSpy
=
jasmine
.
createSpy
(
'
spy
'
);
eventHub
.
$on
(
'
toggleAddRelatedIssuesForm
'
,
toggleAddRelatedIssuesFormSpy
);
});
afterEach
(()
=>
{
eventHub
.
$off
(
'
toggleAddRelatedIssuesForm
'
,
toggleAddRelatedIssuesFormSpy
);
});
it
(
'
when expanding add related issue form
'
,
()
=>
{
expect
(
toggleAddRelatedIssuesFormSpy
).
not
.
toHaveBeenCalled
();
vm
.
toggleAddRelatedIssuesForm
();
expect
(
toggleAddRelatedIssuesFormSpy
).
toHaveBeenCalled
();
});
});
});
spec/javascripts/issuable/related_issues/components/related_issues_root_spec.js
0 → 100644
View file @
6656d92a
import
Vue
from
'
vue
'
;
import
relatedIssuesRoot
from
'
~/issuable/related_issues/components/related_issues_root.vue
'
;
const
defaultProps
=
{
endpoint
:
'
/foo/bar/issues/1/related_issues
'
,
currentNamespacePath
:
'
foo
'
,
currentProjectPath
:
'
bar
'
,
};
const
issuable1
=
{
id
:
'
200
'
,
reference
:
'
foo/bar#123
'
,
title
:
'
issue1
'
,
path
:
'
/foo/bar/issues/123
'
,
state
:
'
opened
'
,
destroy_relation_path
:
'
/foo/bar/issues/123/related_issues/1
'
,
};
const
issuable2
=
{
id
:
'
201
'
,
reference
:
'
foo/bar#124
'
,
title
:
'
issue1
'
,
path
:
'
/foo/bar/issues/124
'
,
state
:
'
opened
'
,
destroy_relation_path
:
'
/foo/bar/issues/124/related_issues/1
'
,
};
describe
(
'
RelatedIssuesRoot
'
,
()
=>
{
let
RelatedIssuesRoot
;
let
vm
;
beforeEach
(()
=>
{
RelatedIssuesRoot
=
Vue
.
extend
(
relatedIssuesRoot
);
});
afterEach
(()
=>
{
if
(
vm
)
{
vm
.
$destroy
();
}
});
describe
(
'
methods
'
,
()
=>
{
describe
(
'
onRelatedIssueRemoveRequest
'
,
()
=>
{
beforeEach
(()
=>
{
vm
=
new
RelatedIssuesRoot
({
propsData
:
defaultProps
,
}).
$mount
();
vm
.
store
.
setRelatedIssues
([
issuable1
]);
});
it
(
'
remove related issue and succeeds
'
,
(
done
)
=>
{
const
interceptor
=
(
request
,
next
)
=>
{
next
(
request
.
respondWith
(
JSON
.
stringify
({
issues
:
[],
}),
{
status
:
200
,
}));
};
Vue
.
http
.
interceptors
.
push
(
interceptor
);
vm
.
onRelatedIssueRemoveRequest
(
issuable1
.
id
);
setTimeout
(()
=>
{
expect
(
vm
.
state
.
relatedIssues
).
toEqual
([]);
Vue
.
http
.
interceptors
=
_
.
without
(
Vue
.
http
.
interceptors
,
interceptor
);
done
();
});
});
it
(
'
remove related issue, fails, and restores to related issues
'
,
(
done
)
=>
{
const
interceptor
=
(
request
,
next
)
=>
{
next
(
request
.
respondWith
(
JSON
.
stringify
({}),
{
status
:
422
,
}));
};
Vue
.
http
.
interceptors
.
push
(
interceptor
);
vm
.
onRelatedIssueRemoveRequest
(
issuable1
.
id
);
setTimeout
(()
=>
{
expect
(
vm
.
state
.
relatedIssues
.
length
).
toEqual
(
1
);
expect
(
vm
.
state
.
relatedIssues
[
0
].
id
).
toEqual
(
issuable1
.
id
);
Vue
.
http
.
interceptors
=
_
.
without
(
Vue
.
http
.
interceptors
,
interceptor
);
done
();
});
});
});
describe
(
'
onToggleAddRelatedIssuesForm
'
,
()
=>
{
beforeEach
(()
=>
{
vm
=
new
RelatedIssuesRoot
({
propsData
:
defaultProps
,
}).
$mount
();
});
it
(
'
toggle related issues form to visible
'
,
()
=>
{
vm
.
onToggleAddRelatedIssuesForm
();
expect
(
vm
.
isFormVisible
).
toEqual
(
true
);
});
it
(
'
show add related issues form to hidden
'
,
()
=>
{
vm
.
isFormVisible
=
true
;
vm
.
onToggleAddRelatedIssuesForm
();
expect
(
vm
.
isFormVisible
).
toEqual
(
false
);
});
});
describe
(
'
onPendingIssueRemoveRequest
'
,
()
=>
{
beforeEach
(()
=>
{
vm
=
new
RelatedIssuesRoot
({
propsData
:
defaultProps
,
}).
$mount
();
vm
.
store
.
setPendingReferences
([
issuable1
.
reference
]);
});
it
(
'
remove pending related issue
'
,
()
=>
{
expect
(
vm
.
state
.
pendingReferences
.
length
).
toEqual
(
1
);
vm
.
onPendingIssueRemoveRequest
(
0
);
expect
(
vm
.
state
.
pendingReferences
.
length
).
toEqual
(
0
);
});
});
describe
(
'
onPendingFormSubmit
'
,
()
=>
{
beforeEach
(()
=>
{
vm
=
new
RelatedIssuesRoot
({
propsData
:
defaultProps
,
}).
$mount
();
});
it
(
'
submit zero pending issue as related issue
'
,
(
done
)
=>
{
vm
.
store
.
setPendingReferences
([]);
vm
.
onPendingFormSubmit
();
setTimeout
(()
=>
{
Vue
.
nextTick
(()
=>
{
expect
(
vm
.
state
.
pendingReferences
.
length
).
toEqual
(
0
);
expect
(
vm
.
state
.
relatedIssues
.
length
).
toEqual
(
0
);
done
();
});
});
});
it
(
'
submit pending issue as related issue
'
,
(
done
)
=>
{
const
interceptor
=
(
request
,
next
)
=>
{
next
(
request
.
respondWith
(
JSON
.
stringify
({
issues
:
[
issuable1
],
result
:
{
message
:
'
something was successfully related
'
,
status
:
'
success
'
,
},
}),
{
status
:
200
,
}));
};
Vue
.
http
.
interceptors
.
push
(
interceptor
);
vm
.
store
.
setPendingReferences
([
issuable1
.
reference
]);
vm
.
onPendingFormSubmit
();
setTimeout
(()
=>
{
Vue
.
nextTick
(()
=>
{
expect
(
vm
.
state
.
pendingReferences
.
length
).
toEqual
(
0
);
expect
(
vm
.
state
.
relatedIssues
.
length
).
toEqual
(
1
);
expect
(
vm
.
state
.
relatedIssues
[
0
].
id
).
toEqual
(
issuable1
.
id
);
Vue
.
http
.
interceptors
=
_
.
without
(
Vue
.
http
.
interceptors
,
interceptor
);
done
();
});
});
});
it
(
'
submit multiple pending issues as related issues
'
,
(
done
)
=>
{
const
interceptor
=
(
request
,
next
)
=>
{
next
(
request
.
respondWith
(
JSON
.
stringify
({
issues
:
[
issuable1
,
issuable2
],
result
:
{
message
:
'
something was successfully related
'
,
status
:
'
success
'
,
},
}),
{
status
:
200
,
}));
};
Vue
.
http
.
interceptors
.
push
(
interceptor
);
vm
.
store
.
setPendingReferences
([
issuable1
.
reference
,
issuable2
.
reference
]);
vm
.
onPendingFormSubmit
();
setTimeout
(()
=>
{
Vue
.
nextTick
(()
=>
{
expect
(
vm
.
state
.
pendingReferences
.
length
).
toEqual
(
0
);
expect
(
vm
.
state
.
relatedIssues
.
length
).
toEqual
(
2
);
expect
(
vm
.
state
.
relatedIssues
[
0
].
id
).
toEqual
(
issuable1
.
id
);
expect
(
vm
.
state
.
relatedIssues
[
1
].
id
).
toEqual
(
issuable2
.
id
);
Vue
.
http
.
interceptors
=
_
.
without
(
Vue
.
http
.
interceptors
,
interceptor
);
done
();
});
});
});
});
describe
(
'
onPendingFormCancel
'
,
()
=>
{
beforeEach
(()
=>
{
vm
=
new
RelatedIssuesRoot
({
propsData
:
defaultProps
,
}).
$mount
();
vm
.
isFormVisible
=
true
;
vm
.
inputValue
=
'
foo
'
;
});
it
(
'
when canceling and hiding add issuable form
'
,
()
=>
{
vm
.
onPendingFormCancel
();
expect
(
vm
.
isFormVisible
).
toEqual
(
false
);
expect
(
vm
.
inputValue
).
toEqual
(
''
);
expect
(
vm
.
state
.
pendingReferences
.
length
).
toEqual
(
0
);
});
});
describe
(
'
fetchRelatedIssues
'
,
()
=>
{
beforeEach
(()
=>
{
vm
=
new
RelatedIssuesRoot
({
propsData
:
defaultProps
,
}).
$mount
();
});
describe
(
'
when the network has not responded yet
'
,
()
=>
{
it
(
'
should be fetching
'
,
(
done
)
=>
{
vm
.
fetchRelatedIssues
();
setTimeout
(()
=>
{
Vue
.
nextTick
(()
=>
{
expect
(
vm
.
isFetching
).
toEqual
(
true
);
done
();
});
});
});
});
describe
(
'
when the network responds
'
,
()
=>
{
const
interceptor
=
(
request
,
next
)
=>
{
next
(
request
.
respondWith
(
JSON
.
stringify
([
issuable1
,
issuable2
]),
{
status
:
200
,
}));
};
beforeEach
(()
=>
{
Vue
.
http
.
interceptors
.
push
(
interceptor
);
});
afterEach
(()
=>
{
Vue
.
http
.
interceptors
=
_
.
without
(
Vue
.
http
.
interceptors
,
interceptor
);
});
it
(
'
should be done fetching
'
,
(
done
)
=>
{
vm
.
fetchRelatedIssues
();
setTimeout
(()
=>
{
Vue
.
nextTick
(()
=>
{
expect
(
vm
.
isFetching
).
toEqual
(
false
);
done
();
});
});
});
it
(
'
should fetch related issues
'
,
(
done
)
=>
{
vm
.
fetchRelatedIssues
();
setTimeout
(()
=>
{
Vue
.
nextTick
(()
=>
{
expect
(
vm
.
state
.
relatedIssues
.
length
).
toEqual
(
2
);
expect
(
vm
.
state
.
relatedIssues
[
0
].
id
).
toEqual
(
issuable1
.
id
);
expect
(
vm
.
state
.
relatedIssues
[
1
].
id
).
toEqual
(
issuable2
.
id
);
done
();
});
});
});
});
});
describe
(
'
onInput
'
,
()
=>
{
beforeEach
(()
=>
{
vm
=
new
RelatedIssuesRoot
({
propsData
:
defaultProps
,
}).
$mount
();
});
it
(
'
fill in issue number reference and adds to pending related issues
'
,
()
=>
{
const
input
=
'
#123
'
;
vm
.
onInput
(
input
,
input
.
length
);
expect
(
vm
.
state
.
pendingReferences
.
length
).
toEqual
(
1
);
expect
(
vm
.
state
.
pendingReferences
[
0
]).
toEqual
(
'
#123
'
);
});
it
(
'
fill in with full reference
'
,
()
=>
{
const
input
=
'
asdf/qwer#444
'
;
vm
.
onInput
(
input
,
input
.
length
);
expect
(
vm
.
state
.
pendingReferences
.
length
).
toEqual
(
1
);
expect
(
vm
.
state
.
pendingReferences
[
0
]).
toEqual
(
'
asdf/qwer#444
'
);
});
it
(
'
fill in with issue link
'
,
()
=>
{
const
link
=
'
http://localhost:3000/foo/bar/issues/111
'
;
const
input
=
`
${
link
}
`
;
vm
.
onInput
(
input
,
input
.
length
);
expect
(
vm
.
state
.
pendingReferences
.
length
).
toEqual
(
1
);
expect
(
vm
.
state
.
pendingReferences
[
0
]).
toEqual
(
link
);
});
it
(
'
fill in with multiple references
'
,
()
=>
{
const
input
=
'
asdf/qwer#444 #12
'
;
vm
.
onInput
(
input
,
input
.
length
);
expect
(
vm
.
state
.
pendingReferences
.
length
).
toEqual
(
2
);
expect
(
vm
.
state
.
pendingReferences
[
0
]).
toEqual
(
'
asdf/qwer#444
'
);
expect
(
vm
.
state
.
pendingReferences
[
1
]).
toEqual
(
'
#12
'
);
});
it
(
'
fill in with some invalid things
'
,
()
=>
{
const
input
=
'
something random
'
;
vm
.
onInput
(
input
,
input
.
length
);
expect
(
vm
.
state
.
pendingReferences
.
length
).
toEqual
(
2
);
expect
(
vm
.
state
.
pendingReferences
[
0
]).
toEqual
(
'
something
'
);
expect
(
vm
.
state
.
pendingReferences
[
1
]).
toEqual
(
'
random
'
);
});
it
(
'
fill in invalid and some legit references
'
,
()
=>
{
const
input
=
'
something random #123
'
;
vm
.
onInput
(
input
,
input
.
length
);
expect
(
vm
.
state
.
pendingReferences
.
length
).
toEqual
(
3
);
expect
(
vm
.
state
.
pendingReferences
[
0
]).
toEqual
(
'
something
'
);
expect
(
vm
.
state
.
pendingReferences
[
1
]).
toEqual
(
'
random
'
);
expect
(
vm
.
state
.
pendingReferences
[
2
]).
toEqual
(
'
#123
'
);
});
it
(
'
keep reference piece in input while we are touching it
'
,
()
=>
{
const
input
=
'
a #123 b
'
;
vm
.
onInput
(
input
,
3
);
expect
(
vm
.
state
.
pendingReferences
.
length
).
toEqual
(
2
);
expect
(
vm
.
state
.
pendingReferences
[
0
]).
toEqual
(
'
a
'
);
expect
(
vm
.
state
.
pendingReferences
[
1
]).
toEqual
(
'
b
'
);
});
});
describe
(
'
onBlur
'
,
()
=>
{
beforeEach
(()
=>
{
vm
=
new
RelatedIssuesRoot
({
propsData
:
defaultProps
,
}).
$mount
();
});
it
(
'
add valid reference to pending when blurring
'
,
()
=>
{
const
input
=
'
#123
'
;
vm
.
onBlur
(
input
);
expect
(
vm
.
state
.
pendingReferences
.
length
).
toEqual
(
1
);
expect
(
vm
.
state
.
pendingReferences
[
0
]).
toEqual
(
'
#123
'
);
});
it
(
'
add any valid references to pending when blurring
'
,
()
=>
{
const
input
=
'
asdf #123
'
;
vm
.
onBlur
(
input
);
expect
(
vm
.
state
.
pendingReferences
.
length
).
toEqual
(
2
);
expect
(
vm
.
state
.
pendingReferences
[
0
]).
toEqual
(
'
asdf
'
);
expect
(
vm
.
state
.
pendingReferences
[
1
]).
toEqual
(
'
#123
'
);
});
});
});
});
spec/javascripts/issuable/related_issues/services/related_issues_service_spec.js
0 → 100644
View file @
6656d92a
import
_
from
'
underscore
'
;
import
Vue
from
'
vue
'
;
import
RelatedIssuesService
from
'
~/issuable/related_issues/services/related_issues_service
'
;
const
issuable1
=
{
reference
:
'
foo/bar#123
'
,
title
:
'
some title
'
,
path
:
'
/foo/bar/issues/123
'
,
state
:
'
opened
'
,
destroy_relation_path
:
'
/foo/bar/issues/123/related_issues/1
'
,
};
describe
(
'
RelatedIssuesService
'
,
()
=>
{
let
service
;
beforeEach
(()
=>
{
service
=
new
RelatedIssuesService
(
''
);
});
describe
(
'
fetchRelatedIssues
'
,
()
=>
{
const
interceptor
=
(
request
,
next
)
=>
{
next
(
request
.
respondWith
(
JSON
.
stringify
([
issuable1
]),
{
status
:
200
,
}));
};
beforeEach
(()
=>
{
Vue
.
http
.
interceptors
.
push
(
interceptor
);
});
afterEach
(()
=>
{
Vue
.
http
.
interceptors
=
_
.
without
(
Vue
.
http
.
interceptors
,
interceptor
);
});
it
(
'
fetch related issues
'
,
(
done
)
=>
{
service
.
fetchRelatedIssues
()
.
then
(
res
=>
res
.
json
())
.
then
((
relatedIssues
)
=>
{
expect
(
relatedIssues
).
toEqual
([
issuable1
]);
done
();
})
.
catch
((
err
)
=>
{
done
.
fail
(
`Failed to fetch related issues:\n
${
err
}
`
);
});
});
});
describe
(
'
addRelatedIssues
'
,
()
=>
{
const
interceptor
=
(
request
,
next
)
=>
{
next
(
request
.
respondWith
(
JSON
.
stringify
({
message
:
`
${
issuable1
.
reference
}
was successfully related`
,
status
:
'
success
'
,
}),
{
status
:
200
,
}));
};
beforeEach
(()
=>
{
Vue
.
http
.
interceptors
.
push
(
interceptor
);
});
afterEach
(()
=>
{
Vue
.
http
.
interceptors
=
_
.
without
(
Vue
.
http
.
interceptors
,
interceptor
);
});
it
(
'
add related issues
'
,
(
done
)
=>
{
service
.
addRelatedIssues
([
issuable1
.
reference
])
.
then
(
res
=>
res
.
json
())
.
then
((
resData
)
=>
{
expect
(
resData
.
status
).
toEqual
(
'
success
'
);
done
();
})
.
catch
((
err
)
=>
{
done
.
fail
(
`Failed to add related issues:\n
${
err
}
`
);
});
});
});
describe
(
'
removeRelatedIssue
'
,
()
=>
{
const
interceptor
=
(
request
,
next
)
=>
{
next
(
request
.
respondWith
(
JSON
.
stringify
({
message
:
'
Relation was removed
'
,
status
:
'
success
'
,
}),
{
status
:
200
,
}));
};
beforeEach
(()
=>
{
Vue
.
http
.
interceptors
.
push
(
interceptor
);
});
afterEach
(()
=>
{
Vue
.
http
.
interceptors
=
_
.
without
(
Vue
.
http
.
interceptors
,
interceptor
);
});
it
(
'
remove related issue
'
,
(
done
)
=>
{
service
.
removeRelatedIssue
(
'
...
'
)
.
then
(
res
=>
res
.
json
())
.
then
((
resData
)
=>
{
expect
(
resData
.
status
).
toEqual
(
'
success
'
);
done
();
})
.
catch
((
err
)
=>
{
done
.
fail
(
`Failed to fetch issue:\n
${
err
}
`
);
});
});
});
});
spec/javascripts/issuable/related_issues/stores/related_issues_store_spec.js
0 → 100644
View file @
6656d92a
import
RelatedIssuesStore
from
'
~/issuable/related_issues/stores/related_issues_store
'
;
const
issuable1
=
{
id
:
'
200
'
,
reference
:
'
foo/bar#123
'
,
title
:
'
issue1
'
,
path
:
'
/foo/bar/issues/123
'
,
state
:
'
opened
'
,
destroy_relation_path
:
'
/foo/bar/issues/123/related_issues/1
'
,
};
const
issuable2
=
{
id
:
'
201
'
,
reference
:
'
foo/bar#124
'
,
title
:
'
issue1
'
,
path
:
'
/foo/bar/issues/124
'
,
state
:
'
opened
'
,
destroy_relation_path
:
'
/foo/bar/issues/124/related_issues/1
'
,
};
describe
(
'
RelatedIssuesStore
'
,
()
=>
{
let
store
;
beforeEach
(()
=>
{
store
=
new
RelatedIssuesStore
();
});
describe
(
'
setRelatedIssues
'
,
()
=>
{
it
(
'
defaults to empty array
'
,
()
=>
{
expect
(
store
.
state
.
relatedIssues
).
toEqual
([]);
});
it
(
'
add issue
'
,
()
=>
{
const
relatedIssues
=
[
issuable1
];
store
.
setRelatedIssues
(
relatedIssues
);
expect
(
store
.
state
.
relatedIssues
).
toEqual
(
relatedIssues
);
});
});
describe
(
'
removeRelatedIssue
'
,
()
=>
{
it
(
'
remove issue
'
,
()
=>
{
const
relatedIssues
=
[
issuable1
];
store
.
state
.
relatedIssues
=
relatedIssues
;
store
.
removeRelatedIssue
(
issuable1
.
id
);
expect
(
store
.
state
.
relatedIssues
).
toEqual
([]);
});
it
(
'
remove issue with multiple in store
'
,
()
=>
{
const
relatedIssues
=
[
issuable1
,
issuable2
];
store
.
state
.
relatedIssues
=
relatedIssues
;
store
.
removeRelatedIssue
(
issuable1
.
id
);
expect
(
store
.
state
.
relatedIssues
).
toEqual
([
issuable2
]);
});
});
describe
(
'
setPendingReferences
'
,
()
=>
{
it
(
'
defaults to empty array
'
,
()
=>
{
expect
(
store
.
state
.
pendingReferences
).
toEqual
([]);
});
it
(
'
add reference
'
,
()
=>
{
const
relatedIssues
=
[
issuable1
.
reference
];
store
.
setPendingReferences
(
relatedIssues
);
expect
(
store
.
state
.
pendingReferences
).
toEqual
(
relatedIssues
);
});
});
describe
(
'
removePendingRelatedIssue
'
,
()
=>
{
it
(
'
remove issue
'
,
()
=>
{
const
relatedIssues
=
[
issuable1
.
reference
];
store
.
state
.
pendingReferences
=
relatedIssues
;
store
.
removePendingRelatedIssue
(
0
);
expect
(
store
.
state
.
pendingReferences
).
toEqual
([]);
});
it
(
'
remove issue with multiple in store
'
,
()
=>
{
const
relatedIssues
=
[
issuable1
.
reference
,
issuable2
.
reference
];
store
.
state
.
pendingReferences
=
relatedIssues
;
store
.
removePendingRelatedIssue
(
0
);
expect
(
store
.
state
.
pendingReferences
).
toEqual
([
issuable2
.
reference
]);
});
});
});
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment