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
20987f4f
Commit
20987f4f
authored
May 12, 2017
by
Filipa Lacerda
Browse files
Options
Browse Files
Download
Plain Diff
Merge branch 'refactor-realtime-issue' into 'master'
Refactored issue tealtime elements See merge request !11242
parents
2ac27a96
3dfce3ab
Changes
17
Hide whitespace changes
Inline
Side-by-side
Showing
17 changed files
with
543 additions
and
262 deletions
+543
-262
app/assets/javascripts/issue_show/actions/tasks.js
app/assets/javascripts/issue_show/actions/tasks.js
+0
-27
app/assets/javascripts/issue_show/components/app.vue
app/assets/javascripts/issue_show/components/app.vue
+96
-0
app/assets/javascripts/issue_show/components/description.vue
app/assets/javascripts/issue_show/components/description.vue
+105
-0
app/assets/javascripts/issue_show/components/title.vue
app/assets/javascripts/issue_show/components/title.vue
+53
-0
app/assets/javascripts/issue_show/index.js
app/assets/javascripts/issue_show/index.js
+36
-14
app/assets/javascripts/issue_show/issue_title_description.vue
...assets/javascripts/issue_show/issue_title_description.vue
+0
-180
app/assets/javascripts/issue_show/mixins/animate.js
app/assets/javascripts/issue_show/mixins/animate.js
+13
-0
app/assets/javascripts/issue_show/services/index.js
app/assets/javascripts/issue_show/services/index.js
+10
-4
app/assets/javascripts/issue_show/stores/index.js
app/assets/javascripts/issue_show/stores/index.js
+25
-0
app/assets/stylesheets/framework/mobile.scss
app/assets/stylesheets/framework/mobile.scss
+1
-1
app/controllers/projects/issues_controller.rb
app/controllers/projects/issues_controller.rb
+0
-1
app/helpers/issuables_helper.rb
app/helpers/issuables_helper.rb
+3
-5
app/views/projects/issues/show.html.haml
app/views/projects/issues/show.html.haml
+9
-4
spec/javascripts/issue_show/components/app_spec.js
spec/javascripts/issue_show/components/app_spec.js
+23
-23
spec/javascripts/issue_show/components/description_spec.js
spec/javascripts/issue_show/components/description_spec.js
+99
-0
spec/javascripts/issue_show/components/title_spec.js
spec/javascripts/issue_show/components/title_spec.js
+67
-0
spec/javascripts/issue_show/mock_data.js
spec/javascripts/issue_show/mock_data.js
+3
-3
No files found.
app/assets/javascripts/issue_show/actions/tasks.js
deleted
100644 → 0
View file @
2ac27a96
export
default
(
newStateData
,
tasks
)
=>
{
const
$tasks
=
$
(
'
#task_status
'
);
const
$tasksShort
=
$
(
'
#task_status_short
'
);
const
$issueableHeader
=
$
(
'
.issuable-header
'
);
const
tasksStates
=
{
newState
:
null
,
currentState
:
null
};
if
(
$tasks
.
length
===
0
)
{
if
(
!
(
newStateData
.
task_status
.
indexOf
(
'
0 of 0
'
)
===
0
))
{
$issueableHeader
.
append
(
`<span id="task_status">
${
newStateData
.
task_status
}
</span>`
);
}
else
{
$issueableHeader
.
append
(
'
<span id="task_status"></span>
'
);
}
}
else
{
tasksStates
.
newState
=
newStateData
.
task_status
.
indexOf
(
'
0 of 0
'
)
===
0
;
tasksStates
.
currentState
=
tasks
.
indexOf
(
'
0 of 0
'
)
===
0
;
}
if
(
$tasks
.
length
!==
0
&&
!
tasksStates
.
newState
)
{
$tasks
.
text
(
newStateData
.
task_status
);
$tasksShort
.
text
(
newStateData
.
task_status
);
}
else
if
(
tasksStates
.
currentState
)
{
$issueableHeader
.
append
(
`<span id="task_status">
${
newStateData
.
task_status
}
</span>`
);
}
else
if
(
tasksStates
.
newState
)
{
$tasks
.
remove
();
$tasksShort
.
remove
();
}
};
app/assets/javascripts/issue_show/components/app.vue
0 → 100644
View file @
20987f4f
<
script
>
import
Visibility
from
'
visibilityjs
'
;
import
Poll
from
'
../../lib/utils/poll
'
;
import
Service
from
'
../services/index
'
;
import
Store
from
'
../stores
'
;
import
titleComponent
from
'
./title.vue
'
;
import
descriptionComponent
from
'
./description.vue
'
;
export
default
{
props
:
{
endpoint
:
{
required
:
true
,
type
:
String
,
},
canUpdate
:
{
required
:
true
,
type
:
Boolean
,
},
issuableRef
:
{
type
:
String
,
required
:
true
,
},
initialTitle
:
{
type
:
String
,
required
:
true
,
},
initialDescriptionHtml
:
{
type
:
String
,
required
:
false
,
default
:
''
,
},
initialDescriptionText
:
{
type
:
String
,
required
:
false
,
default
:
''
,
},
},
data
()
{
const
store
=
new
Store
({
titleHtml
:
this
.
initialTitle
,
descriptionHtml
:
this
.
initialDescriptionHtml
,
descriptionText
:
this
.
initialDescriptionText
,
});
return
{
store
,
state
:
store
.
state
,
};
},
components
:
{
descriptionComponent
,
titleComponent
,
},
created
()
{
const
resource
=
new
Service
(
this
.
endpoint
);
const
poll
=
new
Poll
({
resource
,
method
:
'
getData
'
,
successCallback
:
(
res
)
=>
{
this
.
store
.
updateState
(
res
.
json
());
},
errorCallback
(
err
)
{
throw
new
Error
(
err
);
},
});
if
(
!
Visibility
.
hidden
())
{
poll
.
makeRequest
();
}
Visibility
.
change
(()
=>
{
if
(
!
Visibility
.
hidden
())
{
poll
.
restart
();
}
else
{
poll
.
stop
();
}
});
},
};
</
script
>
<
template
>
<div>
<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>
</
template
>
app/assets/javascripts/issue_show/components/description.vue
0 → 100644
View file @
20987f4f
<
script
>
import
animateMixin
from
'
../mixins/animate
'
;
export
default
{
mixins
:
[
animateMixin
],
props
:
{
canUpdate
:
{
type
:
Boolean
,
required
:
true
,
},
descriptionHtml
:
{
type
:
String
,
required
:
true
,
},
descriptionText
:
{
type
:
String
,
required
:
true
,
},
updatedAt
:
{
type
:
String
,
required
:
true
,
},
taskStatus
:
{
type
:
String
,
required
:
true
,
},
},
data
()
{
return
{
preAnimation
:
false
,
pulseAnimation
:
false
,
timeAgoEl
:
$
(
'
.js-issue-edited-ago
'
),
};
},
watch
:
{
descriptionHtml
()
{
this
.
animateChange
();
this
.
$nextTick
(()
=>
{
const
toolTipTime
=
gl
.
utils
.
formatDate
(
this
.
updatedAt
);
this
.
timeAgoEl
.
attr
(
'
datetime
'
,
this
.
updatedAt
)
.
attr
(
'
title
'
,
toolTipTime
)
.
tooltip
(
'
fixTitle
'
);
this
.
renderGFM
();
});
},
taskStatus
()
{
const
taskRegexMatches
=
this
.
taskStatus
.
match
(
/
(\d
+
)
of
(\d
+
)
/
);
const
$issuableHeader
=
$
(
'
.issuable-meta
'
);
const
$tasks
=
$
(
'
#task_status
'
,
$issuableHeader
);
const
$tasksShort
=
$
(
'
#task_status_short
'
,
$issuableHeader
);
if
(
taskRegexMatches
)
{
$tasks
.
text
(
this
.
taskStatus
);
$tasksShort
.
text
(
`
${
taskRegexMatches
[
1
]}
/
${
taskRegexMatches
[
2
]}
task
${
taskRegexMatches
[
2
]
>
1
?
'
s
'
:
''
}
`
);
}
else
{
$tasks
.
text
(
''
);
$tasksShort
.
text
(
''
);
}
},
},
methods
:
{
renderGFM
()
{
$
(
this
.
$refs
[
'
gfm-entry-content
'
]).
renderGFM
();
if
(
this
.
canUpdate
)
{
// eslint-disable-next-line no-new
new
gl
.
TaskList
({
dataType
:
'
issue
'
,
fieldName
:
'
description
'
,
selector
:
'
.detail-page-description
'
,
});
}
},
},
mounted
()
{
this
.
renderGFM
();
},
};
</
script
>
<
template
>
<div
class=
"description"
:class=
"
{
'js-task-list-container': canUpdate
}">
<div
class=
"wiki"
:class=
"
{
'issue-realtime-pre-pulse': preAnimation,
'issue-realtime-trigger-pulse': pulseAnimation
}"
v-html="descriptionHtml"
ref="gfm-content">
</div>
<textarea
class=
"hidden js-task-list-field"
v-if=
"descriptionText"
v-model=
"descriptionText"
>
</textarea>
</div>
</
template
>
app/assets/javascripts/issue_show/components/title.vue
0 → 100644
View file @
20987f4f
<
script
>
import
animateMixin
from
'
../mixins/animate
'
;
export
default
{
mixins
:
[
animateMixin
],
data
()
{
return
{
preAnimation
:
false
,
pulseAnimation
:
false
,
titleEl
:
document
.
querySelector
(
'
title
'
),
};
},
props
:
{
issuableRef
:
{
type
:
String
,
required
:
true
,
},
titleHtml
:
{
type
:
String
,
required
:
true
,
},
titleText
:
{
type
:
String
,
required
:
true
,
},
},
watch
:
{
titleHtml
()
{
this
.
setPageTitle
();
this
.
animateChange
();
},
},
methods
:
{
setPageTitle
()
{
const
currentPageTitleScope
=
this
.
titleEl
.
innerText
.
split
(
'
·
'
);
currentPageTitleScope
[
0
]
=
`
${
this
.
titleText
}
(
${
this
.
issuableRef
}
) `
;
this
.
titleEl
.
textContent
=
currentPageTitleScope
.
join
(
'
·
'
);
},
},
};
</
script
>
<
template
>
<h2
class=
"title"
:class=
"
{
'issue-realtime-pre-pulse': preAnimation,
'issue-realtime-trigger-pulse': pulseAnimation
}"
v-html="titleHtml"
>
</h2>
</
template
>
app/assets/javascripts/issue_show/index.js
View file @
20987f4f
import
Vue
from
'
vue
'
;
import
Vue
from
'
vue
'
;
import
IssueTitle
from
'
./issue_title_description
.vue
'
;
import
issuableApp
from
'
./components/app
.vue
'
;
import
'
../vue_shared/vue_resource_interceptor
'
;
import
'
../vue_shared/vue_resource_interceptor
'
;
(()
=>
{
document
.
addEventListener
(
'
DOMContentLoaded
'
,
()
=>
new
Vue
({
const
issueTitleData
=
document
.
querySelector
(
'
.issue-title-data
'
).
dataset
;
el
:
document
.
getElementById
(
'
js-issuable-app
'
),
const
{
canUpdateTasksClass
,
endpoint
}
=
issueTitleData
;
components
:
{
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
;
const
vm
=
new
Vue
({
return
{
el
:
'
.issue-title-entrypoint
'
,
canUpdate
:
gl
.
utils
.
convertPermissionToBoolean
(
canUpdate
),
render
:
createElement
=>
createElement
(
IssueTitle
,
{
endpoint
,
issuableRef
,
initialTitle
:
issuableTitleElement
.
innerHTML
,
initialDescriptionHtml
:
issuableDescriptionElement
?
issuableDescriptionElement
.
innerHTML
:
''
,
initialDescriptionText
:
issuableDescriptionTextarea
?
issuableDescriptionTextarea
.
textContent
:
''
,
};
},
render
(
createElement
)
{
return
createElement
(
'
issuable-app
'
,
{
props
:
{
props
:
{
canUpdateTasksClass
,
canUpdate
:
this
.
canUpdate
,
endpoint
,
endpoint
:
this
.
endpoint
,
issuableRef
:
this
.
issuableRef
,
initialTitle
:
this
.
initialTitle
,
initialDescriptionHtml
:
this
.
initialDescriptionHtml
,
initialDescriptionText
:
this
.
initialDescriptionText
,
},
},
}),
});
});
},
}));
return
vm
;
})();
app/assets/javascripts/issue_show/issue_title_description.vue
deleted
100644 → 0
View file @
2ac27a96
<
script
>
import
Visibility
from
'
visibilityjs
'
;
import
Poll
from
'
./../lib/utils/poll
'
;
import
Service
from
'
./services/index
'
;
import
tasks
from
'
./actions/tasks
'
;
export
default
{
props
:
{
endpoint
:
{
required
:
true
,
type
:
String
,
},
canUpdateTasksClass
:
{
required
:
true
,
type
:
String
,
},
},
data
()
{
const
resource
=
new
Service
(
this
.
$http
,
this
.
endpoint
);
const
poll
=
new
Poll
({
resource
,
method
:
'
getTitle
'
,
successCallback
:
(
res
)
=>
{
this
.
renderResponse
(
res
);
},
errorCallback
:
(
err
)
=>
{
throw
new
Error
(
err
);
},
});
return
{
poll
,
apiData
:
{},
tasks
:
'
0 of 0
'
,
title
:
null
,
titleText
:
''
,
titleFlag
:
{
pre
:
true
,
pulse
:
false
,
},
description
:
null
,
descriptionText
:
''
,
descriptionChange
:
false
,
descriptionFlag
:
{
pre
:
true
,
pulse
:
false
,
},
timeAgoEl
:
$
(
'
.issue_edited_ago
'
),
titleEl
:
document
.
querySelector
(
'
title
'
),
};
},
methods
:
{
updateFlag
(
key
,
toggle
)
{
this
[
key
].
pre
=
toggle
;
this
[
key
].
pulse
=
!
toggle
;
},
renderResponse
(
res
)
{
this
.
apiData
=
res
.
json
();
this
.
triggerAnimation
();
},
updateTaskHTML
()
{
tasks
(
this
.
apiData
,
this
.
tasks
);
},
elementsToVisualize
(
noTitleChange
,
noDescriptionChange
)
{
if
(
!
noTitleChange
)
{
this
.
titleText
=
this
.
apiData
.
title_text
;
this
.
updateFlag
(
'
titleFlag
'
,
true
);
}
if
(
!
noDescriptionChange
)
{
// only change to true when we need to bind TaskLists the html of description
this
.
descriptionChange
=
true
;
this
.
updateTaskHTML
();
this
.
tasks
=
this
.
apiData
.
task_status
;
this
.
updateFlag
(
'
descriptionFlag
'
,
true
);
}
},
setTabTitle
()
{
const
currentTabTitleScope
=
this
.
titleEl
.
innerText
.
split
(
'
·
'
);
currentTabTitleScope
[
0
]
=
`
${
this
.
titleText
}
(#
${
this
.
apiData
.
issue_number
}
) `
;
this
.
titleEl
.
innerText
=
currentTabTitleScope
.
join
(
'
·
'
);
},
animate
(
title
,
description
)
{
this
.
title
=
title
;
this
.
description
=
description
;
this
.
setTabTitle
();
this
.
$nextTick
(()
=>
{
this
.
updateFlag
(
'
titleFlag
'
,
false
);
this
.
updateFlag
(
'
descriptionFlag
'
,
false
);
});
},
triggerAnimation
()
{
// always reset to false before checking the change
this
.
descriptionChange
=
false
;
const
{
title
,
description
}
=
this
.
apiData
;
this
.
descriptionText
=
this
.
apiData
.
description_text
;
const
noTitleChange
=
this
.
title
===
title
;
const
noDescriptionChange
=
this
.
description
===
description
;
/**
* since opacity is changed, even if there is no diff for Vue to update
* we must check the title/description even on a 304 to ensure no visual change
*/
if
(
noTitleChange
&&
noDescriptionChange
)
return
;
this
.
elementsToVisualize
(
noTitleChange
,
noDescriptionChange
);
this
.
animate
(
title
,
description
);
},
updateEditedTimeAgo
()
{
const
toolTipTime
=
gl
.
utils
.
formatDate
(
this
.
apiData
.
updated_at
);
this
.
timeAgoEl
.
attr
(
'
datetime
'
,
this
.
apiData
.
updated_at
);
this
.
timeAgoEl
.
attr
(
'
title
'
,
toolTipTime
).
tooltip
(
'
fixTitle
'
);
},
},
created
()
{
if
(
!
Visibility
.
hidden
())
{
this
.
poll
.
makeRequest
();
}
Visibility
.
change
(()
=>
{
if
(
!
Visibility
.
hidden
())
{
this
.
poll
.
restart
();
}
else
{
this
.
poll
.
stop
();
}
});
},
updated
()
{
// if new html is injected (description changed) - bind TaskList and call renderGFM
if
(
this
.
descriptionChange
)
{
this
.
updateEditedTimeAgo
();
$
(
this
.
$refs
[
'
issue-content-container-gfm-entry
'
]).
renderGFM
();
const
tl
=
new
gl
.
TaskList
({
dataType
:
'
issue
'
,
fieldName
:
'
description
'
,
selector
:
'
.detail-page-description
'
,
});
return
tl
&&
null
;
}
return
null
;
},
};
</
script
>
<
template
>
<div>
<h2
class=
"title"
:class=
"
{ 'issue-realtime-pre-pulse': titleFlag.pre, 'issue-realtime-trigger-pulse': titleFlag.pulse }"
ref="issue-title"
v-html="title"
>
</h2>
<div
class=
"description is-task-list-enabled"
:class=
"canUpdateTasksClass"
v-if=
"description"
>
<div
class=
"wiki"
:class=
"
{ 'issue-realtime-pre-pulse': descriptionFlag.pre, 'issue-realtime-trigger-pulse': descriptionFlag.pulse }"
v-html="description"
ref="issue-content-container-gfm-entry"
>
</div>
<textarea
class=
"hidden js-task-list-field"
v-if=
"descriptionText"
>
{{
descriptionText
}}
</textarea>
</div>
</div>
</
template
>
app/assets/javascripts/issue_show/mixins/animate.js
0 → 100644
View file @
20987f4f
export
default
{
methods
:
{
animateChange
()
{
this
.
preAnimation
=
true
;
this
.
pulseAnimation
=
false
;
this
.
$nextTick
(()
=>
{
this
.
preAnimation
=
false
;
this
.
pulseAnimation
=
true
;
});
},
},
};
app/assets/javascripts/issue_show/services/index.js
View file @
20987f4f
import
Vue
from
'
vue
'
;
import
VueResource
from
'
vue-resource
'
;
Vue
.
use
(
VueResource
);
export
default
class
Service
{
export
default
class
Service
{
constructor
(
resource
,
endpoint
)
{
constructor
(
endpoint
)
{
this
.
resource
=
resource
;
this
.
endpoint
=
endpoint
;
this
.
endpoint
=
endpoint
;
this
.
resource
=
Vue
.
resource
(
this
.
endpoint
);
}
}
get
Title
()
{
get
Data
()
{
return
this
.
resource
.
get
(
this
.
endpoint
);
return
this
.
resource
.
get
();
}
}
}
}
app/assets/javascripts/issue_show/stores/index.js
0 → 100644
View file @
20987f4f
export
default
class
Store
{
constructor
({
titleHtml
,
descriptionHtml
,
descriptionText
,
})
{
this
.
state
=
{
titleHtml
,
titleText
:
''
,
descriptionHtml
,
descriptionText
,
taskStatus
:
''
,
updatedAt
:
''
,
};
}
updateState
(
data
)
{
this
.
state
.
titleHtml
=
data
.
title
;
this
.
state
.
titleText
=
data
.
title_text
;
this
.
state
.
descriptionHtml
=
data
.
description
;
this
.
state
.
descriptionText
=
data
.
description_text
;
this
.
state
.
taskStatus
=
data
.
task_status
;
this
.
state
.
updatedAt
=
data
.
updated_at
;
}
}
app/assets/stylesheets/framework/mobile.scss
View file @
20987f4f
...
@@ -112,7 +112,7 @@
...
@@ -112,7 +112,7 @@
}
}
}
}
.issue
_edited_
ago
,
.issue
-edited-
ago
,
.note_edited_ago
{
.note_edited_ago
{
display
:
none
;
display
:
none
;
}
}
...
...
app/controllers/projects/issues_controller.rb
View file @
20987f4f
...
@@ -208,7 +208,6 @@ class Projects::IssuesController < Projects::ApplicationController
...
@@ -208,7 +208,6 @@ class Projects::IssuesController < Projects::ApplicationController
description:
view_context
.
markdown_field
(
@issue
,
:description
),
description:
view_context
.
markdown_field
(
@issue
,
:description
),
description_text:
@issue
.
description
,
description_text:
@issue
.
description
,
task_status:
@issue
.
task_status
,
task_status:
@issue
.
task_status
,
issue_number:
@issue
.
iid
,
updated_at:
@issue
.
updated_at
updated_at:
@issue
.
updated_at
}
}
end
end
...
...
app/helpers/issuables_helper.rb
View file @
20987f4f
...
@@ -136,11 +136,9 @@ module IssuablesHelper
...
@@ -136,11 +136,9 @@ module IssuablesHelper
author_output
<<
link_to_member
(
project
,
issuable
.
author
,
size:
24
,
by_username:
true
,
avatar:
false
,
mobile_classes:
"hidden-sm hidden-md hidden-lg"
)
author_output
<<
link_to_member
(
project
,
issuable
.
author
,
size:
24
,
by_username:
true
,
avatar:
false
,
mobile_classes:
"hidden-sm hidden-md hidden-lg"
)
end
end
if
issuable
.
tasks?
output
<<
" "
.
html_safe
output
<<
" "
.
html_safe
output
<<
content_tag
(
:span
,
issuable
.
task_status
,
id:
"task_status"
,
class:
"hidden-xs hidden-sm"
)
output
<<
content_tag
(
:span
,
issuable
.
task_status
,
id:
"task_status"
,
class:
"hidden-xs hidden-sm"
)
output
<<
content_tag
(
:span
,
issuable
.
task_status_short
,
id:
"task_status_short"
,
class:
"hidden-md hidden-lg"
)
output
<<
content_tag
(
:span
,
issuable
.
task_status_short
,
id:
"task_status_short"
,
class:
"hidden-md hidden-lg"
)
end
output
output
end
end
...
...
app/views/projects/issues/show.html.haml
View file @
20987f4f
...
@@ -51,12 +51,17 @@
...
@@ -51,12 +51,17 @@
.issue-details.issuable-details
.issue-details.issuable-details
.detail-page-description.content-block
.detail-page-description.content-block
.issue-title-data.hidden
{
"data"
=>
{
"endpoint"
=>
rendered_title_namespace_project_issue_path
(
@project
.
namespace
,
@project
,
@issue
),
#js-issuable-app
{
"data"
=>
{
"endpoint"
=>
rendered_title_namespace_project_issue_path
(
@project
.
namespace
,
@project
,
@issue
),
"can-update-tasks-class"
=>
can?
(
current_user
,
:update_issue
,
@issue
)
?
'js-task-list-container'
:
''
,
"can-update"
=>
can?
(
current_user
,
:update_issue
,
@issue
).
to_s
,
"issuable-ref"
=>
@issue
.
to_reference
,
}
}
}
}
.issue-title-entrypoint
%h2
.title
=
markdown_field
(
@issue
,
:title
)
-
if
@issue
.
description
.
present?
.description
{
class:
can?
(
current_user
,
:update_issue
,
@issue
)
?
'js-task-list-container'
:
''
}
.wiki
=
markdown_field
(
@issue
,
:description
)
%textarea
.hidden.js-task-list-field
=
@issue
.
description
=
edited_time_ago_with_tooltip
(
@issue
,
placement:
'bottom'
,
html_class:
'issue
_edited_
ago'
)
=
edited_time_ago_with_tooltip
(
@issue
,
placement:
'bottom'
,
html_class:
'issue
-edited-ago js-issue-edited-
ago'
)
#merge-requests
{
data:
{
url:
referenced_merge_requests_namespace_project_issue_url
(
@project
.
namespace
,
@project
,
@issue
)
}
}
#merge-requests
{
data:
{
url:
referenced_merge_requests_namespace_project_issue_url
(
@project
.
namespace
,
@project
,
@issue
)
}
}
// This element is filled in using JavaScript.
// This element is filled in using JavaScript.
...
...
spec/javascripts/issue_show/
issue_title_description
_spec.js
→
spec/javascripts/issue_show/
components/app
_spec.js
View file @
20987f4f
import
Vue
from
'
vue
'
;
import
Vue
from
'
vue
'
;
import
$
from
'
jquery
'
;
import
'
~/render_math
'
;
import
'
~/render_math
'
;
import
'
~/render_gfm
'
;
import
'
~/render_gfm
'
;
import
issueTitleDescription
from
'
~/issue_show/issue_title_description.vue
'
;
import
issuableApp
from
'
~/issue_show/components/app.vue
'
;
import
issueShowData
from
'
./mock_data
'
;
import
issueShowData
from
'
../mock_data
'
;
window
.
$
=
$
;
const
issueShowInterceptor
=
data
=>
(
request
,
next
)
=>
{
const
issueShowInterceptor
=
data
=>
(
request
,
next
)
=>
{
next
(
request
.
respondWith
(
JSON
.
stringify
(
data
),
{
next
(
request
.
respondWith
(
JSON
.
stringify
(
data
),
{
...
@@ -16,42 +13,45 @@ const issueShowInterceptor = data => (request, next) => {
...
@@ -16,42 +13,45 @@ const issueShowInterceptor = data => (request, next) => {
}));
}));
};
};
describe
(
'
Issu
e Title
'
,
()
=>
{
describe
(
'
Issu
able output
'
,
()
=>
{
document
.
body
.
innerHTML
=
'
<span id="task_status"></span>
'
;
document
.
body
.
innerHTML
=
'
<span id="task_status"></span>
'
;
let
IssueTitleDescriptionComponent
;
let
vm
;
beforeEach
(()
=>
{
beforeEach
(()
=>
{
IssueTitleDescriptionComponent
=
Vue
.
extend
(
issueTitleDescription
);
const
IssuableDescriptionComponent
=
Vue
.
extend
(
issuableApp
);
});
afterEach
(()
=>
{
Vue
.
http
.
interceptors
=
_
.
without
(
Vue
.
http
.
interceptors
,
issueShowInterceptor
);
});
it
(
'
should render a title/description and update title/description on update
'
,
(
done
)
=>
{
Vue
.
http
.
interceptors
.
push
(
issueShowInterceptor
(
issueShowData
.
initialRequest
));
Vue
.
http
.
interceptors
.
push
(
issueShowInterceptor
(
issueShowData
.
initialRequest
));
const
issueShowComponent
=
new
IssueTit
leDescriptionComponent
({
vm
=
new
Issuab
leDescriptionComponent
({
propsData
:
{
propsData
:
{
canUpdate
Issue
:
'
.css-stuff
'
,
canUpdate
:
true
,
endpoint
:
'
/gitlab-org/gitlab-shell/issues/9/rendered_title
'
,
endpoint
:
'
/gitlab-org/gitlab-shell/issues/9/rendered_title
'
,
issuableRef
:
'
#1
'
,
initialTitle
:
''
,
initialDescriptionHtml
:
''
,
initialDescriptionText
:
''
,
},
},
}).
$mount
();
}).
$mount
();
});
afterEach
(()
=>
{
Vue
.
http
.
interceptors
=
_
.
without
(
Vue
.
http
.
interceptors
,
issueShowInterceptor
);
});
it
(
'
should render a title/description and update title/description on update
'
,
(
done
)
=>
{
setTimeout
(()
=>
{
setTimeout
(()
=>
{
expect
(
document
.
querySelector
(
'
title
'
).
innerText
).
toContain
(
'
this is a title (#1)
'
);
expect
(
document
.
querySelector
(
'
title
'
).
innerText
).
toContain
(
'
this is a title (#1)
'
);
expect
(
issueShowComponent
.
$el
.
querySelector
(
'
.title
'
).
innerHTML
).
toContain
(
'
<p>this is a title</p>
'
);
expect
(
vm
.
$el
.
querySelector
(
'
.title
'
).
innerHTML
).
toContain
(
'
<p>this is a title</p>
'
);
expect
(
issueShowComponent
.
$el
.
querySelector
(
'
.wiki
'
).
innerHTML
).
toContain
(
'
<p>this is a description!</p>
'
);
expect
(
vm
.
$el
.
querySelector
(
'
.wiki
'
).
innerHTML
).
toContain
(
'
<p>this is a description!</p>
'
);
expect
(
issueShowComponent
.
$el
.
querySelector
(
'
.js-task-list-field
'
).
innerText
).
toContain
(
'
this is a description
'
);
expect
(
vm
.
$el
.
querySelector
(
'
.js-task-list-field
'
).
value
).
toContain
(
'
this is a description
'
);
Vue
.
http
.
interceptors
.
push
(
issueShowInterceptor
(
issueShowData
.
secondRequest
));
Vue
.
http
.
interceptors
.
push
(
issueShowInterceptor
(
issueShowData
.
secondRequest
));
setTimeout
(()
=>
{
setTimeout
(()
=>
{
expect
(
document
.
querySelector
(
'
title
'
).
innerText
).
toContain
(
'
2 (#1)
'
);
expect
(
document
.
querySelector
(
'
title
'
).
innerText
).
toContain
(
'
2 (#1)
'
);
expect
(
issueShowComponent
.
$el
.
querySelector
(
'
.title
'
).
innerHTML
).
toContain
(
'
<p>2</p>
'
);
expect
(
vm
.
$el
.
querySelector
(
'
.title
'
).
innerHTML
).
toContain
(
'
<p>2</p>
'
);
expect
(
issueShowComponent
.
$el
.
querySelector
(
'
.wiki
'
).
innerHTML
).
toContain
(
'
<p>42</p>
'
);
expect
(
vm
.
$el
.
querySelector
(
'
.wiki
'
).
innerHTML
).
toContain
(
'
<p>42</p>
'
);
expect
(
issueShowComponent
.
$el
.
querySelector
(
'
.js-task-list-field
'
).
innerText
).
toContain
(
'
42
'
);
expect
(
vm
.
$el
.
querySelector
(
'
.js-task-list-field
'
).
value
).
toContain
(
'
42
'
);
done
();
done
();
});
});
...
...
spec/javascripts/issue_show/components/description_spec.js
0 → 100644
View file @
20987f4f
import
Vue
from
'
vue
'
;
import
descriptionComponent
from
'
~/issue_show/components/description.vue
'
;
describe
(
'
Description component
'
,
()
=>
{
let
vm
;
beforeEach
(()
=>
{
const
Component
=
Vue
.
extend
(
descriptionComponent
);
if
(
!
document
.
querySelector
(
'
.issuable-meta
'
))
{
const
metaData
=
document
.
createElement
(
'
div
'
);
metaData
.
classList
.
add
(
'
issuable-meta
'
);
metaData
.
innerHTML
=
'
<span id="task_status"></span><span id="task_status_short"></span>
'
;
document
.
body
.
appendChild
(
metaData
);
}
vm
=
new
Component
({
propsData
:
{
canUpdate
:
true
,
descriptionHtml
:
'
test
'
,
descriptionText
:
'
test
'
,
updatedAt
:
new
Date
().
toString
(),
taskStatus
:
''
,
},
}).
$mount
();
});
it
(
'
animates description changes
'
,
(
done
)
=>
{
vm
.
descriptionHtml
=
'
changed
'
;
Vue
.
nextTick
(()
=>
{
expect
(
vm
.
$el
.
querySelector
(
'
.wiki
'
).
classList
.
contains
(
'
issue-realtime-pre-pulse
'
),
).
toBeTruthy
();
setTimeout
(()
=>
{
expect
(
vm
.
$el
.
querySelector
(
'
.wiki
'
).
classList
.
contains
(
'
issue-realtime-trigger-pulse
'
),
).
toBeTruthy
();
done
();
});
});
});
it
(
'
re-inits the TaskList when description changed
'
,
(
done
)
=>
{
spyOn
(
gl
,
'
TaskList
'
);
vm
.
descriptionHtml
=
'
changed
'
;
setTimeout
(()
=>
{
expect
(
gl
.
TaskList
,
).
toHaveBeenCalled
();
done
();
});
});
it
(
'
does not re-init the TaskList when canUpdate is false
'
,
(
done
)
=>
{
spyOn
(
gl
,
'
TaskList
'
);
vm
.
canUpdate
=
false
;
vm
.
descriptionHtml
=
'
changed
'
;
setTimeout
(()
=>
{
expect
(
gl
.
TaskList
,
).
not
.
toHaveBeenCalled
();
done
();
});
});
describe
(
'
taskStatus
'
,
()
=>
{
it
(
'
adds full taskStatus
'
,
(
done
)
=>
{
vm
.
taskStatus
=
'
1 of 1
'
;
setTimeout
(()
=>
{
expect
(
document
.
querySelector
(
'
.issuable-meta #task_status
'
).
textContent
.
trim
(),
).
toBe
(
'
1 of 1
'
);
done
();
});
});
it
(
'
adds short taskStatus
'
,
(
done
)
=>
{
vm
.
taskStatus
=
'
1 of 1
'
;
setTimeout
(()
=>
{
expect
(
document
.
querySelector
(
'
.issuable-meta #task_status_short
'
).
textContent
.
trim
(),
).
toBe
(
'
1/1 task
'
);
done
();
});
});
});
});
spec/javascripts/issue_show/components/title_spec.js
0 → 100644
View file @
20987f4f
import
Vue
from
'
vue
'
;
import
titleComponent
from
'
~/issue_show/components/title.vue
'
;
describe
(
'
Title component
'
,
()
=>
{
let
vm
;
beforeEach
(()
=>
{
const
Component
=
Vue
.
extend
(
titleComponent
);
vm
=
new
Component
({
propsData
:
{
issuableRef
:
'
#1
'
,
titleHtml
:
'
Testing <img />
'
,
titleText
:
'
Testing
'
,
},
}).
$mount
();
});
it
(
'
renders title HTML
'
,
()
=>
{
expect
(
vm
.
$el
.
innerHTML
.
trim
(),
).
toBe
(
'
Testing <img>
'
);
});
it
(
'
updates page title when changing titleHtml
'
,
(
done
)
=>
{
spyOn
(
vm
,
'
setPageTitle
'
);
vm
.
titleHtml
=
'
test
'
;
Vue
.
nextTick
(()
=>
{
expect
(
vm
.
setPageTitle
,
).
toHaveBeenCalled
();
done
();
});
});
it
(
'
animates title changes
'
,
(
done
)
=>
{
vm
.
titleHtml
=
'
test
'
;
Vue
.
nextTick
(()
=>
{
expect
(
vm
.
$el
.
classList
.
contains
(
'
issue-realtime-pre-pulse
'
),
).
toBeTruthy
();
setTimeout
(()
=>
{
expect
(
vm
.
$el
.
classList
.
contains
(
'
issue-realtime-trigger-pulse
'
),
).
toBeTruthy
();
done
();
});
});
});
it
(
'
updates page title after changing title
'
,
(
done
)
=>
{
vm
.
titleHtml
=
'
changed
'
;
vm
.
titleText
=
'
changed
'
;
Vue
.
nextTick
(()
=>
{
expect
(
document
.
querySelector
(
'
title
'
).
textContent
.
trim
(),
).
toContain
(
'
changed
'
);
done
();
});
});
});
spec/javascripts/issue_show/mock_data.js
View file @
20987f4f
...
@@ -4,23 +4,23 @@ export default {
...
@@ -4,23 +4,23 @@ export default {
title_text
:
'
this is a title
'
,
title_text
:
'
this is a title
'
,
description
:
'
<p>this is a description!</p>
'
,
description
:
'
<p>this is a description!</p>
'
,
description_text
:
'
this is a description
'
,
description_text
:
'
this is a description
'
,
issue_number
:
1
,
task_status
:
'
2 of 4 completed
'
,
task_status
:
'
2 of 4 completed
'
,
updated_at
:
new
Date
().
toString
(),
},
},
secondRequest
:
{
secondRequest
:
{
title
:
'
<p>2</p>
'
,
title
:
'
<p>2</p>
'
,
title_text
:
'
2
'
,
title_text
:
'
2
'
,
description
:
'
<p>42</p>
'
,
description
:
'
<p>42</p>
'
,
description_text
:
'
42
'
,
description_text
:
'
42
'
,
issue_number
:
1
,
task_status
:
'
0 of 0 completed
'
,
task_status
:
'
0 of 0 completed
'
,
updated_at
:
new
Date
().
toString
(),
},
},
issueSpecRequest
:
{
issueSpecRequest
:
{
title
:
'
<p>this is a title</p>
'
,
title
:
'
<p>this is a title</p>
'
,
title_text
:
'
this is a title
'
,
title_text
:
'
this is a title
'
,
description
:
'
<li class="task-list-item enabled"><input type="checkbox" class="task-list-item-checkbox">Task List Item</li>
'
,
description
:
'
<li class="task-list-item enabled"><input type="checkbox" class="task-list-item-checkbox">Task List Item</li>
'
,
description_text
:
'
- [ ] Task List Item
'
,
description_text
:
'
- [ ] Task List Item
'
,
issue_number
:
1
,
task_status
:
'
0 of 1 completed
'
,
task_status
:
'
0 of 1 completed
'
,
updated_at
:
new
Date
().
toString
(),
},
},
};
};
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