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
0
Merge Requests
0
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
Jérome Perrin
gitlab-ce
Commits
882a591d
Commit
882a591d
authored
May 31, 2017
by
Filipa Lacerda
Browse files
Options
Browse Files
Download
Plain Diff
Merge branch 'issue-edit-inline' into 'master'
Issue inline editing Closes #24873 See merge request !11322
parents
f9f04244
29fd1305
Changes
46
Hide whitespace changes
Inline
Side-by-side
Showing
46 changed files
with
2057 additions
and
96 deletions
+2057
-96
app/assets/javascripts/dropzone_input.js
app/assets/javascripts/dropzone_input.js
+1
-0
app/assets/javascripts/issue_show/components/app.vue
app/assets/javascripts/issue_show/components/app.vue
+169
-20
app/assets/javascripts/issue_show/components/description.vue
app/assets/javascripts/issue_show/components/description.vue
+5
-2
app/assets/javascripts/issue_show/components/edit_actions.vue
...assets/javascripts/issue_show/components/edit_actions.vue
+79
-0
app/assets/javascripts/issue_show/components/fields/confidential_checkbox.vue
...ts/issue_show/components/fields/confidential_checkbox.vue
+23
-0
app/assets/javascripts/issue_show/components/fields/description.vue
.../javascripts/issue_show/components/fields/description.vue
+54
-0
app/assets/javascripts/issue_show/components/fields/description_template.vue
...pts/issue_show/components/fields/description_template.vue
+111
-0
app/assets/javascripts/issue_show/components/fields/project_move.vue
...javascripts/issue_show/components/fields/project_move.vue
+83
-0
app/assets/javascripts/issue_show/components/fields/title.vue
...assets/javascripts/issue_show/components/fields/title.vue
+31
-0
app/assets/javascripts/issue_show/components/form.vue
app/assets/javascripts/issue_show/components/form.vue
+104
-0
app/assets/javascripts/issue_show/components/locked_warning.vue
...sets/javascripts/issue_show/components/locked_warning.vue
+20
-0
app/assets/javascripts/issue_show/event_hub.js
app/assets/javascripts/issue_show/event_hub.js
+3
-0
app/assets/javascripts/issue_show/index.js
app/assets/javascripts/issue_show/index.js
+44
-37
app/assets/javascripts/issue_show/mixins/animate.js
app/assets/javascripts/issue_show/mixins/animate.js
+1
-1
app/assets/javascripts/issue_show/mixins/update.js
app/assets/javascripts/issue_show/mixins/update.js
+10
-0
app/assets/javascripts/issue_show/services/index.js
app/assets/javascripts/issue_show/services/index.js
+15
-2
app/assets/javascripts/issue_show/stores/index.js
app/assets/javascripts/issue_show/stores/index.js
+21
-1
app/assets/javascripts/lib/utils/text_utility.js
app/assets/javascripts/lib/utils/text_utility.js
+1
-1
app/assets/javascripts/shortcuts_issuable.js
app/assets/javascripts/shortcuts_issuable.js
+3
-1
app/assets/javascripts/vue_shared/components/markdown/field.vue
...sets/javascripts/vue_shared/components/markdown/field.vue
+107
-0
app/assets/javascripts/vue_shared/components/markdown/header.vue
...ets/javascripts/vue_shared/components/markdown/header.vue
+113
-0
app/assets/javascripts/vue_shared/components/markdown/toolbar.vue
...ts/javascripts/vue_shared/components/markdown/toolbar.vue
+33
-0
app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue
...scripts/vue_shared/components/markdown/toolbar_button.vue
+58
-0
app/assets/javascripts/vue_shared/mixins/tooltip.js
app/assets/javascripts/vue_shared/mixins/tooltip.js
+4
-0
app/controllers/concerns/issuable_actions.rb
app/controllers/concerns/issuable_actions.rb
+10
-1
app/controllers/projects/issues_controller.rb
app/controllers/projects/issues_controller.rb
+1
-4
app/helpers/issuables_helper.rb
app/helpers/issuables_helper.rb
+21
-0
app/serializers/issue_entity.rb
app/serializers/issue_entity.rb
+6
-0
app/views/projects/issues/show.html.haml
app/views/projects/issues/show.html.haml
+3
-5
changelogs/unreleased/issue-edit-inline.yml
changelogs/unreleased/issue-edit-inline.yml
+4
-0
features/steps/shared/markdown.rb
features/steps/shared/markdown.rb
+3
-3
spec/controllers/projects/issues_controller_spec.rb
spec/controllers/projects/issues_controller_spec.rb
+1
-1
spec/features/issues/move_spec.rb
spec/features/issues/move_spec.rb
+5
-3
spec/javascripts/issue_show/components/app_spec.js
spec/javascripts/issue_show/components/app_spec.js
+305
-1
spec/javascripts/issue_show/components/edit_actions_spec.js
spec/javascripts/issue_show/components/edit_actions_spec.js
+147
-0
spec/javascripts/issue_show/components/fields/description_spec.js
...ascripts/issue_show/components/fields/description_spec.js
+56
-0
spec/javascripts/issue_show/components/fields/description_template_spec.js
...issue_show/components/fields/description_template_spec.js
+49
-0
spec/javascripts/issue_show/components/fields/project_move_spec.js
...scripts/issue_show/components/fields/project_move_spec.js
+38
-0
spec/javascripts/issue_show/components/fields/title_spec.js
spec/javascripts/issue_show/components/fields/title_spec.js
+30
-0
spec/javascripts/issue_show/components/form_spec.js
spec/javascripts/issue_show/components/form_spec.js
+68
-0
spec/javascripts/issue_show/components/title_spec.js
spec/javascripts/issue_show/components/title_spec.js
+8
-0
spec/javascripts/pipelines/graph/action_component_spec.js
spec/javascripts/pipelines/graph/action_component_spec.js
+4
-2
spec/javascripts/pipelines/graph/dropdown_action_component_spec.js
...scripts/pipelines/graph/dropdown_action_component_spec.js
+3
-1
spec/javascripts/pipelines/graph/job_component_spec.js
spec/javascripts/pipelines/graph/job_component_spec.js
+14
-10
spec/javascripts/vue_shared/components/markdown/field_spec.js
.../javascripts/vue_shared/components/markdown/field_spec.js
+121
-0
spec/javascripts/vue_shared/components/markdown/header_spec.js
...javascripts/vue_shared/components/markdown/header_spec.js
+67
-0
No files found.
app/assets/javascripts/dropzone_input.js
View file @
882a591d
...
@@ -194,6 +194,7 @@ window.DropzoneInput = (function() {
...
@@ -194,6 +194,7 @@ window.DropzoneInput = (function() {
$
(
child
).
val
(
beforeSelection
+
formattedText
+
afterSelection
);
$
(
child
).
val
(
beforeSelection
+
formattedText
+
afterSelection
);
textarea
.
setSelectionRange
(
caretStart
+
formattedText
.
length
,
caretEnd
+
formattedText
.
length
);
textarea
.
setSelectionRange
(
caretStart
+
formattedText
.
length
,
caretEnd
+
formattedText
.
length
);
textarea
.
style
.
height
=
`
${
textarea
.
scrollHeight
}
px`
;
textarea
.
style
.
height
=
`
${
textarea
.
scrollHeight
}
px`
;
formTextarea
.
get
(
0
).
dispatchEvent
(
new
Event
(
'
input
'
));
return
formTextarea
.
trigger
(
'
input
'
);
return
formTextarea
.
trigger
(
'
input
'
);
};
};
...
...
app/assets/javascripts/issue_show/components/app.vue
View file @
882a591d
<
script
>
<
script
>
/* global Flash */
import
Visibility
from
'
visibilityjs
'
;
import
Visibility
from
'
visibilityjs
'
;
import
Poll
from
'
../../lib/utils/poll
'
;
import
Poll
from
'
../../lib/utils/poll
'
;
import
eventHub
from
'
../event_hub
'
;
import
Service
from
'
../services/index
'
;
import
Service
from
'
../services/index
'
;
import
Store
from
'
../stores
'
;
import
Store
from
'
../stores
'
;
import
titleComponent
from
'
./title.vue
'
;
import
titleComponent
from
'
./title.vue
'
;
import
descriptionComponent
from
'
./description.vue
'
;
import
descriptionComponent
from
'
./description.vue
'
;
import
formComponent
from
'
./form.vue
'
;
import
'
../../lib/utils/url_utility
'
;
export
default
{
export
default
{
props
:
{
props
:
{
...
@@ -12,15 +16,27 @@ export default {
...
@@ -12,15 +16,27 @@ export default {
required
:
true
,
required
:
true
,
type
:
String
,
type
:
String
,
},
},
canMove
:
{
required
:
true
,
type
:
Boolean
,
},
canUpdate
:
{
canUpdate
:
{
required
:
true
,
required
:
true
,
type
:
Boolean
,
type
:
Boolean
,
},
},
canDestroy
:
{
required
:
true
,
type
:
Boolean
,
},
issuableRef
:
{
issuableRef
:
{
type
:
String
,
type
:
String
,
required
:
true
,
required
:
true
,
},
},
initialTitle
:
{
initialTitleHtml
:
{
type
:
String
,
required
:
true
,
},
initialTitleText
:
{
type
:
String
,
type
:
String
,
required
:
true
,
required
:
true
,
},
},
...
@@ -34,10 +50,40 @@ export default {
...
@@ -34,10 +50,40 @@ export default {
required
:
false
,
required
:
false
,
default
:
''
,
default
:
''
,
},
},
issuableTemplates
:
{
type
:
Array
,
required
:
false
,
default
:
()
=>
[],
},
isConfidential
:
{
type
:
Boolean
,
required
:
true
,
},
markdownPreviewUrl
:
{
type
:
String
,
required
:
true
,
},
markdownDocs
:
{
type
:
String
,
required
:
true
,
},
projectPath
:
{
type
:
String
,
required
:
true
,
},
projectNamespace
:
{
type
:
String
,
required
:
true
,
},
projectsAutocompleteUrl
:
{
type
:
String
,
required
:
true
,
},
},
},
data
()
{
data
()
{
const
store
=
new
Store
({
const
store
=
new
Store
({
titleHtml
:
this
.
initialTitle
,
titleHtml
:
this
.
initialTitleHtml
,
titleText
:
this
.
initialTitleText
,
descriptionHtml
:
this
.
initialDescriptionHtml
,
descriptionHtml
:
this
.
initialDescriptionHtml
,
descriptionText
:
this
.
initialDescriptionText
,
descriptionText
:
this
.
initialDescriptionText
,
});
});
...
@@ -45,19 +91,97 @@ export default {
...
@@ -45,19 +91,97 @@ export default {
return
{
return
{
store
,
store
,
state
:
store
.
state
,
state
:
store
.
state
,
showForm
:
false
,
};
};
},
},
computed
:
{
formState
()
{
return
this
.
store
.
formState
;
},
},
components
:
{
components
:
{
descriptionComponent
,
descriptionComponent
,
titleComponent
,
titleComponent
,
formComponent
,
},
methods
:
{
openForm
()
{
if
(
!
this
.
showForm
)
{
this
.
showForm
=
true
;
this
.
store
.
setFormState
({
title
:
this
.
state
.
titleText
,
confidential
:
this
.
isConfidential
,
description
:
this
.
state
.
descriptionText
,
lockedWarningVisible
:
false
,
move_to_project_id
:
0
,
updateLoading
:
false
,
});
}
},
closeForm
()
{
this
.
showForm
=
false
;
},
updateIssuable
()
{
const
canPostUpdate
=
this
.
store
.
formState
.
move_to_project_id
!==
0
?
confirm
(
'
Are you sure you want to move this issue to another project?
'
)
:
true
;
// eslint-disable-line no-alert
if
(
!
canPostUpdate
)
{
this
.
store
.
setFormState
({
updateLoading
:
false
,
});
return
;
}
this
.
service
.
updateIssuable
(
this
.
store
.
formState
)
.
then
(
res
=>
res
.
json
())
.
then
((
data
)
=>
{
if
(
location
.
pathname
!==
data
.
web_url
)
{
gl
.
utils
.
visitUrl
(
data
.
web_url
);
}
else
if
(
data
.
confidential
!==
this
.
isConfidential
)
{
gl
.
utils
.
visitUrl
(
location
.
pathname
);
}
return
this
.
service
.
getData
();
})
.
then
(
res
=>
res
.
json
())
.
then
((
data
)
=>
{
this
.
store
.
updateState
(
data
);
eventHub
.
$emit
(
'
close.form
'
);
})
.
catch
(()
=>
{
eventHub
.
$emit
(
'
close.form
'
);
return
new
Flash
(
'
Error updating issue
'
);
});
},
deleteIssuable
()
{
this
.
service
.
deleteIssuable
()
.
then
(
res
=>
res
.
json
())
.
then
((
data
)
=>
{
// Stop the poll so we don't get 404's with the issue not existing
this
.
poll
.
stop
();
gl
.
utils
.
visitUrl
(
data
.
web_url
);
})
.
catch
(()
=>
{
eventHub
.
$emit
(
'
close.form
'
);
return
new
Flash
(
'
Error deleting issue
'
);
});
},
},
},
created
()
{
created
()
{
const
resour
ce
=
new
Service
(
this
.
endpoint
);
this
.
servi
ce
=
new
Service
(
this
.
endpoint
);
const
poll
=
new
Poll
({
this
.
poll
=
new
Poll
({
resource
,
resource
:
this
.
service
,
method
:
'
getData
'
,
method
:
'
getData
'
,
successCallback
:
(
res
)
=>
{
successCallback
:
(
res
)
=>
{
this
.
store
.
updateState
(
res
.
json
());
const
data
=
res
.
json
();
const
shouldUpdate
=
this
.
store
.
stateShouldUpdate
(
data
);
this
.
store
.
updateState
(
data
);
if
(
this
.
showForm
&&
(
shouldUpdate
.
title
||
shouldUpdate
.
description
))
{
this
.
store
.
formState
.
lockedWarningVisible
=
true
;
}
},
},
errorCallback
(
err
)
{
errorCallback
(
err
)
{
throw
new
Error
(
err
);
throw
new
Error
(
err
);
...
@@ -65,32 +189,57 @@ export default {
...
@@ -65,32 +189,57 @@ export default {
});
});
if
(
!
Visibility
.
hidden
())
{
if
(
!
Visibility
.
hidden
())
{
poll
.
makeRequest
();
this
.
poll
.
makeRequest
();
}
}
Visibility
.
change
(()
=>
{
Visibility
.
change
(()
=>
{
if
(
!
Visibility
.
hidden
())
{
if
(
!
Visibility
.
hidden
())
{
poll
.
restart
();
this
.
poll
.
restart
();
}
else
{
}
else
{
poll
.
stop
();
this
.
poll
.
stop
();
}
}
});
});
eventHub
.
$on
(
'
delete.issuable
'
,
this
.
deleteIssuable
);
eventHub
.
$on
(
'
update.issuable
'
,
this
.
updateIssuable
);
eventHub
.
$on
(
'
close.form
'
,
this
.
closeForm
);
eventHub
.
$on
(
'
open.form
'
,
this
.
openForm
);
},
beforeDestroy
()
{
eventHub
.
$off
(
'
delete.issuable
'
,
this
.
deleteIssuable
);
eventHub
.
$off
(
'
update.issuable
'
,
this
.
updateIssuable
);
eventHub
.
$off
(
'
close.form
'
,
this
.
closeForm
);
eventHub
.
$off
(
'
open.form
'
,
this
.
openForm
);
},
},
};
};
</
script
>
</
script
>
<
template
>
<
template
>
<div>
<div>
<title-component
<form-component
:issuable-ref=
"issuableRef"
v-if=
"canUpdate && showForm"
:title-html=
"state.titleHtml"
:form-state=
"formState"
:title-text=
"state.titleText"
/>
:can-move=
"canMove"
<description-component
:can-destroy=
"canDestroy"
v-if=
"state.descriptionHtml"
:issuable-templates=
"issuableTemplates"
:can-update=
"canUpdate"
:markdown-docs=
"markdownDocs"
:description-html=
"state.descriptionHtml"
:markdown-preview-url=
"markdownPreviewUrl"
:description-text=
"state.descriptionText"
:project-path=
"projectPath"
:updated-at=
"state.updatedAt"
:project-namespace=
"projectNamespace"
:task-status=
"state.taskStatus"
/>
:projects-autocomplete-url=
"projectsAutocompleteUrl"
/>
<div
v-else
>
<title-component
:issuable-ref=
"issuableRef"
:title-html=
"state.titleHtml"
:title-text=
"state.titleText"
/>
<description-component
v-if=
"state.descriptionHtml"
:can-update=
"canUpdate"
:description-html=
"state.descriptionHtml"
:description-text=
"state.descriptionText"
:updated-at=
"state.updatedAt"
:task-status=
"state.taskStatus"
/>
</div>
</div>
</div>
</
template
>
</
template
>
app/assets/javascripts/issue_show/components/description.vue
View file @
882a591d
...
@@ -18,11 +18,13 @@
...
@@ -18,11 +18,13 @@
},
},
updatedAt
:
{
updatedAt
:
{
type
:
String
,
type
:
String
,
required
:
true
,
required
:
false
,
default
:
''
,
},
},
taskStatus
:
{
taskStatus
:
{
type
:
String
,
type
:
String
,
required
:
true
,
required
:
false
,
default
:
''
,
},
},
},
},
data
()
{
data
()
{
...
@@ -83,6 +85,7 @@
...
@@ -83,6 +85,7 @@
<
template
>
<
template
>
<div
<div
v-if=
"descriptionHtml"
class=
"description"
class=
"description"
:class=
"
{
:class=
"
{
'js-task-list-container': canUpdate
'js-task-list-container': canUpdate
...
...
app/assets/javascripts/issue_show/components/edit_actions.vue
0 → 100644
View file @
882a591d
<
script
>
import
updateMixin
from
'
../mixins/update
'
;
import
eventHub
from
'
../event_hub
'
;
export
default
{
mixins
:
[
updateMixin
],
props
:
{
canDestroy
:
{
type
:
Boolean
,
required
:
true
,
},
formState
:
{
type
:
Object
,
required
:
true
,
},
},
data
()
{
return
{
deleteLoading
:
false
,
};
},
computed
:
{
isSubmitEnabled
()
{
return
this
.
formState
.
title
.
trim
()
!==
''
;
},
},
methods
:
{
closeForm
()
{
eventHub
.
$emit
(
'
close.form
'
);
},
deleteIssuable
()
{
// eslint-disable-next-line no-alert
if
(
confirm
(
'
Issue will be removed! Are you sure?
'
))
{
this
.
deleteLoading
=
true
;
eventHub
.
$emit
(
'
delete.issuable
'
);
}
},
},
};
</
script
>
<
template
>
<div
class=
"prepend-top-default append-bottom-default clearfix"
>
<button
class=
"btn btn-save pull-left"
:class=
"
{ disabled: formState.updateLoading || !isSubmitEnabled }"
type="submit"
:disabled="formState.updateLoading || !isSubmitEnabled"
@click.prevent="updateIssuable">
Save changes
<i
class=
"fa fa-spinner fa-spin"
aria-hidden=
"true"
v-if=
"formState.updateLoading"
>
</i>
</button>
<button
class=
"btn btn-default pull-right"
type=
"button"
@
click=
"closeForm"
>
Cancel
</button>
<button
v-if=
"canDestroy"
class=
"btn btn-danger pull-right append-right-default"
:class=
"
{ disabled: deleteLoading }"
type="button"
:disabled="deleteLoading"
@click="deleteIssuable">
Delete
<i
class=
"fa fa-spinner fa-spin"
aria-hidden=
"true"
v-if=
"deleteLoading"
>
</i>
</button>
</div>
</
template
>
app/assets/javascripts/issue_show/components/fields/confidential_checkbox.vue
0 → 100644
View file @
882a591d
<
script
>
export
default
{
props
:
{
formState
:
{
type
:
Object
,
required
:
true
,
},
},
};
</
script
>
<
template
>
<fieldset
class=
"checkbox"
>
<label
for=
"issue-confidential"
>
<input
type=
"checkbox"
value=
"1"
id=
"issue-confidential"
v-model=
"formState.confidential"
/>
This issue is confidential and should only be visible to team members with at least Reporter access.
</label>
</fieldset>
</
template
>
app/assets/javascripts/issue_show/components/fields/description.vue
0 → 100644
View file @
882a591d
<
script
>
/* global Flash */
import
updateMixin
from
'
../../mixins/update
'
;
import
markdownField
from
'
../../../vue_shared/components/markdown/field.vue
'
;
export
default
{
mixins
:
[
updateMixin
],
props
:
{
formState
:
{
type
:
Object
,
required
:
true
,
},
markdownPreviewUrl
:
{
type
:
String
,
required
:
true
,
},
markdownDocs
:
{
type
:
String
,
required
:
true
,
},
},
components
:
{
markdownField
,
},
mounted
()
{
this
.
$refs
.
textarea
.
focus
();
},
};
</
script
>
<
template
>
<div
class=
"common-note-form"
>
<label
class=
"sr-only"
for=
"issue-description"
>
Description
</label>
<markdown-field
:markdown-preview-url=
"markdownPreviewUrl"
:markdown-docs=
"markdownDocs"
>
<textarea
id=
"issue-description"
class=
"note-textarea js-gfm-input js-autosize markdown-area"
data-supports-slash-commands=
"false"
aria-label=
"Description"
v-model=
"formState.description"
ref=
"textarea"
slot=
"textarea"
placeholder=
"Write a comment or drag your files here..."
@
keydown.meta.enter=
"updateIssuable"
>
</textarea>
</markdown-field>
</div>
</
template
>
app/assets/javascripts/issue_show/components/fields/description_template.vue
0 → 100644
View file @
882a591d
<
script
>
export
default
{
props
:
{
formState
:
{
type
:
Object
,
required
:
true
,
},
issuableTemplates
:
{
type
:
Array
,
required
:
false
,
default
:
()
=>
[],
},
projectPath
:
{
type
:
String
,
required
:
true
,
},
projectNamespace
:
{
type
:
String
,
required
:
true
,
},
},
computed
:
{
issuableTemplatesJson
()
{
return
JSON
.
stringify
(
this
.
issuableTemplates
);
},
},
mounted
()
{
// Create the editor for the template
const
editor
=
document
.
querySelector
(
'
.detail-page-description .note-textarea
'
)
||
{};
editor
.
setValue
=
(
val
)
=>
{
this
.
formState
.
description
=
val
;
};
editor
.
getValue
=
()
=>
this
.
formState
.
description
;
this
.
issuableTemplate
=
new
gl
.
IssuableTemplateSelectors
({
$dropdowns
:
$
(
this
.
$refs
.
toggle
),
editor
,
});
},
};
</
script
>
<
template
>
<div
class=
"dropdown js-issuable-selector-wrap"
data-issuable-type=
"issue"
>
<button
class=
"dropdown-menu-toggle js-issuable-selector"
type=
"button"
ref=
"toggle"
data-field-name=
"issuable_template"
data-selected=
"null"
data-toggle=
"dropdown"
:data-namespace-path=
"projectNamespace"
:data-project-path=
"projectPath"
:data-data=
"issuableTemplatesJson"
>
<span
class=
"dropdown-toggle-text"
>
Choose a template
</span>
<i
aria-hidden=
"true"
class=
"fa fa-chevron-down"
>
</i>
</button>
<div
class=
"dropdown-menu dropdown-select"
>
<div
class=
"dropdown-title"
>
Choose a template
<button
class=
"dropdown-title-button dropdown-menu-close"
aria-label=
"Close"
type=
"button"
>
<i
aria-hidden=
"true"
class=
"fa fa-times dropdown-menu-close-icon"
>
</i>
</button>
</div>
<div
class=
"dropdown-input"
>
<input
type=
"search"
class=
"dropdown-input-field"
placeholder=
"Filter"
autocomplete=
"off"
/>
<i
aria-hidden=
"true"
class=
"fa fa-search dropdown-input-search"
>
</i>
<i
role=
"button"
aria-label=
"Clear templates search input"
class=
"fa fa-times dropdown-input-clear js-dropdown-input-clear"
>
</i>
</div>
<div
class=
"dropdown-content"
></div>
<div
class=
"dropdown-footer"
>
<ul
class=
"dropdown-footer-list"
>
<li>
<a
class=
"no-template"
>
No template
</a>
</li>
<li>
<a
class=
"reset-template"
>
Reset template
</a>
</li>
</ul>
</div>
</div>
</div>
</
template
>
app/assets/javascripts/issue_show/components/fields/project_move.vue
0 → 100644
View file @
882a591d
<
script
>
import
tooltipMixin
from
'
../../../vue_shared/mixins/tooltip
'
;
export
default
{
mixins
:
[
tooltipMixin
,
],
props
:
{
formState
:
{
type
:
Object
,
required
:
true
,
},
projectsAutocompleteUrl
:
{
type
:
String
,
required
:
true
,
},
},
mounted
()
{
const
$moveDropdown
=
$
(
this
.
$refs
[
'
move-dropdown
'
]);
$moveDropdown
.
select2
({
ajax
:
{
url
:
this
.
projectsAutocompleteUrl
,
quietMillis
:
125
,
data
(
term
,
page
,
context
)
{
return
{
search
:
term
,
offset_id
:
context
,
};
},
results
(
data
)
{
const
more
=
data
.
length
>=
50
;
const
context
=
data
[
data
.
length
-
1
]
?
data
[
data
.
length
-
1
].
id
:
null
;
return
{
results
:
data
,
more
,
context
,
};
},
},
formatResult
(
project
)
{
return
project
.
name_with_namespace
;
},
formatSelection
(
project
)
{
return
project
.
name_with_namespace
;
},
})
.
on
(
'
change
'
,
(
e
)
=>
{
this
.
formState
.
move_to_project_id
=
parseInt
(
e
.
target
.
value
,
10
);
});
},
beforeDestroy
()
{
$
(
this
.
$refs
[
'
move-dropdown
'
]).
select2
(
'
destroy
'
);
},
};
</
script
>
<
template
>
<fieldset>
<label
for=
"issuable-move"
class=
"sr-only"
>
Move
</label>
<div
class=
"issuable-form-select-holder append-right-5"
>
<input
ref=
"move-dropdown"
type=
"hidden"
id=
"issuable-move"
data-placeholder=
"Move to a different project"
/>
</div>
<span
data-placement=
"auto top"
title=
"Moving an issue will copy the discussion to a different project and close it here. All participants will be notified of the new location."
ref=
"tooltip"
>
<i
class=
"fa fa-question-circle"
aria-hidden=
"true"
>
</i>
</span>
</fieldset>
</
template
>
app/assets/javascripts/issue_show/components/fields/title.vue
0 → 100644
View file @
882a591d
<
script
>
import
updateMixin
from
'
../../mixins/update
'
;
export
default
{
mixins
:
[
updateMixin
],
props
:
{
formState
:
{
type
:
Object
,
required
:
true
,
},
},
};
</
script
>
<
template
>
<fieldset>
<label
class=
"sr-only"
for=
"issue-title"
>
Title
</label>
<input
id=
"issue-title"
class=
"form-control"
type=
"text"
placeholder=
"Issue title"
aria-label=
"Issue title"
v-model=
"formState.title"
@
keydown.meta.enter=
"updateIssuable"
/>
</fieldset>
</
template
>
app/assets/javascripts/issue_show/components/form.vue
0 → 100644
View file @
882a591d
<
script
>
import
lockedWarning
from
'
./locked_warning.vue
'
;
import
titleField
from
'
./fields/title.vue
'
;
import
descriptionField
from
'
./fields/description.vue
'
;
import
editActions
from
'
./edit_actions.vue
'
;
import
descriptionTemplate
from
'
./fields/description_template.vue
'
;
import
projectMove
from
'
./fields/project_move.vue
'
;
import
confidentialCheckbox
from
'
./fields/confidential_checkbox.vue
'
;
export
default
{
props
:
{
canMove
:
{
type
:
Boolean
,
required
:
true
,
},
canDestroy
:
{
type
:
Boolean
,
required
:
true
,
},
formState
:
{
type
:
Object
,
required
:
true
,
},
issuableTemplates
:
{
type
:
Array
,
required
:
false
,
default
:
()
=>
[],
},
markdownPreviewUrl
:
{
type
:
String
,
required
:
true
,
},
markdownDocs
:
{
type
:
String
,
required
:
true
,
},
projectPath
:
{
type
:
String
,
required
:
true
,
},
projectNamespace
:
{
type
:
String
,
required
:
true
,
},
projectsAutocompleteUrl
:
{
type
:
String
,
required
:
true
,
},
},
components
:
{
lockedWarning
,
titleField
,
descriptionField
,
descriptionTemplate
,
editActions
,
projectMove
,
confidentialCheckbox
,
},
computed
:
{
hasIssuableTemplates
()
{
return
this
.
issuableTemplates
.
length
;
},
},
};
</
script
>
<
template
>
<form>
<locked-warning
v-if=
"formState.lockedWarningVisible"
/>
<div
class=
"row"
>
<div
class=
"col-sm-4 col-lg-3"
v-if=
"hasIssuableTemplates"
>
<description-template
:form-state=
"formState"
:issuable-templates=
"issuableTemplates"
:project-path=
"projectPath"
:project-namespace=
"projectNamespace"
/>
</div>
<div
:class=
"
{
'col-sm-8 col-lg-9': hasIssuableTemplates,
'col-xs-12': !hasIssuableTemplates,
}">
<title-field
:form-state=
"formState"
:issuable-templates=
"issuableTemplates"
/>
</div>
</div>
<description-field
:form-state=
"formState"
:markdown-preview-url=
"markdownPreviewUrl"
:markdown-docs=
"markdownDocs"
/>
<confidential-checkbox
:form-state=
"formState"
/>
<project-move
v-if=
"canMove"
:form-state=
"formState"
:projects-autocomplete-url=
"projectsAutocompleteUrl"
/>
<edit-actions
:form-state=
"formState"
:can-destroy=
"canDestroy"
/>
</form>
</
template
>
app/assets/javascripts/issue_show/components/locked_warning.vue
0 → 100644
View file @
882a591d
<
script
>
export
default
{
computed
:
{
currentPath
()
{
return
location
.
pathname
;
},
},
};
</
script
>
<
template
>
<div
class=
"alert alert-danger"
>
Someone edited the issue at the same time you did. Please check out
<a
:href=
"currentPath"
target=
"_blank"
rel=
"nofollow"
>
the issue
</a>
and make sure your changes will not unintentionally remove theirs.
</div>
</
template
>
app/assets/javascripts/issue_show/event_hub.js
0 → 100644
View file @
882a591d
import
Vue
from
'
vue
'
;
export
default
new
Vue
();
app/assets/javascripts/issue_show/index.js
View file @
882a591d
import
Vue
from
'
vue
'
;
import
Vue
from
'
vue
'
;
import
eventHub
from
'
./event_hub
'
;
import
issuableApp
from
'
./components/app.vue
'
;
import
issuableApp
from
'
./components/app.vue
'
;
import
'
../vue_shared/vue_resource_interceptor
'
;
import
'
../vue_shared/vue_resource_interceptor
'
;
document
.
addEventListener
(
'
DOMContentLoaded
'
,
()
=>
new
Vue
({
document
.
addEventListener
(
'
DOMContentLoaded
'
,
()
=>
{
el
:
document
.
getElementById
(
'
js-issuable-app
'
),
const
initialDataEl
=
document
.
getElementById
(
'
js-issuable-app-initial-data
'
);
components
:
{
const
initialData
=
JSON
.
parse
(
initialDataEl
.
innerHTML
.
replace
(
/"/g
,
'
"
'
));
issuableApp
,
},
data
()
{
const
issuableElement
=
this
.
$options
.
el
;
const
issuableTitleElement
=
issuableElement
.
querySelector
(
'
.title
'
);
const
issuableDescriptionElement
=
issuableElement
.
querySelector
(
'
.wiki
'
);
const
issuableDescriptionTextarea
=
issuableElement
.
querySelector
(
'
.js-task-list-field
'
);
const
{
canUpdate
,
endpoint
,
issuableRef
,
}
=
issuableElement
.
dataset
;
return
{
$
(
'
.issuable-edit
'
).
on
(
'
click
'
,
(
e
)
=>
{
canUpdate
:
gl
.
utils
.
convertPermissionToBoolean
(
canUpdate
),
e
.
preventDefault
();
endpoint
,
issuableRef
,
eventHub
.
$emit
(
'
open.form
'
);
initialTitle
:
issuableTitleElement
.
innerHTML
,
});
initialDescriptionHtml
:
issuableDescriptionElement
?
issuableDescriptionElement
.
innerHTML
:
''
,
initialDescriptionText
:
issuableDescriptionTextarea
?
issuableDescriptionTextarea
.
textContent
:
''
,
return
new
Vue
({
};
el
:
document
.
getElementById
(
'
js-issuable-app
'
),
},
components
:
{
render
(
createElement
)
{
issuableApp
,
return
createElement
(
'
issuable-app
'
,
{
},
props
:
{
data
()
{
canUpdate
:
this
.
canUpdate
,
return
{
endpoint
:
this
.
endpoint
,
...
initialData
,
issuableRef
:
this
.
issuableRef
,
};
initialTitle
:
this
.
initialTitle
,
},
initialDescriptionHtml
:
this
.
initialDescriptionHtml
,
render
(
createElement
)
{
initialDescriptionText
:
this
.
initialDescriptionText
,
return
createElement
(
'
issuable-app
'
,
{
},
props
:
{
});
canUpdate
:
this
.
canUpdate
,
},
canDestroy
:
this
.
canDestroy
,
}));
canMove
:
this
.
canMove
,
endpoint
:
this
.
endpoint
,
issuableRef
:
this
.
issuableRef
,
initialTitleHtml
:
this
.
initialTitleHtml
,
initialTitleText
:
this
.
initialTitleText
,
initialDescriptionHtml
:
this
.
initialDescriptionHtml
,
initialDescriptionText
:
this
.
initialDescriptionText
,
issuableTemplates
:
this
.
issuableTemplates
,
isConfidential
:
this
.
isConfidential
,
markdownPreviewUrl
:
this
.
markdownPreviewUrl
,
markdownDocs
:
this
.
markdownDocs
,
projectPath
:
this
.
projectPath
,
projectNamespace
:
this
.
projectNamespace
,
projectsAutocompleteUrl
:
this
.
projectsAutocompleteUrl
,
},
});
},
});
});
app/assets/javascripts/issue_show/mixins/animate.js
View file @
882a591d
...
@@ -4,7 +4,7 @@ export default {
...
@@ -4,7 +4,7 @@ export default {
this
.
preAnimation
=
true
;
this
.
preAnimation
=
true
;
this
.
pulseAnimation
=
false
;
this
.
pulseAnimation
=
false
;
this
.
$nextTick
(()
=>
{
setTimeout
(()
=>
{
this
.
preAnimation
=
false
;
this
.
preAnimation
=
false
;
this
.
pulseAnimation
=
true
;
this
.
pulseAnimation
=
true
;
});
});
...
...
app/assets/javascripts/issue_show/mixins/update.js
0 → 100644
View file @
882a591d
import
eventHub
from
'
../event_hub
'
;
export
default
{
methods
:
{
updateIssuable
()
{
this
.
formState
.
updateLoading
=
true
;
eventHub
.
$emit
(
'
update.issuable
'
);
},
},
};
app/assets/javascripts/issue_show/services/index.js
View file @
882a591d
...
@@ -7,10 +7,23 @@ export default class Service {
...
@@ -7,10 +7,23 @@ export default class Service {
constructor
(
endpoint
)
{
constructor
(
endpoint
)
{
this
.
endpoint
=
endpoint
;
this
.
endpoint
=
endpoint
;
this
.
resource
=
Vue
.
resource
(
this
.
endpoint
);
this
.
resource
=
Vue
.
resource
(
`
${
this
.
endpoint
}
.json`
,
{},
{
realtimeChanges
:
{
method
:
'
GET
'
,
url
:
`
${
this
.
endpoint
}
/realtime_changes`
,
},
});
}
}
getData
()
{
getData
()
{
return
this
.
resource
.
get
();
return
this
.
resource
.
realtimeChanges
();
}
deleteIssuable
()
{
return
this
.
resource
.
delete
();
}
updateIssuable
(
data
)
{
return
this
.
resource
.
update
(
data
);
}
}
}
}
app/assets/javascripts/issue_show/stores/index.js
View file @
882a591d
export
default
class
Store
{
export
default
class
Store
{
constructor
({
constructor
({
titleHtml
,
titleHtml
,
titleText
,
descriptionHtml
,
descriptionHtml
,
descriptionText
,
descriptionText
,
})
{
})
{
this
.
state
=
{
this
.
state
=
{
titleHtml
,
titleHtml
,
titleText
:
''
,
titleText
,
descriptionHtml
,
descriptionHtml
,
descriptionText
,
descriptionText
,
taskStatus
:
''
,
taskStatus
:
''
,
updatedAt
:
''
,
updatedAt
:
''
,
};
};
this
.
formState
=
{
title
:
''
,
confidential
:
false
,
description
:
''
,
lockedWarningVisible
:
false
,
move_to_project_id
:
0
,
updateLoading
:
false
,
};
}
}
updateState
(
data
)
{
updateState
(
data
)
{
...
@@ -22,4 +31,15 @@ export default class Store {
...
@@ -22,4 +31,15 @@ export default class Store {
this
.
state
.
taskStatus
=
data
.
task_status
;
this
.
state
.
taskStatus
=
data
.
task_status
;
this
.
state
.
updatedAt
=
data
.
updated_at
;
this
.
state
.
updatedAt
=
data
.
updated_at
;
}
}
stateShouldUpdate
(
data
)
{
return
{
title
:
this
.
state
.
titleText
!==
data
.
title_text
,
description
:
this
.
state
.
descriptionText
!==
data
.
description_text
,
};
}
setFormState
(
state
)
{
this
.
formState
=
Object
.
assign
(
this
.
formState
,
state
);
}
}
}
app/assets/javascripts/lib/utils/text_utility.js
View file @
882a591d
...
@@ -170,7 +170,7 @@ gl.text.init = function(form) {
...
@@ -170,7 +170,7 @@ gl.text.init = function(form) {
});
});
};
};
gl
.
text
.
removeListeners
=
function
(
form
)
{
gl
.
text
.
removeListeners
=
function
(
form
)
{
return
$
(
'
.js-md
'
,
form
).
off
();
return
$
(
'
.js-md
'
,
form
).
off
(
'
click
'
);
};
};
gl
.
text
.
humanize
=
function
(
string
)
{
gl
.
text
.
humanize
=
function
(
string
)
{
return
string
.
charAt
(
0
).
toUpperCase
()
+
string
.
replace
(
/_/g
,
'
'
).
slice
(
1
);
return
string
.
charAt
(
0
).
toUpperCase
()
+
string
.
replace
(
/_/g
,
'
'
).
slice
(
1
);
...
...
app/assets/javascripts/shortcuts_issuable.js
View file @
882a591d
...
@@ -77,7 +77,9 @@ import './shortcuts_navigation';
...
@@ -77,7 +77,9 @@ import './shortcuts_navigation';
ShortcutsIssuable
.
prototype
.
editIssue
=
function
()
{
ShortcutsIssuable
.
prototype
.
editIssue
=
function
()
{
var
$editBtn
;
var
$editBtn
;
$editBtn
=
$
(
'
.issuable-edit
'
);
$editBtn
=
$
(
'
.issuable-edit
'
);
return
gl
.
utils
.
visitUrl
(
$editBtn
.
attr
(
'
href
'
));
// Need to click the element as on issues, editing is inline
// on merge request, editing is on a different page
$editBtn
.
get
(
0
).
click
();
};
};
ShortcutsIssuable
.
prototype
.
openSidebarDropdown
=
function
(
name
)
{
ShortcutsIssuable
.
prototype
.
openSidebarDropdown
=
function
(
name
)
{
...
...
app/assets/javascripts/vue_shared/components/markdown/field.vue
0 → 100644
View file @
882a591d
<
script
>
/* global Flash */
import
markdownHeader
from
'
./header.vue
'
;
import
markdownToolbar
from
'
./toolbar.vue
'
;
export
default
{
props
:
{
markdownPreviewUrl
:
{
type
:
String
,
required
:
false
,
default
:
''
,
},
markdownDocs
:
{
type
:
String
,
required
:
true
,
},
},
data
()
{
return
{
markdownPreview
:
''
,
markdownPreviewLoading
:
false
,
previewMarkdown
:
false
,
};
},
components
:
{
markdownHeader
,
markdownToolbar
,
},
methods
:
{
toggleMarkdownPreview
()
{
this
.
previewMarkdown
=
!
this
.
previewMarkdown
;
if
(
!
this
.
previewMarkdown
)
{
this
.
markdownPreview
=
''
;
}
else
{
this
.
markdownPreviewLoading
=
true
;
this
.
$http
.
post
(
this
.
markdownPreviewUrl
,
{
/*
Can't use `$refs` as the component is technically in the parent component
so we access the VNode & then get the element
*/
text
:
this
.
$slots
.
textarea
[
0
].
elm
.
value
,
},
)
.
then
((
res
)
=>
{
const
data
=
res
.
json
();
this
.
markdownPreviewLoading
=
false
;
this
.
markdownPreview
=
data
.
body
;
this
.
$nextTick
(()
=>
{
$
(
this
.
$refs
[
'
markdown-preview
'
]).
renderGFM
();
});
})
.
catch
(()
=>
new
Flash
(
'
Error loading markdown preview
'
));
}
},
},
mounted
()
{
/*
GLForm class handles all the toolbar buttons
*/
return
new
gl
.
GLForm
(
$
(
this
.
$refs
[
'
gl-form
'
]),
true
);
},
};
</
script
>
<
template
>
<div
class=
"md-area prepend-top-default append-bottom-default js-vue-markdown-field"
ref=
"gl-form"
>
<markdown-header
:preview-markdown=
"previewMarkdown"
@
toggle-markdown=
"toggleMarkdownPreview"
/>
<div
class=
"md-write-holder"
v-show=
"!previewMarkdown"
>
<div
class=
"zen-backdrop"
>
<slot
name=
"textarea"
></slot>
<a
class=
"zen-control zen-control-leave js-zen-leave"
href=
"#"
aria-label=
"Enter zen mode"
>
<i
class=
"fa fa-compress"
aria-hidden=
"true"
>
</i>
</a>
<markdown-toolbar
:markdown-docs=
"markdownDocs"
/>
</div>
</div>
<div
class=
"md md-preview-holder md-preview"
v-show=
"previewMarkdown"
>
<div
ref=
"markdown-preview"
v-html=
"markdownPreview"
>
</div>
<span
v-if=
"markdownPreviewLoading"
>
Loading...
</span>
</div>
</div>
</
template
>
app/assets/javascripts/vue_shared/components/markdown/header.vue
0 → 100644
View file @
882a591d
<
script
>
import
tooltipMixin
from
'
../../mixins/tooltip
'
;
import
toolbarButton
from
'
./toolbar_button.vue
'
;
export
default
{
mixins
:
[
tooltipMixin
,
],
props
:
{
previewMarkdown
:
{
type
:
Boolean
,
required
:
true
,
},
},
components
:
{
toolbarButton
,
},
methods
:
{
toggleMarkdownPreview
(
e
,
form
)
{
if
(
form
&&
!
form
.
find
(
'
.js-vue-markdown-field
'
).
length
)
{
return
;
}
else
if
(
e
.
target
.
blur
)
{
e
.
target
.
blur
();
}
this
.
$emit
(
'
toggle-markdown
'
);
},
},
mounted
()
{
$
(
document
).
on
(
'
markdown-preview:show.vue
'
,
this
.
toggleMarkdownPreview
);
$
(
document
).
on
(
'
markdown-preview:hide.vue
'
,
this
.
toggleMarkdownPreview
);
},
beforeDestroy
()
{
$
(
document
).
on
(
'
markdown-preview:show.vue
'
,
this
.
toggleMarkdownPreview
);
$
(
document
).
off
(
'
markdown-preview:hide.vue
'
,
this
.
toggleMarkdownPreview
);
},
};
</
script
>
<
template
>
<div
class=
"md-header"
>
<ul
class=
"nav-links clearfix"
>
<li
:class=
"
{ active: !previewMarkdown }">
<a
href=
"#md-write-holder"
tabindex=
"-1"
@
click.prevent=
"toggleMarkdownPreview($event)"
>
Write
</a>
</li>
<li
:class=
"
{ active: previewMarkdown }">
<a
href=
"#md-preview-holder"
tabindex=
"-1"
@
click.prevent=
"toggleMarkdownPreview($event)"
>
Preview
</a>
</li>
<li
class=
"pull-right"
>
<div
class=
"toolbar-group"
>
<toolbar-button
tag=
"**"
button-title=
"Add bold text"
icon=
"bold"
/>
<toolbar-button
tag=
"*"
button-title=
"Add italic text"
icon=
"italic"
/>
<toolbar-button
tag=
"> "
:prepend=
"true"
button-title=
"Insert a quote"
icon=
"quote-right"
/>
<toolbar-button
tag=
"`"
tag-block=
"```"
button-title=
"Insert code"
icon=
"code"
/>
<toolbar-button
tag=
"* "
:prepend=
"true"
button-title=
"Add a bullet list"
icon=
"list-ul"
/>
<toolbar-button
tag=
"1. "
:prepend=
"true"
button-title=
"Add a numbered list"
icon=
"list-ol"
/>
<toolbar-button
tag=
"* [ ] "
:prepend=
"true"
button-title=
"Add a task list"
icon=
"check-square-o"
/>
</div>
<div
class=
"toolbar-group"
>
<button
aria-label=
"Go full screen"
class=
"toolbar-btn js-zen-enter"
data-container=
"body"
tabindex=
"-1"
title=
"Go full screen"
type=
"button"
ref=
"tooltip"
>
<i
aria-hidden=
"true"
class=
"fa fa-arrows-alt fa-fw"
>
</i>
</button>
</div>
</li>
</ul>
</div>
</
template
>
app/assets/javascripts/vue_shared/components/markdown/toolbar.vue
0 → 100644
View file @
882a591d
<
script
>
export
default
{
props
:
{
markdownDocs
:
{
type
:
String
,
required
:
true
,
},
},
};
</
script
>
<
template
>
<div
class=
"comment-toolbar clearfix"
>
<div
class=
"toolbar-text"
>
<a
:href=
"markdownDocs"
target=
"_blank"
tabindex=
"-1"
>
Markdown is supported
</a>
</div>
<button
class=
"toolbar-button markdown-selector"
type=
"button"
tabindex=
"-1"
>
<i
class=
"fa fa-file-image-o toolbar-button-icon"
aria-hidden=
"true"
>
</i>
Attach a file
</button>
</div>
</
template
>
app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue
0 → 100644
View file @
882a591d
<
script
>
import
tooltipMixin
from
'
../../mixins/tooltip
'
;
export
default
{
mixins
:
[
tooltipMixin
,
],
props
:
{
buttonTitle
:
{
type
:
String
,
required
:
true
,
},
icon
:
{
type
:
String
,
required
:
true
,
},
tag
:
{
type
:
String
,
required
:
true
,
},
tagBlock
:
{
type
:
String
,
required
:
false
,
default
:
''
,
},
prepend
:
{
type
:
Boolean
,
required
:
false
,
default
:
false
,
},
},
computed
:
{
iconClass
()
{
return
`fa-
${
this
.
icon
}
`
;
},
},
};
</
script
>
<
template
>
<button
type=
"button"
class=
"toolbar-btn js-md hidden-xs"
tabindex=
"-1"
ref=
"tooltip"
data-container=
"body"
:data-md-tag=
"tag"
:data-md-block=
"tagBlock"
:data-md-prepend=
"prepend"
:title=
"buttonTitle"
:aria-label=
"buttonTitle"
>
<i
aria-hidden=
"true"
class=
"fa fa-fw"
:class=
"iconClass"
>
</i>
</button>
</
template
>
app/assets/javascripts/vue_shared/mixins/tooltip.js
View file @
882a591d
...
@@ -6,4 +6,8 @@ export default {
...
@@ -6,4 +6,8 @@ export default {
updated
()
{
updated
()
{
$
(
this
.
$refs
.
tooltip
).
tooltip
(
'
fixTitle
'
);
$
(
this
.
$refs
.
tooltip
).
tooltip
(
'
fixTitle
'
);
},
},
beforeDestroy
()
{
$
(
this
.
$refs
.
tooltip
).
tooltip
(
'
destroy
'
);
},
};
};
app/controllers/concerns/issuable_actions.rb
View file @
882a591d
...
@@ -14,7 +14,16 @@ module IssuableActions
...
@@ -14,7 +14,16 @@ module IssuableActions
name
=
issuable
.
human_class_name
name
=
issuable
.
human_class_name
flash
[
:notice
]
=
"The
#{
name
}
was successfully deleted."
flash
[
:notice
]
=
"The
#{
name
}
was successfully deleted."
redirect_to
polymorphic_path
([
@project
.
namespace
.
becomes
(
Namespace
),
@project
,
issuable
.
class
])
index_path
=
polymorphic_path
([
@project
.
namespace
.
becomes
(
Namespace
),
@project
,
issuable
.
class
])
respond_to
do
|
format
|
format
.
html
{
redirect_to
index_path
}
format
.
json
do
render
json:
{
web_url:
index_path
}
end
end
end
end
def
bulk_update
def
bulk_update
...
...
app/controllers/projects/issues_controller.rb
View file @
882a591d
...
@@ -148,10 +148,7 @@ class Projects::IssuesController < Projects::ApplicationController
...
@@ -148,10 +148,7 @@ class Projects::IssuesController < Projects::ApplicationController
format
.
json
do
format
.
json
do
if
@issue
.
valid?
if
@issue
.
valid?
render
json:
@issue
.
to_json
(
methods:
[
:task_status
,
:task_status_short
],
render
json:
IssueSerializer
.
new
.
represent
(
@issue
)
include:
{
milestone:
{},
assignees:
{
only:
[
:id
,
:name
,
:username
],
methods:
[
:avatar_url
]
},
labels:
{
methods: :text_color
}
})
else
else
render
json:
{
errors:
@issue
.
errors
.
full_messages
},
status: :unprocessable_entity
render
json:
{
errors:
@issue
.
errors
.
full_messages
},
status: :unprocessable_entity
end
end
...
...
app/helpers/issuables_helper.rb
View file @
882a591d
...
@@ -199,6 +199,27 @@ module IssuablesHelper
...
@@ -199,6 +199,27 @@ module IssuablesHelper
issuable_filter_params
.
any?
{
|
k
|
params
.
key?
(
k
)
}
issuable_filter_params
.
any?
{
|
k
|
params
.
key?
(
k
)
}
end
end
def
issuable_initial_data
(
issuable
)
{
endpoint:
namespace_project_issue_path
(
@project
.
namespace
,
@project
,
issuable
),
canUpdate:
can?
(
current_user
,
:update_issue
,
issuable
),
canDestroy:
can?
(
current_user
,
:destroy_issue
,
issuable
),
canMove:
current_user
?
issuable
.
can_move?
(
current_user
)
:
false
,
issuableRef:
issuable
.
to_reference
,
isConfidential:
issuable
.
confidential
,
markdownPreviewUrl:
preview_markdown_path
(
@project
),
markdownDocs:
help_page_path
(
'user/markdown'
),
projectsAutocompleteUrl:
autocomplete_projects_path
(
project_id:
@project
.
id
),
issuableTemplates:
issuable_templates
(
issuable
),
projectPath:
ref_project
.
path
,
projectNamespace:
ref_project
.
namespace
.
full_path
,
initialTitleHtml:
markdown_field
(
issuable
,
:title
),
initialTitleText:
issuable
.
title
,
initialDescriptionHtml:
markdown_field
(
issuable
,
:description
),
initialDescriptionText:
issuable
.
description
}.
to_json
end
private
private
def
sidebar_gutter_collapsed?
def
sidebar_gutter_collapsed?
...
...
app/serializers/issue_entity.rb
View file @
882a591d
class
IssueEntity
<
IssuableEntity
class
IssueEntity
<
IssuableEntity
include
RequestAwareEntity
expose
:branch_name
expose
:branch_name
expose
:confidential
expose
:confidential
expose
:assignees
,
using:
API
::
Entities
::
UserBasic
expose
:assignees
,
using:
API
::
Entities
::
UserBasic
...
@@ -7,4 +9,8 @@ class IssueEntity < IssuableEntity
...
@@ -7,4 +9,8 @@ class IssueEntity < IssuableEntity
expose
:project_id
expose
:project_id
expose
:milestone
,
using:
API
::
Entities
::
Milestone
expose
:milestone
,
using:
API
::
Entities
::
Milestone
expose
:labels
,
using:
LabelEntity
expose
:labels
,
using:
LabelEntity
expose
:web_url
do
|
issue
|
namespace_project_issue_path
(
issue
.
project
.
namespace
,
issue
.
project
,
issue
)
end
end
end
app/views/projects/issues/show.html.haml
View file @
882a591d
...
@@ -31,7 +31,7 @@
...
@@ -31,7 +31,7 @@
%ul
%ul
-
if
can_update_issue
-
if
can_update_issue
%li
%li
=
link_to
'Edit'
,
edit_namespace_project_issue_path
(
@project
.
namespace
,
@project
,
@issue
)
=
link_to
'Edit'
,
edit_namespace_project_issue_path
(
@project
.
namespace
,
@project
,
@issue
)
,
class:
'issuable-edit'
%li
%li
=
link_to
'Close issue'
,
issue_path
(
@issue
,
issue:
{
state_event: :close
},
format:
'json'
),
class:
"btn-close
#{
issue_button_visibility
(
@issue
,
true
)
}
"
,
title:
'Close issue'
=
link_to
'Close issue'
,
issue_path
(
@issue
,
issue:
{
state_event: :close
},
format:
'json'
),
class:
"btn-close
#{
issue_button_visibility
(
@issue
,
true
)
}
"
,
title:
'Close issue'
%li
%li
...
@@ -55,10 +55,8 @@
...
@@ -55,10 +55,8 @@
.issue-details.issuable-details
.issue-details.issuable-details
.detail-page-description.content-block
.detail-page-description.content-block
#js-issuable-app
{
"data"
=>
{
"endpoint"
=>
realtime_changes_namespace_project_issue_path
(
@project
.
namespace
,
@project
,
@issue
),
%script
#js-issuable-app-initial-data
{
type:
"application/json"
}=
issuable_initial_data
(
@issue
)
"can-update"
=>
can?
(
current_user
,
:update_issue
,
@issue
).
to_s
,
#js-issuable-app
"issuable-ref"
=>
@issue
.
to_reference
,
}
}
%h2
.title
=
markdown_field
(
@issue
,
:title
)
%h2
.title
=
markdown_field
(
@issue
,
:title
)
-
if
@issue
.
description
.
present?
-
if
@issue
.
description
.
present?
.description
{
class:
can?
(
current_user
,
:update_issue
,
@issue
)
?
'js-task-list-container'
:
''
}
.description
{
class:
can?
(
current_user
,
:update_issue
,
@issue
)
?
'js-task-list-container'
:
''
}
...
...
changelogs/unreleased/issue-edit-inline.yml
0 → 100644
View file @
882a591d
---
title
:
Enables inline editing for an issues title & description
merge_request
:
author
:
features/steps/shared/markdown.rb
View file @
882a591d
...
@@ -30,7 +30,7 @@ module SharedMarkdown
...
@@ -30,7 +30,7 @@ module SharedMarkdown
end
end
step
'I should see the Markdown write tab'
do
step
'I should see the Markdown write tab'
do
expect
(
fi
nd
(
'.gfm-form'
)).
to
have_css
(
'.js-md-write-button
'
,
visible:
true
)
expect
(
fi
rst
(
'.gfm-form'
)).
to
have_link
(
'Write
'
,
visible:
true
)
end
end
step
'I should see the Markdown preview'
do
step
'I should see the Markdown preview'
do
...
@@ -49,9 +49,9 @@ module SharedMarkdown
...
@@ -49,9 +49,9 @@ module SharedMarkdown
end
end
step
'I preview a description text like "Bug fixed :smile:"'
do
step
'I preview a description text like "Bug fixed :smile:"'
do
page
.
within
(
'.gfm-form'
)
do
page
.
within
(
first
(
'.gfm-form'
)
)
do
fill_in
'Description'
,
with:
'Bug fixed :smile:'
fill_in
'Description'
,
with:
'Bug fixed :smile:'
find
(
'.js-md-preview-button'
).
click
click_link
'Preview'
end
end
end
end
...
...
spec/controllers/projects/issues_controller_spec.rb
View file @
882a591d
...
@@ -204,7 +204,7 @@ describe Projects::IssuesController do
...
@@ -204,7 +204,7 @@ describe Projects::IssuesController do
body
=
JSON
.
parse
(
response
.
body
)
body
=
JSON
.
parse
(
response
.
body
)
expect
(
body
[
'assignees'
].
first
.
keys
)
expect
(
body
[
'assignees'
].
first
.
keys
)
.
to
match_array
(
%w(id name username avatar_url)
)
.
to
match_array
(
%w(id name username avatar_url
state web_url
)
)
end
end
end
end
...
...
spec/features/issues/move_spec.rb
View file @
882a591d
...
@@ -38,9 +38,11 @@ feature 'issue move to another project' do
...
@@ -38,9 +38,11 @@ feature 'issue move to another project' do
end
end
scenario
'moving issue to another project'
,
js:
true
do
scenario
'moving issue to another project'
,
js:
true
do
find
(
'#
move_to_project_id
'
,
visible:
false
).
set
(
new_project
.
id
)
find
(
'#
issuable-move
'
,
visible:
false
).
set
(
new_project
.
id
)
click_button
(
'Save changes'
)
click_button
(
'Save changes'
)
wait_for_requests
expect
(
current_url
).
to
include
project_path
(
new_project
)
expect
(
current_url
).
to
include
project_path
(
new_project
)
expect
(
page
).
to
have_content
(
"Text with
#{
cross_reference
}#{
mr
.
to_reference
}
"
)
expect
(
page
).
to
have_content
(
"Text with
#{
cross_reference
}#{
mr
.
to_reference
}
"
)
...
@@ -51,7 +53,7 @@ feature 'issue move to another project' do
...
@@ -51,7 +53,7 @@ feature 'issue move to another project' do
scenario
'searching project dropdown'
,
js:
true
do
scenario
'searching project dropdown'
,
js:
true
do
new_project_search
.
team
<<
[
user
,
:reporter
]
new_project_search
.
team
<<
[
user
,
:reporter
]
page
.
within
'.
js-move-dropdow
n'
do
page
.
within
'.
detail-page-descriptio
n'
do
first
(
'.select2-choice'
).
click
first
(
'.select2-choice'
).
click
end
end
...
@@ -69,7 +71,7 @@ feature 'issue move to another project' do
...
@@ -69,7 +71,7 @@ feature 'issue move to another project' do
background
{
another_project
.
team
<<
[
user
,
:guest
]
}
background
{
another_project
.
team
<<
[
user
,
:guest
]
}
scenario
'browsing projects in projects select'
do
scenario
'browsing projects in projects select'
do
click_link
'
Selec
t project'
click_link
'
Move to a differen
t project'
page
.
within
'.select2-results'
do
page
.
within
'.select2-results'
do
expect
(
page
).
to
have_content
'No project'
expect
(
page
).
to
have_content
'No project'
...
...
spec/javascripts/issue_show/components/app_spec.js
View file @
882a591d
...
@@ -2,6 +2,7 @@ import Vue from 'vue';
...
@@ -2,6 +2,7 @@ import Vue from 'vue';
import
'
~/render_math
'
;
import
'
~/render_math
'
;
import
'
~/render_gfm
'
;
import
'
~/render_gfm
'
;
import
issuableApp
from
'
~/issue_show/components/app.vue
'
;
import
issuableApp
from
'
~/issue_show/components/app.vue
'
;
import
eventHub
from
'
~/issue_show/event_hub
'
;
import
issueShowData
from
'
../mock_data
'
;
import
issueShowData
from
'
../mock_data
'
;
const
issueShowInterceptor
=
data
=>
(
request
,
next
)
=>
{
const
issueShowInterceptor
=
data
=>
(
request
,
next
)
=>
{
...
@@ -22,14 +23,25 @@ describe('Issuable output', () => {
...
@@ -22,14 +23,25 @@ describe('Issuable output', () => {
const
IssuableDescriptionComponent
=
Vue
.
extend
(
issuableApp
);
const
IssuableDescriptionComponent
=
Vue
.
extend
(
issuableApp
);
Vue
.
http
.
interceptors
.
push
(
issueShowInterceptor
(
issueShowData
.
initialRequest
));
Vue
.
http
.
interceptors
.
push
(
issueShowInterceptor
(
issueShowData
.
initialRequest
));
spyOn
(
eventHub
,
'
$emit
'
);
vm
=
new
IssuableDescriptionComponent
({
vm
=
new
IssuableDescriptionComponent
({
propsData
:
{
propsData
:
{
canUpdate
:
true
,
canUpdate
:
true
,
canDestroy
:
true
,
canMove
:
true
,
endpoint
:
'
/gitlab-org/gitlab-shell/issues/9/realtime_changes
'
,
endpoint
:
'
/gitlab-org/gitlab-shell/issues/9/realtime_changes
'
,
issuableRef
:
'
#1
'
,
issuableRef
:
'
#1
'
,
initialTitle
:
''
,
initialTitleHtml
:
''
,
initialTitleText
:
''
,
initialDescriptionHtml
:
''
,
initialDescriptionHtml
:
''
,
initialDescriptionText
:
''
,
initialDescriptionText
:
''
,
markdownPreviewUrl
:
'
/
'
,
markdownDocs
:
'
/
'
,
projectsAutocompleteUrl
:
'
/
'
,
isConfidential
:
false
,
projectNamespace
:
'
/
'
,
projectPath
:
'
/
'
,
},
},
}).
$mount
();
}).
$mount
();
});
});
...
@@ -57,4 +69,296 @@ describe('Issuable output', () => {
...
@@ -57,4 +69,296 @@ describe('Issuable output', () => {
});
});
});
});
});
});
it
(
'
shows actions if permissions are correct
'
,
(
done
)
=>
{
vm
.
showForm
=
true
;
Vue
.
nextTick
(()
=>
{
expect
(
vm
.
$el
.
querySelector
(
'
.btn
'
),
).
not
.
toBeNull
();
done
();
});
});
it
(
'
does not show actions if permissions are incorrect
'
,
(
done
)
=>
{
vm
.
showForm
=
true
;
vm
.
canUpdate
=
false
;
Vue
.
nextTick
(()
=>
{
expect
(
vm
.
$el
.
querySelector
(
'
.btn
'
),
).
toBeNull
();
done
();
});
});
it
(
'
does not update formState if form is already open
'
,
(
done
)
=>
{
vm
.
openForm
();
vm
.
state
.
titleText
=
'
testing 123
'
;
vm
.
openForm
();
Vue
.
nextTick
(()
=>
{
expect
(
vm
.
store
.
formState
.
title
,
).
not
.
toBe
(
'
testing 123
'
);
done
();
});
});
describe
(
'
updateIssuable
'
,
()
=>
{
it
(
'
fetches new data after update
'
,
(
done
)
=>
{
spyOn
(
vm
.
service
,
'
getData
'
);
spyOn
(
vm
.
service
,
'
updateIssuable
'
).
and
.
callFake
(()
=>
new
Promise
((
resolve
)
=>
{
resolve
({
json
()
{
return
{
confidential
:
false
,
web_url
:
location
.
pathname
,
};
},
});
}));
vm
.
updateIssuable
();
setTimeout
(()
=>
{
expect
(
vm
.
service
.
getData
,
).
toHaveBeenCalled
();
done
();
});
});
it
(
'
reloads the page if the confidential status has changed
'
,
(
done
)
=>
{
spyOn
(
gl
.
utils
,
'
visitUrl
'
);
spyOn
(
vm
.
service
,
'
updateIssuable
'
).
and
.
callFake
(()
=>
new
Promise
((
resolve
)
=>
{
resolve
({
json
()
{
return
{
confidential
:
true
,
web_url
:
location
.
pathname
,
};
},
});
}));
vm
.
updateIssuable
();
setTimeout
(()
=>
{
expect
(
gl
.
utils
.
visitUrl
,
).
toHaveBeenCalledWith
(
location
.
pathname
);
done
();
});
});
it
(
'
correctly updates issuable data
'
,
(
done
)
=>
{
spyOn
(
vm
.
service
,
'
updateIssuable
'
).
and
.
callFake
(()
=>
new
Promise
((
resolve
)
=>
{
resolve
();
}));
vm
.
updateIssuable
();
setTimeout
(()
=>
{
expect
(
vm
.
service
.
updateIssuable
,
).
toHaveBeenCalledWith
(
vm
.
formState
);
expect
(
eventHub
.
$emit
,
).
toHaveBeenCalledWith
(
'
close.form
'
);
done
();
});
});
it
(
'
does not redirect if issue has not moved
'
,
(
done
)
=>
{
spyOn
(
gl
.
utils
,
'
visitUrl
'
);
spyOn
(
vm
.
service
,
'
updateIssuable
'
).
and
.
callFake
(()
=>
new
Promise
((
resolve
)
=>
{
resolve
({
json
()
{
return
{
web_url
:
location
.
pathname
,
confidential
:
vm
.
isConfidential
,
};
},
});
}));
vm
.
updateIssuable
();
setTimeout
(()
=>
{
expect
(
gl
.
utils
.
visitUrl
,
).
not
.
toHaveBeenCalled
();
done
();
});
});
it
(
'
redirects if issue is moved
'
,
(
done
)
=>
{
spyOn
(
gl
.
utils
,
'
visitUrl
'
);
spyOn
(
vm
.
service
,
'
updateIssuable
'
).
and
.
callFake
(()
=>
new
Promise
((
resolve
)
=>
{
resolve
({
json
()
{
return
{
web_url
:
'
/testing-issue-move
'
,
confidential
:
vm
.
isConfidential
,
};
},
});
}));
vm
.
updateIssuable
();
setTimeout
(()
=>
{
expect
(
gl
.
utils
.
visitUrl
,
).
toHaveBeenCalledWith
(
'
/testing-issue-move
'
);
done
();
});
});
it
(
'
does not update issuable if project move confirm is false
'
,
(
done
)
=>
{
spyOn
(
window
,
'
confirm
'
).
and
.
returnValue
(
false
);
spyOn
(
vm
.
service
,
'
updateIssuable
'
);
vm
.
store
.
formState
.
move_to_project_id
=
1
;
vm
.
updateIssuable
();
setTimeout
(()
=>
{
expect
(
vm
.
service
.
updateIssuable
,
).
not
.
toHaveBeenCalled
();
done
();
});
});
it
(
'
closes form on error
'
,
(
done
)
=>
{
spyOn
(
window
,
'
Flash
'
).
and
.
callThrough
();
spyOn
(
vm
.
service
,
'
updateIssuable
'
).
and
.
callFake
(()
=>
new
Promise
((
resolve
,
reject
)
=>
{
reject
();
}));
vm
.
updateIssuable
();
setTimeout
(()
=>
{
expect
(
eventHub
.
$emit
,
).
toHaveBeenCalledWith
(
'
close.form
'
);
expect
(
window
.
Flash
,
).
toHaveBeenCalledWith
(
'
Error updating issue
'
);
done
();
});
});
});
describe
(
'
deleteIssuable
'
,
()
=>
{
it
(
'
changes URL when deleted
'
,
(
done
)
=>
{
spyOn
(
gl
.
utils
,
'
visitUrl
'
);
spyOn
(
vm
.
service
,
'
deleteIssuable
'
).
and
.
callFake
(()
=>
new
Promise
((
resolve
)
=>
{
resolve
({
json
()
{
return
{
web_url
:
'
/test
'
};
},
});
}));
vm
.
deleteIssuable
();
setTimeout
(()
=>
{
expect
(
gl
.
utils
.
visitUrl
,
).
toHaveBeenCalledWith
(
'
/test
'
);
done
();
});
});
it
(
'
stops polling when deleting
'
,
(
done
)
=>
{
spyOn
(
gl
.
utils
,
'
visitUrl
'
);
spyOn
(
vm
.
poll
,
'
stop
'
);
spyOn
(
vm
.
service
,
'
deleteIssuable
'
).
and
.
callFake
(()
=>
new
Promise
((
resolve
)
=>
{
resolve
({
json
()
{
return
{
web_url
:
'
/test
'
};
},
});
}));
vm
.
deleteIssuable
();
setTimeout
(()
=>
{
expect
(
vm
.
poll
.
stop
,
).
toHaveBeenCalledWith
();
done
();
});
});
it
(
'
closes form on error
'
,
(
done
)
=>
{
spyOn
(
window
,
'
Flash
'
).
and
.
callThrough
();
spyOn
(
vm
.
service
,
'
deleteIssuable
'
).
and
.
callFake
(()
=>
new
Promise
((
resolve
,
reject
)
=>
{
reject
();
}));
vm
.
deleteIssuable
();
setTimeout
(()
=>
{
expect
(
eventHub
.
$emit
,
).
toHaveBeenCalledWith
(
'
close.form
'
);
expect
(
window
.
Flash
,
).
toHaveBeenCalledWith
(
'
Error deleting issue
'
);
done
();
});
});
});
describe
(
'
open form
'
,
()
=>
{
it
(
'
shows locked warning if form is open & data is different
'
,
(
done
)
=>
{
Vue
.
http
.
interceptors
.
push
(
issueShowInterceptor
(
issueShowData
.
initialRequest
));
Vue
.
nextTick
()
.
then
(()
=>
new
Promise
((
resolve
)
=>
{
setTimeout
(
resolve
);
}))
.
then
(()
=>
{
vm
.
openForm
();
Vue
.
http
.
interceptors
.
push
(
issueShowInterceptor
(
issueShowData
.
secondRequest
));
return
new
Promise
((
resolve
)
=>
{
setTimeout
(
resolve
);
});
})
.
then
(()
=>
{
expect
(
vm
.
formState
.
lockedWarningVisible
,
).
toBeTruthy
();
expect
(
vm
.
$el
.
querySelector
(
'
.alert
'
),
).
not
.
toBeNull
();
done
();
})
.
catch
(
done
.
fail
);
});
});
});
});
spec/javascripts/issue_show/components/edit_actions_spec.js
0 → 100644
View file @
882a591d
import
Vue
from
'
vue
'
;
import
editActions
from
'
~/issue_show/components/edit_actions.vue
'
;
import
eventHub
from
'
~/issue_show/event_hub
'
;
import
Store
from
'
~/issue_show/stores
'
;
describe
(
'
Edit Actions components
'
,
()
=>
{
let
vm
;
beforeEach
((
done
)
=>
{
const
Component
=
Vue
.
extend
(
editActions
);
const
store
=
new
Store
({
titleHtml
:
''
,
descriptionHtml
:
''
,
issuableRef
:
''
,
});
store
.
formState
.
title
=
'
test
'
;
spyOn
(
eventHub
,
'
$emit
'
);
vm
=
new
Component
({
propsData
:
{
canDestroy
:
true
,
formState
:
store
.
formState
,
},
}).
$mount
();
Vue
.
nextTick
(
done
);
});
it
(
'
renders all buttons as enabled
'
,
()
=>
{
expect
(
vm
.
$el
.
querySelectorAll
(
'
.disabled
'
).
length
,
).
toBe
(
0
);
expect
(
vm
.
$el
.
querySelectorAll
(
'
[disabled]
'
).
length
,
).
toBe
(
0
);
});
it
(
'
does not render delete button if canUpdate is false
'
,
(
done
)
=>
{
vm
.
canDestroy
=
false
;
Vue
.
nextTick
(()
=>
{
expect
(
vm
.
$el
.
querySelector
(
'
.btn-danger
'
),
).
toBeNull
();
done
();
});
});
it
(
'
disables submit button when title is blank
'
,
(
done
)
=>
{
vm
.
formState
.
title
=
''
;
Vue
.
nextTick
(()
=>
{
expect
(
vm
.
$el
.
querySelector
(
'
.btn-save
'
).
getAttribute
(
'
disabled
'
),
).
toBe
(
'
disabled
'
);
done
();
});
});
describe
(
'
updateIssuable
'
,
()
=>
{
it
(
'
sends update.issauble event when clicking save button
'
,
()
=>
{
vm
.
$el
.
querySelector
(
'
.btn-save
'
).
click
();
expect
(
eventHub
.
$emit
,
).
toHaveBeenCalledWith
(
'
update.issuable
'
);
});
it
(
'
shows loading icon after clicking save button
'
,
(
done
)
=>
{
vm
.
$el
.
querySelector
(
'
.btn-save
'
).
click
();
Vue
.
nextTick
(()
=>
{
expect
(
vm
.
$el
.
querySelector
(
'
.btn-save .fa
'
),
).
not
.
toBeNull
();
done
();
});
});
it
(
'
disabled button after clicking save button
'
,
(
done
)
=>
{
vm
.
$el
.
querySelector
(
'
.btn-save
'
).
click
();
Vue
.
nextTick
(()
=>
{
expect
(
vm
.
$el
.
querySelector
(
'
.btn-save
'
).
getAttribute
(
'
disabled
'
),
).
toBe
(
'
disabled
'
);
done
();
});
});
});
describe
(
'
closeForm
'
,
()
=>
{
it
(
'
emits close.form when clicking cancel
'
,
()
=>
{
vm
.
$el
.
querySelector
(
'
.btn-default
'
).
click
();
expect
(
eventHub
.
$emit
,
).
toHaveBeenCalledWith
(
'
close.form
'
);
});
});
describe
(
'
deleteIssuable
'
,
()
=>
{
it
(
'
sends delete.issuable event when clicking save button
'
,
()
=>
{
spyOn
(
window
,
'
confirm
'
).
and
.
returnValue
(
true
);
vm
.
$el
.
querySelector
(
'
.btn-danger
'
).
click
();
expect
(
eventHub
.
$emit
,
).
toHaveBeenCalledWith
(
'
delete.issuable
'
);
});
it
(
'
shows loading icon after clicking delete button
'
,
(
done
)
=>
{
spyOn
(
window
,
'
confirm
'
).
and
.
returnValue
(
true
);
vm
.
$el
.
querySelector
(
'
.btn-danger
'
).
click
();
Vue
.
nextTick
(()
=>
{
expect
(
vm
.
$el
.
querySelector
(
'
.btn-danger .fa
'
),
).
not
.
toBeNull
();
done
();
});
});
it
(
'
does no actions when confirm is false
'
,
(
done
)
=>
{
spyOn
(
window
,
'
confirm
'
).
and
.
returnValue
(
false
);
vm
.
$el
.
querySelector
(
'
.btn-danger
'
).
click
();
Vue
.
nextTick
(()
=>
{
expect
(
eventHub
.
$emit
,
).
not
.
toHaveBeenCalledWith
(
'
delete.issuable
'
);
expect
(
vm
.
$el
.
querySelector
(
'
.btn-danger .fa
'
),
).
toBeNull
();
done
();
});
});
});
});
spec/javascripts/issue_show/components/fields/description_spec.js
0 → 100644
View file @
882a591d
import
Vue
from
'
vue
'
;
import
Store
from
'
~/issue_show/stores
'
;
import
descriptionField
from
'
~/issue_show/components/fields/description.vue
'
;
describe
(
'
Description field component
'
,
()
=>
{
let
vm
;
let
store
;
beforeEach
((
done
)
=>
{
const
Component
=
Vue
.
extend
(
descriptionField
);
const
el
=
document
.
createElement
(
'
div
'
);
store
=
new
Store
({
titleHtml
:
''
,
descriptionHtml
:
''
,
issuableRef
:
''
,
});
store
.
formState
.
description
=
'
test
'
;
document
.
body
.
appendChild
(
el
);
vm
=
new
Component
({
el
,
propsData
:
{
markdownPreviewUrl
:
'
/
'
,
markdownDocs
:
'
/
'
,
formState
:
store
.
formState
,
},
}).
$mount
();
Vue
.
nextTick
(
done
);
});
it
(
'
renders markdown field with description
'
,
()
=>
{
expect
(
vm
.
$el
.
querySelector
(
'
.md-area textarea
'
).
value
,
).
toBe
(
'
test
'
);
});
it
(
'
renders markdown field with a markdown description
'
,
(
done
)
=>
{
store
.
formState
.
description
=
'
**test**
'
;
Vue
.
nextTick
(()
=>
{
expect
(
vm
.
$el
.
querySelector
(
'
.md-area textarea
'
).
value
,
).
toBe
(
'
**test**
'
);
done
();
});
});
it
(
'
focuses field when mounted
'
,
()
=>
{
expect
(
document
.
activeElement
,
).
toBe
(
vm
.
$refs
.
textarea
);
});
});
spec/javascripts/issue_show/components/fields/description_template_spec.js
0 → 100644
View file @
882a591d
import
Vue
from
'
vue
'
;
import
descriptionTemplate
from
'
~/issue_show/components/fields/description_template.vue
'
;
import
'
~/templates/issuable_template_selector
'
;
import
'
~/templates/issuable_template_selectors
'
;
describe
(
'
Issue description template component
'
,
()
=>
{
let
vm
;
let
formState
;
beforeEach
((
done
)
=>
{
const
Component
=
Vue
.
extend
(
descriptionTemplate
);
formState
=
{
description
:
'
test
'
,
};
vm
=
new
Component
({
propsData
:
{
formState
,
issuableTemplates
:
[{
name
:
'
test
'
}],
projectPath
:
'
/
'
,
projectNamespace
:
'
/
'
,
},
}).
$mount
();
Vue
.
nextTick
(
done
);
});
it
(
'
renders templates as JSON array in data attribute
'
,
()
=>
{
expect
(
vm
.
$el
.
querySelector
(
'
.js-issuable-selector
'
).
getAttribute
(
'
data-data
'
),
).
toBe
(
'
[{"name":"test"}]
'
);
});
it
(
'
updates formState when changing template
'
,
()
=>
{
vm
.
issuableTemplate
.
editor
.
setValue
(
'
test new template
'
);
expect
(
formState
.
description
,
).
toBe
(
'
test new template
'
);
});
it
(
'
returns formState description with editor getValue
'
,
()
=>
{
formState
.
description
=
'
testing new template
'
;
expect
(
vm
.
issuableTemplate
.
editor
.
getValue
(),
).
toBe
(
'
testing new template
'
);
});
});
spec/javascripts/issue_show/components/fields/project_move_spec.js
0 → 100644
View file @
882a591d
import
Vue
from
'
vue
'
;
import
projectMove
from
'
~/issue_show/components/fields/project_move.vue
'
;
describe
(
'
Project move field component
'
,
()
=>
{
let
vm
;
let
formState
;
beforeEach
((
done
)
=>
{
const
Component
=
Vue
.
extend
(
projectMove
);
formState
=
{
move_to_project_id
:
0
,
};
vm
=
new
Component
({
propsData
:
{
formState
,
projectsAutocompleteUrl
:
'
/autocomplete
'
,
},
}).
$mount
();
Vue
.
nextTick
(
done
);
});
it
(
'
mounts select2 element
'
,
()
=>
{
expect
(
vm
.
$el
.
querySelector
(
'
.select2-container
'
),
).
not
.
toBeNull
();
});
it
(
'
updates formState on change
'
,
()
=>
{
$
(
vm
.
$refs
[
'
move-dropdown
'
]).
val
(
2
).
trigger
(
'
change
'
);
expect
(
formState
.
move_to_project_id
,
).
toBe
(
2
);
});
});
spec/javascripts/issue_show/components/fields/title_spec.js
0 → 100644
View file @
882a591d
import
Vue
from
'
vue
'
;
import
Store
from
'
~/issue_show/stores
'
;
import
titleField
from
'
~/issue_show/components/fields/title.vue
'
;
describe
(
'
Title field component
'
,
()
=>
{
let
vm
;
let
store
;
beforeEach
(()
=>
{
const
Component
=
Vue
.
extend
(
titleField
);
store
=
new
Store
({
titleHtml
:
''
,
descriptionHtml
:
''
,
issuableRef
:
''
,
});
store
.
formState
.
title
=
'
test
'
;
vm
=
new
Component
({
propsData
:
{
formState
:
store
.
formState
,
},
}).
$mount
();
});
it
(
'
renders form control with formState title
'
,
()
=>
{
expect
(
vm
.
$el
.
querySelector
(
'
.form-control
'
).
value
,
).
toBe
(
'
test
'
);
});
});
spec/javascripts/issue_show/components/form_spec.js
0 → 100644
View file @
882a591d
import
Vue
from
'
vue
'
;
import
formComponent
from
'
~/issue_show/components/form.vue
'
;
import
'
~/templates/issuable_template_selector
'
;
import
'
~/templates/issuable_template_selectors
'
;
describe
(
'
Inline edit form component
'
,
()
=>
{
let
vm
;
beforeEach
((
done
)
=>
{
const
Component
=
Vue
.
extend
(
formComponent
);
vm
=
new
Component
({
propsData
:
{
canDestroy
:
true
,
canMove
:
true
,
formState
:
{
title
:
'
b
'
,
description
:
'
a
'
,
lockedWarningVisible
:
false
,
},
markdownPreviewUrl
:
'
/
'
,
markdownDocs
:
'
/
'
,
projectsAutocompleteUrl
:
'
/
'
,
projectPath
:
'
/
'
,
projectNamespace
:
'
/
'
,
},
}).
$mount
();
Vue
.
nextTick
(
done
);
});
it
(
'
does not render template selector if no templates exist
'
,
()
=>
{
expect
(
vm
.
$el
.
querySelector
(
'
.js-issuable-selector-wrap
'
),
).
toBeNull
();
});
it
(
'
renders template selector when templates exists
'
,
(
done
)
=>
{
spyOn
(
gl
,
'
IssuableTemplateSelectors
'
);
vm
.
issuableTemplates
=
[
'
test
'
];
Vue
.
nextTick
(()
=>
{
expect
(
vm
.
$el
.
querySelector
(
'
.js-issuable-selector-wrap
'
),
).
not
.
toBeNull
();
done
();
});
});
it
(
'
hides locked warning by default
'
,
()
=>
{
expect
(
vm
.
$el
.
querySelector
(
'
.alert
'
),
).
toBeNull
();
});
it
(
'
shows locked warning if formState is different
'
,
(
done
)
=>
{
vm
.
formState
.
lockedWarningVisible
=
true
;
Vue
.
nextTick
(()
=>
{
expect
(
vm
.
$el
.
querySelector
(
'
.alert
'
),
).
not
.
toBeNull
();
done
();
});
});
});
spec/javascripts/issue_show/components/title_spec.js
View file @
882a591d
import
Vue
from
'
vue
'
;
import
Vue
from
'
vue
'
;
import
Store
from
'
~/issue_show/stores
'
;
import
titleComponent
from
'
~/issue_show/components/title.vue
'
;
import
titleComponent
from
'
~/issue_show/components/title.vue
'
;
describe
(
'
Title component
'
,
()
=>
{
describe
(
'
Title component
'
,
()
=>
{
...
@@ -6,11 +7,18 @@ describe('Title component', () => {
...
@@ -6,11 +7,18 @@ describe('Title component', () => {
beforeEach
(()
=>
{
beforeEach
(()
=>
{
const
Component
=
Vue
.
extend
(
titleComponent
);
const
Component
=
Vue
.
extend
(
titleComponent
);
const
store
=
new
Store
({
titleHtml
:
''
,
descriptionHtml
:
''
,
issuableRef
:
''
,
});
vm
=
new
Component
({
vm
=
new
Component
({
propsData
:
{
propsData
:
{
issuableRef
:
'
#1
'
,
issuableRef
:
'
#1
'
,
titleHtml
:
'
Testing <img />
'
,
titleHtml
:
'
Testing <img />
'
,
titleText
:
'
Testing
'
,
titleText
:
'
Testing
'
,
showForm
:
false
,
formState
:
store
.
formState
,
},
},
}).
$mount
();
}).
$mount
();
});
});
...
...
spec/javascripts/pipelines/graph/action_component_spec.js
View file @
882a591d
...
@@ -4,7 +4,7 @@ import actionComponent from '~/pipelines/components/graph/action_component.vue';
...
@@ -4,7 +4,7 @@ import actionComponent from '~/pipelines/components/graph/action_component.vue';
describe
(
'
pipeline graph action component
'
,
()
=>
{
describe
(
'
pipeline graph action component
'
,
()
=>
{
let
component
;
let
component
;
beforeEach
(()
=>
{
beforeEach
((
done
)
=>
{
const
ActionComponent
=
Vue
.
extend
(
actionComponent
);
const
ActionComponent
=
Vue
.
extend
(
actionComponent
);
component
=
new
ActionComponent
({
component
=
new
ActionComponent
({
propsData
:
{
propsData
:
{
...
@@ -14,6 +14,8 @@ describe('pipeline graph action component', () => {
...
@@ -14,6 +14,8 @@ describe('pipeline graph action component', () => {
actionIcon
:
'
icon_action_cancel
'
,
actionIcon
:
'
icon_action_cancel
'
,
},
},
}).
$mount
();
}).
$mount
();
Vue
.
nextTick
(
done
);
});
});
it
(
'
should render a link
'
,
()
=>
{
it
(
'
should render a link
'
,
()
=>
{
...
@@ -27,7 +29,7 @@ describe('pipeline graph action component', () => {
...
@@ -27,7 +29,7 @@ describe('pipeline graph action component', () => {
it
(
'
should update bootstrap tooltip when title changes
'
,
(
done
)
=>
{
it
(
'
should update bootstrap tooltip when title changes
'
,
(
done
)
=>
{
component
.
tooltipText
=
'
changed
'
;
component
.
tooltipText
=
'
changed
'
;
Vue
.
nextTick
(()
=>
{
setTimeout
(()
=>
{
expect
(
component
.
$el
.
getAttribute
(
'
data-original-title
'
)).
toBe
(
'
changed
'
);
expect
(
component
.
$el
.
getAttribute
(
'
data-original-title
'
)).
toBe
(
'
changed
'
);
done
();
done
();
});
});
...
...
spec/javascripts/pipelines/graph/dropdown_action_component_spec.js
View file @
882a591d
...
@@ -4,7 +4,7 @@ import dropdownActionComponent from '~/pipelines/components/graph/dropdown_actio
...
@@ -4,7 +4,7 @@ import dropdownActionComponent from '~/pipelines/components/graph/dropdown_actio
describe
(
'
action component
'
,
()
=>
{
describe
(
'
action component
'
,
()
=>
{
let
component
;
let
component
;
beforeEach
(()
=>
{
beforeEach
((
done
)
=>
{
const
DropdownActionComponent
=
Vue
.
extend
(
dropdownActionComponent
);
const
DropdownActionComponent
=
Vue
.
extend
(
dropdownActionComponent
);
component
=
new
DropdownActionComponent
({
component
=
new
DropdownActionComponent
({
propsData
:
{
propsData
:
{
...
@@ -14,6 +14,8 @@ describe('action component', () => {
...
@@ -14,6 +14,8 @@ describe('action component', () => {
actionIcon
:
'
icon_action_cancel
'
,
actionIcon
:
'
icon_action_cancel
'
,
},
},
}).
$mount
();
}).
$mount
();
Vue
.
nextTick
(
done
);
});
});
it
(
'
should render a link
'
,
()
=>
{
it
(
'
should render a link
'
,
()
=>
{
...
...
spec/javascripts/pipelines/graph/job_component_spec.js
View file @
882a591d
...
@@ -27,26 +27,30 @@ describe('pipeline graph job component', () => {
...
@@ -27,26 +27,30 @@ describe('pipeline graph job component', () => {
});
});
describe
(
'
name with link
'
,
()
=>
{
describe
(
'
name with link
'
,
()
=>
{
it
(
'
should render the job name and status with a link
'
,
()
=>
{
it
(
'
should render the job name and status with a link
'
,
(
done
)
=>
{
const
component
=
new
JobComponent
({
const
component
=
new
JobComponent
({
propsData
:
{
propsData
:
{
job
:
mockJob
,
job
:
mockJob
,
},
},
}).
$mount
();
}).
$mount
();
const
link
=
component
.
$el
.
querySelector
(
'
a
'
);
Vue
.
nextTick
(()
=>
{
const
link
=
component
.
$el
.
querySelector
(
'
a
'
);
expect
(
link
.
getAttribute
(
'
href
'
)).
toEqual
(
mockJob
.
status
.
details_path
);
expect
(
link
.
getAttribute
(
'
href
'
)).
toEqual
(
mockJob
.
status
.
details_path
);
expect
(
expect
(
link
.
getAttribute
(
'
data-original-title
'
),
link
.
getAttribute
(
'
data-original-title
'
),
).
toEqual
(
`
${
mockJob
.
name
}
-
${
mockJob
.
status
.
label
}
`
);
).
toEqual
(
`
${
mockJob
.
name
}
-
${
mockJob
.
status
.
label
}
`
);
expect
(
component
.
$el
.
querySelector
(
'
.js-status-icon-success
'
)).
toBeDefined
();
expect
(
component
.
$el
.
querySelector
(
'
.js-status-icon-success
'
)).
toBeDefined
();
expect
(
expect
(
component
.
$el
.
querySelector
(
'
.ci-status-text
'
).
textContent
.
trim
(),
component
.
$el
.
querySelector
(
'
.ci-status-text
'
).
textContent
.
trim
(),
).
toEqual
(
mockJob
.
name
);
).
toEqual
(
mockJob
.
name
);
done
();
});
});
});
});
});
...
...
spec/javascripts/vue_shared/components/markdown/field_spec.js
0 → 100644
View file @
882a591d
import
Vue
from
'
vue
'
;
import
fieldComponent
from
'
~/vue_shared/components/markdown/field.vue
'
;
describe
(
'
Markdown field component
'
,
()
=>
{
let
vm
;
beforeEach
(()
=>
{
vm
=
new
Vue
({
render
(
createElement
)
{
return
createElement
(
fieldComponent
,
{
props
:
{
markdownPreviewUrl
:
'
/preview
'
,
markdownDocs
:
'
/docs
'
,
},
},
[
createElement
(
'
textarea
'
,
{
slot
:
'
textarea
'
,
}),
],
);
},
});
});
it
(
'
creates a new instance of GL form
'
,
(
done
)
=>
{
spyOn
(
gl
,
'
GLForm
'
);
vm
.
$mount
();
Vue
.
nextTick
(()
=>
{
expect
(
gl
.
GLForm
,
).
toHaveBeenCalled
();
done
();
});
});
describe
(
'
mounted
'
,
()
=>
{
beforeEach
((
done
)
=>
{
vm
.
$mount
();
Vue
.
nextTick
(
done
);
});
it
(
'
renders textarea inside backdrop
'
,
()
=>
{
expect
(
vm
.
$el
.
querySelector
(
'
.zen-backdrop textarea
'
),
).
not
.
toBeNull
();
});
describe
(
'
markdown preview
'
,
()
=>
{
let
previewLink
;
beforeEach
(()
=>
{
spyOn
(
Vue
.
http
,
'
post
'
).
and
.
callFake
(()
=>
new
Promise
((
resolve
)
=>
{
resolve
({
json
()
{
return
{
body
:
'
<p>markdown preview</p>
'
,
};
},
});
}));
previewLink
=
vm
.
$el
.
querySelector
(
'
.nav-links li:nth-child(2) a
'
);
});
it
(
'
sets preview link as active
'
,
(
done
)
=>
{
previewLink
.
click
();
Vue
.
nextTick
(()
=>
{
expect
(
previewLink
.
parentNode
.
classList
.
contains
(
'
active
'
),
).
toBeTruthy
();
done
();
});
});
it
(
'
shows preview loading text
'
,
(
done
)
=>
{
previewLink
.
click
();
Vue
.
nextTick
(()
=>
{
expect
(
vm
.
$el
.
querySelector
(
'
.md-preview
'
).
textContent
.
trim
(),
).
toContain
(
'
Loading...
'
);
done
();
});
});
it
(
'
renders markdown preview
'
,
(
done
)
=>
{
previewLink
.
click
();
setTimeout
(()
=>
{
expect
(
vm
.
$el
.
querySelector
(
'
.md-preview
'
).
innerHTML
,
).
toContain
(
'
<p>markdown preview</p>
'
);
done
();
});
});
it
(
'
renders GFM with jQuery
'
,
(
done
)
=>
{
spyOn
(
$
.
fn
,
'
renderGFM
'
);
previewLink
.
click
();
setTimeout
(()
=>
{
expect
(
$
.
fn
.
renderGFM
,
).
toHaveBeenCalled
();
done
();
});
});
});
});
});
spec/javascripts/vue_shared/components/markdown/header_spec.js
0 → 100644
View file @
882a591d
import
Vue
from
'
vue
'
;
import
headerComponent
from
'
~/vue_shared/components/markdown/header.vue
'
;
describe
(
'
Markdown field header component
'
,
()
=>
{
let
vm
;
beforeEach
((
done
)
=>
{
const
Component
=
Vue
.
extend
(
headerComponent
);
vm
=
new
Component
({
propsData
:
{
previewMarkdown
:
false
,
},
}).
$mount
();
Vue
.
nextTick
(
done
);
});
it
(
'
renders markdown buttons
'
,
()
=>
{
expect
(
vm
.
$el
.
querySelectorAll
(
'
.js-md
'
).
length
,
).
toBe
(
7
);
});
it
(
'
renders `write` link as active when previewMarkdown is false
'
,
()
=>
{
expect
(
vm
.
$el
.
querySelector
(
'
li:nth-child(1)
'
).
classList
.
contains
(
'
active
'
),
).
toBeTruthy
();
});
it
(
'
renders `preview` link as active when previewMarkdown is true
'
,
(
done
)
=>
{
vm
.
previewMarkdown
=
true
;
Vue
.
nextTick
(()
=>
{
expect
(
vm
.
$el
.
querySelector
(
'
li:nth-child(2)
'
).
classList
.
contains
(
'
active
'
),
).
toBeTruthy
();
done
();
});
});
it
(
'
emits toggle markdown event when clicking preview
'
,
()
=>
{
spyOn
(
vm
,
'
$emit
'
);
vm
.
$el
.
querySelector
(
'
li:nth-child(2) a
'
).
click
();
expect
(
vm
.
$emit
,
).
toHaveBeenCalledWith
(
'
toggle-markdown
'
);
});
it
(
'
blurs preview link after click
'
,
(
done
)
=>
{
const
link
=
vm
.
$el
.
querySelector
(
'
li:nth-child(2) a
'
);
spyOn
(
HTMLElement
.
prototype
,
'
blur
'
);
link
.
click
();
setTimeout
(()
=>
{
expect
(
link
.
blur
,
).
toHaveBeenCalled
();
done
();
});
});
});
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