THIS IS A TEST INSTANCE ONLY! REPOSITORIES CAN BE DELETED AT ANY TIME!

test
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

586 lines
30 KiB

@**
* Yona, 21st Century Project Hosting SW
*
* Copyright Yona & Yobi Authors & NAVER Corp. & NAVER LABS Corp.
* https://yona.io
**@
@(title:String, issue:Issue, issueForm: play.data.Form[Issue], commentForm: play.data.Form[Comment],project:Project)
@import scala.collection.mutable.ArrayBuffer
@import org.apache.commons.lang.StringUtils
@import models.enumeration.ResourceType
@import models.enumeration.Operation
@import models.Milestone
@import utils.JodaDateUtil
@import utils.TemplateHelper._
@import utils.AccessControl._
@import play.libs.Json.toJson
@import utils.Markdown
@import models.enumeration.State
@import controllers.api.IssueApi
@getTitle(issue:Issue) = @{ issue.title }
@isFirstState(state:State) = {@if(issue.state.state == Issue.availableStates.get(0).state()){dirty}}
@urlToIssues = @{
requestHeader.headers.get("Referer") match {
case Some(u) => urlToList(u, routes.IssueApp.issues(project.owner, project.name, issue.state.state()).toString())
case _ => routes.IssueApp.issues(project.owner, project.name, "open").toString()
}
}
@isVotedByCurrentUser = @{
issue.isVotedBy(UserApp.currentUser)
}
@urlToVote = @{
if(isVotedByCurrentUser){
routes.VoteApp.unvote(project.owner, project.name, issue.getNumber).toString
} else {
routes.VoteApp.vote(project.owner, project.name, issue.getNumber).toString
}
}
@getVoteButtonTitle = {
@if(isVotedByCurrentUser){
@Messages("issue.unvote.description")
} else {
@Messages("issue.vote.description")
}
}
@titleForOGTag = @{getTitle(issue) + " |:| " + issue.body.substring(0, Math.min(issue.body.length, 200))}
@parentIssueId = @{
if(issue.parent != null){
issue.parent.id
} else {
issue.id
}
}
@hasAssignee = @{
issue.assigneeName != null
}
@hasSharer = @{
issue.sharers.size > 0
}
@sharers = @{
var sharerIds = ArrayBuffer[String]()
for( sharedUser <- issue.sharers ) {
sharerIds += sharedUser.loginId
}
sharerIds.mkString(",")
}
@VOTER_AVATAR_SHOW_LIMIT = @{ 5 }
@conatinsCurrentUserInWatchers = @{Watch.isWatching(UserApp.currentUser(), issue.asResource())}
@isThisParentIssue() = @{
issue.parent == null
}
@amIShared() = @{
var found = false
for(sharer <- issue.sharers) {
if(sharer.user.id.equals(UserApp.currentUser().id)) {
found = true
}
}
found
}
@isFavoriteIssue = @{
FavoriteIssue.findByIssueId(UserApp.currentUser().id, issue.id) != null
}
@isAllowedUpdate = @{
isAllowed(UserApp.currentUser(), issue.asResource(), Operation.UPDATE)
}
@projectLayout(titleForOGTag, project, utils.MenuType.ISSUE){
@projectMenu(project, utils.MenuType.ISSUE, "main-menu-only")
<div class="page-wrap-outer">
<div class="project-page-wrap board-view">
@** Post Info **@
<div class="board-header issue">
<div class="pull-right mr10 mt10 hide-in-mobile">
<div class="date" title="@JodaDateUtil.getDateString(issue.createdDate)">
@agoOrDateString(issue.createdDate)
</div>
<span class="badge badge-issue-@issue.state.state.toLowerCase">@Messages("issue.state." + issue.state.state)</span>
</div>
<div class="title">
@if(issue.parent != null) {
<span class="subtask-mark">subtask</span>
}
<strong class="board-id">#@issue.getNumber</strong> @issue.title
<span class="favorite-issue" data-issue-id="@issue.id">
<i class="@if(isFavoriteIssue){starred} star material-icons va-text-top">star</i>
</span>
<div class="pull-right hide show-in-mobile" style="font-size: 0.7em">
<span class="date" title="@JodaDateUtil.getDateString(issue.createdDate)">
@agoOrDateString(issue.createdDate)
</span>
<span class="badge badge-small badge-issue-@issue.state.state.toLowerCase">@Messages("issue.state." + issue.state.state)</span>
</div>
</div>
</div>
@** Content body **@
<!--board-body-->
<div class="board-body row-fluid">
<div class="span9 span-left-pane">
<div class="author-info">
<a href="@userInfo(issue.authorLoginId)" class="usf-group">
<span class="avatar-wrap smaller">
<img src="@User.findByLoginId(issue.authorLoginId).avatarUrl(32)" width="20" height="20">
</span>
@if(issue.authorLoginId != null){
<strong class="name">@issue.getAuthor.getDisplayName</strong>
<span class="loginid"> <strong>@{"@"}</strong>@issue.authorLoginId</span>
} else {
<strong class="name">@Messages("issue.noAuthor")</strong>
}
</a>
@if(StringUtils.isNotEmpty(issue.history)){
@if(UserApp.currentUser().isAnonymous){
<div class="posting-history">
<a href="@routes.UserApp.loginForm()?redirectUrl=@routes.IssueApp.issue(project.owner, project.name, issue.getNumber)" data-toggle="modal">@Messages("change.history")</a>
</div>
} else {
<div class="posting-history">
<a href="#-yona-posting-history" data-toggle="modal">@Messages("change.history")</a>
@common.partial_history(issue)
</div>
}
}
</div>
@if(StringUtils.isEmpty(issue.body)){
<div class="content empty-content"></div>
} else {
<div id="issue-@issue.getNumber" class="hide">
<form action="@api.routes.IssueApi.updateIssueContent(project.owner, project.name, issue.getNumber)">
<textarea>@issue.body</textarea>
</form>
</div>
<div id="issue-body-@issue.getNumber">
@common.tasklistBar()
<div class="content markdown-wrap" data-allowed-update="@isAllowedUpdate">@Html(Markdown.render(issue.body, issue.project))</div>
</div>
}
<div class="attachments" id="attachments" data-attachments="@toJson(AttachmentApp.getFileList(ResourceType.ISSUE_POST.toString(), issue.id.toString()))"></div>
<div class="board-actrow right-txt">
<div class="pull-left">
<div>
@if(isAllowed(UserApp.currentUser(), issue.asResource(), Operation.WATCH)) {
<button id="watch-button" type="button" class="ybtn @if(conatinsCurrentUserInWatchers) {ybtn-watching}"
data-toggle="tooltip" data-placement="top" title="@Messages("issue.watch.description")" data-watching="@conatinsCurrentUserInWatchers">
@if(conatinsCurrentUserInWatchers) {
@Messages("issue.unwatch")
} else {
@Messages("issue.watch")
}
</button>
}
@if(isAllowedUpdate && !hasSharer) {
<button id="issue-share-button" type="button" class="ybtn" data-toggle="popover" data-trigger="hover" data-placement="top" data-content="@Messages("issue.sharer.description")">@Messages("button.share.issue")</button>
}
</div>
</div>
<div id="vote" class="vote-wrap @if(issue.voters.size > 0){voter-exists}">
@if(isResourceCreatable(UserApp.currentUser, issue.asResource(), ResourceType.ISSUE_COMMENT)) {
<a href="@urlToVote" class="@if(issue.isVotedBy(UserApp.currentUser)) {ybtn-watching}" title="@getVoteButtonTitle"
data-request-method="post" data-toggle="tooltip">
<span class="heart"><i class="yobicon-hearts"></i></span>
</a>
} else {
<span class="ybtn-disabled" style="color: #777;" data-toggle="tooltip" title="@Messages("user.login.alert")" data-login="required">
<span class="heart"><i class="yobicon-hearts"></i></span>
</span>
}
@if(issue.voters.size > 0) {
@partial_voters(issue, 3)
}
</div>
@if(issue.voters.size > 0) {
@partial_voter_list("voters", issue.voters)
}
@if(StringUtils.isNotBlank(IssueApi.TRANSLATION_API)){
<button type="button" id="translate" class="icon btn-transparent-with-fontsize-lineheight ml10" data-toggle="tooltip" title="@Messages("button.translation")"><i class="yobicon-lang"></i></button>
}
<span class="act-row">
@if(isAllowedUpdate) {
<button type="button" class="icon btn-transparent-with-fontsize-lineheight ml10 pt5px" data-toggle="tooltip" title="@Messages("button.edit")" onclick="window.location='@routes.IssueApp.editIssueForm(project.owner, project.name, issue.getNumber)'"><i class="yobicon-edit-2"></i></button>
} else {
<a href="@routes.IssueApp.editIssueForm(project.owner, project.name, issue.getNumber)"><button type="button" class="icon btn-transparent-with-fontsize-lineheight ml10 pt5px" data-toggle="tooltip" title="@Messages("button.show.original")"><i class="yobicon-edit-2"></i></button></a>
}
@if(issue.canBeDeleted) {
@if(isAllowed(UserApp.currentUser(), issue.asResource(), Operation.DELETE)) {
<a href="#deleteConfirm" data-toggle="modal">
<button type="button" class="icon btn-transparent-with-fontsize-lineheight ml6" data-toggle='tooltip' title="@Messages("button.delete")"><i class="yobicon-trash"></i></button></a>
}
} else {
<button type="button" class="icon disabled btn-transparent-with-fontsize-lineheight ml6" data-toggle='popover' data-trigger="hover" data-placement="top" data-content="@Messages("issue.can.not.be.deleted")"><i class="yobicon-trash"></i></button>
}
</span>
</div>
<dl class="sharer-list @if(!hasSharer){hideFromDisplayOnly}">
<dt class="issue-share-title mb10">
@Messages("issue.sharer") <span class="num issue-sharer-count">@if(issue.sharers.size > 0) { @issue.sharers.size }</span>
</dt>
<dd id="sharer-list" class="@if(!hasSharer){hideFromDisplayOnly}">
@if(isAllowedUpdate) {
<input type="hidden" class="bigdrop width100p" id="issueSharer" name="issueSharer" placeholder="@Messages("issue.sharer.select")" value="@sharers" title="">
} else {
@for(sharer <- issue.getSortedSharer){
<div class="text-ellipsis sharer-item">
<a href="@userInfo(sharer.loginId)" class="usf-group">
<strong class="name">@sharer.user.getDisplayName</strong>
</a>
</div>
}
}
</dd>
</dl>
<div class="watcher-list"></div>
<div class="subtasks">
@if(amIShared) {
@if(isThisParentIssue) {
@partial_view_childIssueList(issue, project)
}
} else {
@partial_view_childIssueList(issue, project)
}
</div>
@** Comment **@
<div id="comments" class="board-comment-wrap">
<div id="timeline">
<div class="timeline-list">
@partial_comments(project, issue)
</div>
</div>
@common.commentForm(issue.asResource(), ResourceType.ISSUE_COMMENT, routes.IssueApp.newComment(project.owner, project.name, issue.getNumber).toString())
</div>
@** // Comment **@
</div>
<div class="span3 span-right-pane mb20">
<div class="issue-info">
<form id="issueUpdateForm" action="@routes.IssueApp.massUpdate(project.owner, project.name)" method="post">
<input type="hidden" name="issues[0].id" value="@issue.id" />
@**<!-- assignee -->**@
<dl>
@if(project.menuSetting.issue) {
<dd class="project-btn-item">
<a href="@routes.IssueApp.newIssueForm(project.owner, project.name)?parentIssueId=@parentIssueId" class="ybtn ybtn-success">@Messages("button.newSubtask")</a>
</dd>
}
<dt>@Messages("issue.assignee")</dt>
<dd>
@if(isAllowedUpdate) {
@partial_assignee(project, issue)
} else {
@if(hasAssignee){
<a href="@userInfo(issue.assignee.user.loginId)" class="usf-group">
<span class="avatar-wrap smaller">
<img src="@User.findByLoginId(issue.assignee.user.loginId).avatarUrl" width="20" height="20">
</span>
<strong class="name">@issue.assignee.user.getDisplayName</strong>
<span class="loginid"> <strong>@{"@"}</strong>@issue.assignee.user.loginId</span>
</a>
} else {
<div>
@Messages("issue.noAssignee")
</div>
}
}
</dd>
</dl>
@**<!-- // -->**@
@**<!-- milestones -->**@
@if(project.menuSetting.milestone) {
<dl>
<dt>@Messages("milestone")</dt>
<dd>
@if(Milestone.findByProjectId(project.id).isEmpty()){
<a href="@routes.MilestoneApp.newMilestoneForm(project.owner, project.name)"
class="ybtn ybtn-small ybtn-fullsize" target="_blank">
@Messages("milestone.menu.new")
</a>
} else {
@defining(issue.milestone != null) { hasMilestone =>
@if(isAllowedUpdate) {
<select id="milestone" name="milestone.id"
data-toggle="select2" data-format="milestone" data-container-css-class="fullsize">
<option value="@Milestone.NULL_MILESTONE_ID" @if(!hasMilestone){ selected }>
@Messages("issue.noMilestone")
</option>
<optgroup label="@Messages("milestone.state.open")">
@for(milestone <- Milestone.findOpenMilestones(project.id)){
<option value="@milestone.id" data-state="@milestone.state"
@if(hasMilestone && issue.milestone.id == milestone.id){
selected
}>
@milestone.title
</option>
}
</optgroup>
<optgroup label="@Messages("milestone.state.closed")">
@for(milestone <- Milestone.findClosedMilestones(project.id)){
<option value="@milestone.id" data-state="@milestone.state"
@if(hasMilestone && issue.milestone.id == milestone.id){
selected
}>
@milestone.title
</option>
}
</optgroup>
</select>
} else {
@if(hasMilestone){
<a href="@routes.MilestoneApp.milestone(project.owner, project.name, issue.milestone.id)">
@issue.milestone.title
</a>
} else {
@Messages("issue.noMilestone")
}
}
}
}
</dd>
</dl>
}
@**<!-- // -->**@
<dl>
<dt>
@Messages("issue.dueDate")
<span class="duedate-status @if(issue.isOverDueDate) {overdue}">
@if(issue.dueDate != null && issue.isOpen) {
@if(issue.isOverDueDate) {
(@Messages("issue.dueDate.overdue"))
} else {
(@issue.until)
}
}
</span>
</dt>
<dd>
@if(isAllowedUpdate) {
<div class="search search-bar">
<input type="text" name="dueDate" value="@issue.getDueDateString" class="textbox full" autocomplete="off" data-toggle="calendar">
<button type="button" class="search-btn btn-calendar"><i class="yobicon-calendar2"></i></button>
</div>
} else {
@if(issue.dueDate != null) {
@issue.getDueDateString
} else {
@Messages("issue.noDuedate")
}
}
</dd>
</dl>
@**<!-- labels -->**@
@if(!IssueLabel.findByProject(project).isEmpty){
@if(isAllowedUpdate){
@partial_select_label(IssueLabel.findByProject(project), issue.getLabelIds, "", "", project)
} else {
@partial_show_selected_label(issue.labels.toList, routes.IssueApp.issues(project.owner, project.name, issue.state.state(), "html", 1).toString)
}
}
<div class="act-row right-menu-icons">
@if(isAllowedUpdate) {
<button type="button" class="icon btn-transparent-with-fontsize-lineheight ml10 pt5px" data-toggle="tooltip" title="@Messages("button.edit")" onclick="window.location='@routes.IssueApp.editIssueForm(project.owner, project.name, issue.getNumber)'"><i class="yobicon-edit-2"></i></button>
} else {
<a href="@routes.IssueApp.editIssueForm(project.owner, project.name, issue.getNumber)"><button type="button" class="icon btn-transparent-with-fontsize-lineheight ml10 pt5px" data-toggle="tooltip" title="@Messages("button.show.original")"><i class="yobicon-edit-2"></i></button></a>
}
@if(issue.canBeDeleted) {
@if(isAllowed(UserApp.currentUser(), issue.asResource(), Operation.DELETE)) {
<a href="#deleteConfirm" data-toggle="modal">
<button type="button" class="icon btn-transparent-with-fontsize-lineheight ml6" data-toggle='tooltip' title="@Messages("button.delete")"><i class="yobicon-trash"></i></button></a>
}
} else {
<button type="button" class="icon disabled btn-transparent-with-fontsize-lineheight ml6" data-toggle='popover' data-trigger="hover" data-placement="top" data-content="@Messages("issue.can.not.be.deleted")"><i class="yobicon-trash"></i></button>
}
</div>
@**<!-- // -->**@
</form>
</div>
</div>
</div>
<div class="board-footer">
@help.keymap("issueDetail", project)
</div>
</div>
<script type="text/x-jquery-tmpl" id="tplAttachedFile"><!--
--><li class="attached-file" data-name="${fileName}" data-href="${fileHref}" data-mime="${mimeType}" data-size="${fileSize}">
<strong>${fileName}(${fileSizeReadable})${notice}</strong><!--
--><a class="attached-delete"><i class="ico btn-delete"></i></a></li>
</script>
@** Confirm to delete post **@
<div id="deleteConfirm" class="modal hide fade">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal">×</button>
<h3>@Messages("issue.delete")</h3>
</div>
<div class="modal-body">
<p>@Messages("post.delete.confirm")</p>
</div>
<div class="modal-footer">
<button type="button" class="ybtn ybtn-danger" data-request-method="delete" data-request-uri="@routes.IssueApp.deleteIssue(project.owner, project.name, issue.getNumber)">@Messages("button.yes")</button>
<button type="button" class="ybtn" data-dismiss="modal">@Messages("button.no")</button>
</div>
</div>
</div>
@common.markdown(project)
@common.commentDeleteModal()
@common.select2()
@common.calendar()
<link rel="stylesheet" type="text/css" media="screen" href="@routes.IssueLabelApp.labelStyles(project.owner, project.name)">
<link rel="stylesheet" type="text/css" media="screen" href="@routes.Assets.at("javascripts/lib/atjs/jquery.atwho.css")">
<link rel="stylesheet" type="text/css" media="screen" href="@routes.Assets.at("javascripts/lib/elevator/jquery.elevator.css")">
<script type="text/javascript" src="@routes.Assets.at("javascripts/lib/atjs/jquery.caret.min.js")"></script>
<script type="text/javascript" src="@routes.Assets.at("javascripts/lib/atjs/jquery.atwho.js")"></script>
<script type="text/javascript" src="@routes.Assets.at("javascripts/lib/elevator/jquery.elevator.js")"></script>
<script type="text/javascript" src="@routes.Assets.at("javascripts/service/yona.issue.Assginee.js")"></script>
<script type="text/javascript" src="@routes.Assets.at("javascripts/service/yona.issue.Sharer.js")"></script>
<script type="text/javascript" src="@routes.Assets.at("javascripts/common/yona.Sha1.js")"></script>
<script type="text/javascript" src="@routes.Assets.at("javascripts/common/yona.Tasklist.js")"></script>
<script type="text/javascript" src="@routes.Assets.at("javascripts/common/yona.SubComment.js")"></script>
<script type="text/javascript">
$(function(){
// yobi.issue.View
$yobi.loadModule("issue.View", {
"issueId" : "@issue.id",
"nextState": "@issue.nextState().toString.toLowerCase",
"urls" : {
"watch" : "@routes.WatchApp.watch(issue.asResource.asParameter)",
"unwatch" : "@routes.WatchApp.unwatch(issue.asResource.asParameter)",
"timeline" : "@routes.IssueApp.timeline(project.owner, project.name, issue.getNumber)",
"nextState" : "@routes.IssueApp.nextState(project.owner, project.name, issue.getNumber)",
"massUpdate": "@routes.IssueApp.massUpdate(project.owner, project.name)"
}
});
// yobi.ShortcutKey
yobi.ShortcutKey.setKeymapLink({
"L": "@urlToIssues"
@if(project.menuSetting.issue) {
,"N": "@routes.IssueApp.newIssueForm(project.owner, project.name)"
}
@if(isAllowedUpdate) {
,"E": "@routes.IssueApp.editIssueForm(project.owner, project.name, issue.getNumber)"
}
});
// yobi.Mention
yobi.Mention({
"target": 'textarea[id^=editor-]',
"url" : "@Html(routes.ProjectApp.mentionList(project.owner, project.name, issue.getNumber, issue.asResource().getType.resource()).toString())"
});
// detect comment which contains mention at me
$(".comment-body:contains('@UserApp.currentUser().getPureNameOnly')").closest(".comment").addClass("mentioned");
});
</script>
<script>
$(function () {
yonaAssgineeModule(
"@api.routes.IssueApi.findAssignableUsers(project.owner, project.name, issue.getNumber)",
"@api.routes.IssueApi.updateAssginees(project.owner, project.name, issue.getNumber)",
"@Messages("issue.assignee")"
);
yonaIssueSharerModule(
"@api.routes.IssueApi.findSharerByloginIds(project.owner, project.name, issue.getNumber)",
"@api.routes.IssueApi.findSharableUsers(project.owner, project.name, issue.getNumber)",
"@api.routes.IssueApi.updateSharer(project.owner, project.name, issue.getNumber)",
"@Messages("issue.sharer")"
);
$('#issue-share-button').on('click', function () {
$('#sharer-list').show();
$('.sharer-list').show().addClass("sharer-list-border");
});
$('#translate').one('click', function (e) {
var data = {
owner: "@project.owner",
projectName: "@project.name",
type: "issue",
number: "@issue.getNumber"
};
$.ajax({
url: "/-_-api/v1/translation",
data: JSON.stringify(data),
type: "POST",
dataType: "json",
contentType: "application/json"
}).done(function (data) {
$(".markdown-wrap").first().html(data.translated);
$(this).attr("disabled", true);
});
});
$('.comment-translate').one('click', function (e) {
var payload = {
owner: "@project.owner",
projectName: "@project.name",
type: "issue-comment",
number: $(this).data("commentId")
};
$.ajax({
url: "/-_-api/v1/translation",
data: JSON.stringify(payload),
type: "POST",
dataType: "json",
contentType: "application/json"
}).done(function (data) {
$("#comment-body-" + payload.number).find(".markdown-wrap").html(data.translated)
$(this).attr("disabled", true);
});
});
if (ClipboardJS.isSupported()) {
var clipboard = new ClipboardJS('#copyEmailBtn');
clipboard.on('success', function(e) {
$yobi.alert('@Messages("button.copy.email.success.message")');
});
} else {
$yobi.notify('@Messages("site.features.error.clipboard")', 1500);
}
// timeline label text color adjusting
$(".event > .label").each(function() {
var $this = $(this);
$this.removeClass("dimgray white")
.addClass($yobi.getContrastColor($this.css('background-color')))
});
$.elevator({
shape: 'rounded',
tooltips: true
});
});
</script>
}