Compare commits

...

114 Commits
2.3 ... 2.5

Author SHA1 Message Date
Naoki Takezoe
2a60f607ff Update README.md for 2.5 2014-11-04 01:38:29 +09:00
Naoki Takezoe
78f4d26aa0 Add version 2.5 2014-11-03 05:46:32 +09:00
Naoki Takezoe
f59e86f5ca (refs #529)Add icons 2014-11-03 05:25:03 +09:00
Naoki Takezoe
1c2af36c92 (refs #529)Mentioned filter 2014-11-03 04:35:41 +09:00
takezoe
badbe73f4e Fix for Firefox 2014-11-03 02:30:19 +09:00
takezoe
a9d58698cd (refs #529)Adjust bottom line 2014-11-03 01:40:47 +09:00
Naoki Takezoe
bb3f086aa6 (refs #529)Enhance dashboard header 2014-11-03 00:31:34 +09:00
Naoki Takezoe
2db674bb03 (refs #529)Organization filter 2014-11-02 13:26:46 +09:00
Naoki Takezoe
4bc4a16a80 Merge branch 'master' into newui-for-dashboard
Conflicts:
	src/main/twirl/dashboard/issueslist.scala.html
	src/main/twirl/dashboard/pullslist.scala.html
2014-11-01 03:14:19 +09:00
Naoki Takezoe
d88a105628 Merge pull request #512 from mrkm4ntr/create-branch-ui
(refs #394) Create branch from Web UI
2014-11-01 03:10:38 +09:00
Naoki Takezoe
15d0c5b506 Merge pull request #526 from mrkm4ntr/datetime-format
(ref #519) Change datetime formats
2014-11-01 03:10:18 +09:00
Naoki Takezoe
dbde79d2f2 Merge pull request #342 from bati11/feature-tasklist
Implement "Task List" in markdown
2014-11-01 03:09:24 +09:00
Naoki Takezoe
e6e3786b47 (refs #529)Visibility filter 2014-11-01 03:05:52 +09:00
Tomofumi Tanaka
4c1b8004fc (refs #533)Admin user must not disable self account yourself 2014-10-29 09:15:20 +09:00
Naoki Takezoe
13c206d068 Applying new Issues UI to dashboard 2014-10-19 21:34:12 +09:00
Shintaro Murakami
5b875d7c73 Refactored, sorry. 2014-10-19 01:22:31 +09:00
Shintaro Murakami
e33dd9008b (ref #519) Change datetime formats 2014-10-18 23:21:47 +09:00
Naoki Takezoe
8764910553 (refs #504)Fix word-break of "Pages" table 2014-10-18 19:26:11 +09:00
Naoki Takezoe
4c89c40944 (refs #522)Recover user filter in dashboard 2014-10-18 19:11:30 +09:00
Shintaro Murakami
0f0986afcf (refs #394)Create branch from Web UI 2014-10-16 22:03:49 +09:00
Shintaro Murakami
5d5f1f8bdd (refs #514) Fix problems of renaming repository. 2014-10-16 22:03:49 +09:00
Naoki Takezoe
03e386b3ce Merge pull request #515 from mrkm4ntr/hotfix-514
(refs #514) Fix problems of renaming repository.
2014-10-12 19:05:54 +09:00
takezoe
435eac7ae6 (refs #511)Fix problem which is not possible to choose color at the colorpicker 2014-10-12 16:29:41 +09:00
takezoe
bd5df3977d (refs #518)Compile for Java7 2014-10-12 15:49:49 +09:00
bati11
ba218053f9 Modify to correspond to that "issuedetail.scala.html" has been deleted 2014-10-11 13:50:32 +09:00
bati11
1fe448a83b Merge branch 'master' into feature-tasklist
Conflicts:
	src/main/twirl/issues/issuedetail.scala.html
2014-10-11 12:07:14 +09:00
Shintaro Murakami
26a45d0117 (refs #514) Fix problems of renaming repository. 2014-10-09 22:05:42 +09:00
Naoki Takezoe
320585a530 Fix presentation problem on Firefox 2014-10-07 14:52:11 +09:00
Naoki Takezoe
ca0f888a99 Update for 2.4.1 2014-10-06 13:56:33 +09:00
Naoki Takezoe
3b08dc2e41 (refs #510)Fix dropdown presentation 2014-10-06 13:47:41 +09:00
Naoki Takezoe
cc128a49c1 (refs #510)Dirty fix for Firefox 2014-10-06 13:37:32 +09:00
Naoki Takezoe
e0148695f2 (refs #509)Fix link broken bug in Wiki 2014-10-06 13:22:42 +09:00
Naoki Takezoe
afe0b1dd71 Fix link bug in pull requests 2014-10-06 03:08:35 +09:00
Naoki Takezoe
353852d6da Update README.md 2014-10-06 02:14:10 +09:00
Naoki Takezoe
28585d1a3d Merge branch 'new-issue-ui' 2014-10-06 01:54:19 +09:00
shimamoto
9d69a48c65 (refs #488) Fix bug for refer comment. 2014-10-06 01:40:23 +09:00
shimamoto
2f95c76634 (refs #488) Fixed compile error. 2014-10-06 01:39:42 +09:00
shimamoto
eac9f0e6ff (refs #488) Fixed the action for assignee. 2014-10-06 01:30:30 +09:00
Naoki Takezoe
043fc21e05 Add Version 2.4 2014-10-06 01:05:40 +09:00
shimamoto
5854a75615 (refs #488) Fixed the action for label and milestone. 2014-10-06 00:52:14 +09:00
Naoki Takezoe
7b02946496 (refs #506)Fix generated URL for images 2014-10-05 23:07:15 +09:00
Naoki Takezoe
70f0ffd4f4 Fix label text color 2014-10-05 20:01:34 +09:00
Naoki Takezoe
91b82c2652 (refs #505)Disable the plugin system in default 2014-10-05 18:08:33 +09:00
shimamoto
b1017140aa (refs #488) Fixed the action for issue and comment content change. 2014-10-05 17:13:58 +09:00
takezoe
fc806b8813 Merge branch 'new-issue-ui' of https://github.com/takezoe/gitbucket into new-issue-ui 2014-10-05 15:37:22 +09:00
takezoe
836913482b (refs #488)Issue label editing is completed 2014-10-05 15:37:09 +09:00
Naoki Takezoe
b3df3f44c6 Merge pull request #500 from mrkm4ntr/split-diff-style
Modify styles of split diff.
2014-10-05 11:56:49 +09:00
shimamoto
4ffbf89e74 (refs #488) Fixed the action for issue title change. 2014-10-05 04:43:11 +09:00
shimamoto
9851c7d93d (refs #488) Changing the issue edit. 2014-10-04 23:53:37 +09:00
Tomofumi Tanaka
2201f2b202 Simplify query 2014-10-04 00:31:23 +09:00
takezoe
c92e71bb7a Merge branch 'new-issue-ui' of https://github.com/takezoe/gitbucket into new-issue-ui 2014-10-01 02:25:43 +09:00
takezoe
d271fac350 (refs #488)Add transparent label icons 2014-10-01 02:25:19 +09:00
Naoki Takezoe
ce4522fc30 (refs #488)Remove top margin of the clear condition link 2014-09-30 02:31:11 +09:00
Tomofumi Tanaka
a178c48de6 Merge branch 'fix-498-dash-pr' 2014-09-29 20:51:32 +09:00
Tomofumi Tanaka
9d1323a044 (refs #498)Reformat counting pull request query 2014-09-29 16:38:12 +09:00
Tomofumi Tanaka
43babfed94 (refs #498) Correct pull request counts 2014-09-29 14:00:29 +09:00
Tomofumi Tanaka
6fa7ea30fb (refs #498)Returns private own repository
RepositoryService.getAllRepositories should returns
private own repository too.
2014-09-29 11:53:19 +09:00
Tomofumi Tanaka
d78315695b (refs $#498)Don't show private repo user doesn't have permission
This fix in the dashboard pull request view
But this fix still has a problem show wrong count number pull request.
2014-09-29 11:27:48 +09:00
shimamoto
16021865cb (refs #488) Fixed the screen layout. 2014-09-28 21:15:30 +09:00
Naoki Takezoe
b516be242d (refs #488)Show number of issues for each labels 2014-09-28 11:43:58 +09:00
Naoki Takezoe
0124f7cc3c (refs #488)Change permission to access to labels 2014-09-28 11:35:19 +09:00
Naoki Takezoe
f3eec35287 (refs #488)Fix nav-pills style in the header of the issues system 2014-09-28 11:24:27 +09:00
Naoki Takezoe
fb396a33b0 (refs #488)Remove unnecessary div 2014-09-28 03:05:04 +09:00
Naoki Takezoe
3370499421 (refs #488)Apply new UI to labels 2014-09-28 03:04:36 +09:00
shimamoto
d847e27cf9 (refs #488) Move Milestone and Assignee. 2014-09-27 23:48:38 +09:00
Naoki Takezoe
9684b158ce (refs #488)Apply new UI to the milestone create / edit page 2014-09-26 02:14:30 +09:00
Naoki Takezoe
8456808a8e (refs #488)Add milestone icons 2014-09-25 09:37:13 +09:00
Naoki Takezoe
9747899a19 (refs #488)Update icons 2014-09-25 09:36:48 +09:00
shimamoto
099304605e Merge branch 'new-issue-ui' of https://github.com/takezoe/gitbucket into new-issue-ui 2014-09-25 04:49:07 +09:00
shimamoto
30994d0465 (refs #488) WIP: Fixing the screen layout for the issue detail. 2014-09-25 04:48:39 +09:00
Naoki Takezoe
71fdbe7b71 (refs #488)Apply new UI to Milestones tab 2014-09-25 02:32:55 +09:00
Naoki Takezoe
86432c5ffe (refs #488)Small fix 2014-09-25 01:52:32 +09:00
takezoe
4dfa1fb0f8 Merge remote-tracking branch 'origin/new-issue-ui' into new-issue-ui 2014-09-23 23:26:54 +09:00
takezoe
db59a7652f (refs #488)Add icons for new Issues UI 2014-09-23 23:26:46 +09:00
shimamoto
417470a81c (refs #488) Fixed the screen layout for the issue creation. 2014-09-23 21:42:56 +09:00
Naoki Takezoe
cc639da17e (refs #488)Unify issues and pull requests template 2014-09-23 18:11:46 +09:00
Naoki Takezoe
f619f4a9bc (refs #488)Remove unnecessary template arguments 2014-09-23 17:50:47 +09:00
Naoki Takezoe
5dffc2a64e (refs #488)Batch edit for pull requests 2014-09-23 17:41:53 +09:00
Naoki Takezoe
bb63a8d14c (refs #488)Remove unnecessary template arguments 2014-09-23 17:23:52 +09:00
Naoki Takezoe
c1263cc16d (refs #488)Batch edit for issues 2014-09-23 14:57:29 +09:00
Shintaro Murakami
49f2e7d70f Add empty style. 2014-09-23 02:10:46 +09:00
Shintaro Murakami
f93b535f70 Modify styles of split diff. 2014-09-23 00:07:00 +09:00
shimamoto
e16d3c823b Update slick version. 2014-09-22 21:59:17 +09:00
Naoki Takezoe
7a6fdbcf50 (refs #488)Fix the "New pull request" button 2014-09-22 19:26:36 +09:00
Naoki Takezoe
46041a3762 (refs #488)Apply new UI to the pull request list 2014-09-22 19:10:39 +09:00
Naoki Takezoe
20b0553f7f (refs #488)Exclude pull requests from the issue list 2014-09-22 18:38:28 +09:00
Naoki Takezoe
5870cacf44 (refs #488)Fix presentation of issue list header 2014-09-22 18:29:30 +09:00
Naoki Takezoe
cb512cd98d (refs #488)Add link to clear search condition 2014-09-22 10:18:26 +09:00
Naoki Takezoe
90487eb7b7 (refs #488)Merge user filter into IssueSearchCondition 2014-09-22 10:08:22 +09:00
Naoki Takezoe
706fa77de3 (refs #488)Add service.IssuesService.IssueInfo 2014-09-21 02:06:38 +09:00
bati11
26b14ded58 Add nested task list support 2014-09-20 10:57:33 +09:00
Naoki Takezoe
3b1367dd8e (refs #488)Displays the milestone on the issue list. 2014-09-20 03:02:52 +09:00
bati11
e1f310317d Modify GitBucketHtmlSerializer constructor parameters
- Add to the GitBucketHtmlSerializer constructor parameter "hasWritePermission"
- Remove the call to the RepositoryService.hasWritePermission in GitBucketHtmlSerializer
2014-09-19 14:13:53 +09:00
bati11
937814ec5d Merge branch 'master' into feature-tasklist
Conflicts:
	src/main/scala/app/IssuesController.scala
	src/main/twirl/issues/create.scala.html
2014-09-19 12:45:09 +09:00
bati11
b55fc649a6 Change crlf to lf 2014-09-19 12:43:04 +09:00
Naoki Takezoe
f4e4506517 (refs #488)Start to apply new issue UI to the issue list. 2014-09-16 00:46:38 +09:00
Naoki Takezoe
287a0b6669 (refs #488)Copy listparts.scala.html from issues/pulls to dashboard. 2014-09-15 23:47:06 +09:00
Naoki Takezoe
5bddd352af (refs #453)Fix "Test Hook" behavior 2014-09-15 13:04:39 +09:00
Naoki Takezoe
9c6ea8fb9d Fix client side validation error 2014-09-15 12:39:30 +09:00
Naoki Takezoe
32e8bf46a7 Merge pull request #491 from mrkm4ntr/myBranch
Change to show a shortcut path of the directory whose ancestors have only one child directory.
2014-09-15 02:12:52 +09:00
Naoki Takezoe
d61fe1bf84 (refs #483)Fix link in markdown 2014-09-14 20:31:02 +09:00
Naoki Takezoe
47dbea947d (refs #487) split diff is available 2014-09-14 10:53:17 +09:00
Naoki Takezoe
97c6b0495e Fix Warnings 2014-09-13 19:01:49 +09:00
Tomofumi Tanaka
a602ece8e9 (refs #490)Set HEAD ref when saved default branch
GitBucket allows user to configure default branch in the repository.
But it's only affect repository viewer in the GitBucekt world.
This change set default branch in the Git world.
2014-09-12 22:01:21 +09:00
Shintaro Murakami
cf6dca84d8 Release TreeWalk in recuresive function. 2014-09-11 00:06:28 +09:00
Shintaro Murakami
79432ff8ad Like GitHub, show a shortcut path of the directory whoes ancestors have only one child directory. 2014-09-10 07:07:04 +09:00
tanacasino
b8613431de Merge pull request #484 from douglarek/patch-1
Use a shebang (#!/bin/sh) to run sbt.sh
2014-09-04 11:42:17 +09:00
Lingchao Xin
698eafa562 Use a shebang (#!/bin/sh) to run sbt.sh
If no shebang provided in a shell file, it will be executed by default shell.

However this script is not compatible with Fish Shell, so use sh to execute it.
2014-09-04 10:25:24 +08:00
Naoki Takezoe
d33886db89 Add RawData and 404 error response for plugin action 2014-09-03 02:26:06 +09:00
Naoki Takezoe
cde09d3a59 Fix plugin path problem 2014-09-03 01:46:37 +09:00
bati11
6175eb7c08 Merge branch 'master' into feature-tasklist
Conflicts:
	src/main/twirl/issues/commentform.scala.html
	src/main/twirl/issues/create.scala.html
	src/main/twirl/pulls/compare.scala.html
	src/main/twirl/wiki/edit.scala.html
2014-05-31 12:17:30 +09:00
bati11
ebb9d9329a Merge branch 'master' into feature-tasklist 2014-05-03 10:51:18 +09:00
bati11
843722f82e Implement the feature "Task List" 2014-04-10 02:08:45 +09:00
bati11
ce79eaada8 Add escapeTaskList method, it escapse '- [] ' characters 2014-04-10 01:21:55 +09:00
102 changed files with 2862 additions and 3065 deletions

View File

@@ -80,6 +80,23 @@ Run the following commands in `Terminal` to
Release Notes Release Notes
-------- --------
### 2.5 - 4 Nov 2014
- New Dashboard
- Change datetime format
- Create branch from Web UI
- Task list in Markdown
- Some bug fix and improvements
### 2.4.1 - 6 Oct 2014
- Bug fix
### 2.4 - 6 Oct 2014
- New UI is applied to Issues and Pull requests
- Side-by-side diff is available
- Fix relative path problem in Markdown links and images
- Plugin System is disabled in default
- Some bug fix and improvements
### 2.3 - 1 Sep 2014 ### 2.3 - 1 Sep 2014
- Scala based plugin system - Scala based plugin system
- Embedded Jetty war extraction directory moved to `GITBUCKET_HOME/tmp` - Embedded Jetty war extraction directory moved to `GITBUCKET_HOME/tmp`

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 87 KiB

After

Width:  |  Height:  |  Size: 78 KiB

View File

@@ -42,7 +42,7 @@ object MyBuild extends Build {
"org.apache.commons" % "commons-email" % "1.3.1", "org.apache.commons" % "commons-email" % "1.3.1",
"org.apache.httpcomponents" % "httpclient" % "4.3", "org.apache.httpcomponents" % "httpclient" % "4.3",
"org.apache.sshd" % "apache-sshd" % "0.11.0", "org.apache.sshd" % "apache-sshd" % "0.11.0",
"com.typesafe.slick" %% "slick" % "2.1.0-RC3", "com.typesafe.slick" %% "slick" % "2.1.0",
"com.novell.ldap" % "jldap" % "2009-10-07", "com.novell.ldap" % "jldap" % "2009-10-07",
"org.quartz-scheduler" % "quartz" % "2.2.1", "org.quartz-scheduler" % "quartz" % "2.2.1",
"com.h2database" % "h2" % "1.4.180", "com.h2database" % "h2" % "1.4.180",
@@ -53,7 +53,7 @@ object MyBuild extends Build {
"com.typesafe.play" %% "twirl-compiler" % "1.0.2" "com.typesafe.play" %% "twirl-compiler" % "1.0.2"
), ),
EclipseKeys.withSource := true, EclipseKeys.withSource := true,
javacOptions in compile ++= Seq("-target", "6", "-source", "6"), javacOptions in compile ++= Seq("-target", "7", "-source", "7"),
testOptions in Test += Tests.Argument(TestFrameworks.Specs2, "junitxml", "console"), testOptions in Test += Tests.Argument(TestFrameworks.Specs2, "junitxml", "console"),
packageOptions += Package.MainClass("JettyLauncher") packageOptions += Package.MainClass("JettyLauncher")
).enablePlugins(SbtTwirl) ).enablePlugins(SbtTwirl)

1
sbt.sh
View File

@@ -1 +1,2 @@
#!/bin/sh
java -Dsbt.log.noformat=true -XX:+CMSClassUnloadingEnabled -XX:MaxPermSize=256m -Xmx512M -Xss2M -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5005 -jar `dirname $0`/sbt-launch-0.13.5.jar "$@" java -Dsbt.log.noformat=true -XX:+CMSClassUnloadingEnabled -XX:MaxPermSize=256m -Xmx512M -Xss2M -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5005 -jar `dirname $0`/sbt-launch-0.13.5.jar "$@"

View File

@@ -9,10 +9,11 @@ class DashboardController extends DashboardControllerBase
with UsersAuthenticator with UsersAuthenticator
trait DashboardControllerBase extends ControllerBase { trait DashboardControllerBase extends ControllerBase {
self: IssuesService with PullRequestService with RepositoryService with UsersAuthenticator => self: IssuesService with PullRequestService with RepositoryService with AccountService
with UsersAuthenticator =>
get("/dashboard/issues/repos")(usersOnly { get("/dashboard/issues/repos")(usersOnly {
searchIssues("all") searchIssues("created_by")
}) })
get("/dashboard/issues/assigned")(usersOnly { get("/dashboard/issues/assigned")(usersOnly {
@@ -23,6 +24,10 @@ trait DashboardControllerBase extends ControllerBase {
searchIssues("created_by") searchIssues("created_by")
}) })
get("/dashboard/issues/mentioned")(usersOnly {
searchIssues("mentioned")
})
get("/dashboard/pulls")(usersOnly { get("/dashboard/pulls")(usersOnly {
searchPullRequests("created_by", None) searchPullRequests("created_by", None)
}) })
@@ -31,6 +36,10 @@ trait DashboardControllerBase extends ControllerBase {
searchPullRequests("created_by", None) searchPullRequests("created_by", None)
}) })
get("/dashboard/pulls/mentioned")(usersOnly {
searchPullRequests("mentioned", None)
})
get("/dashboard/pulls/public")(usersOnly { get("/dashboard/pulls/public")(usersOnly {
searchPullRequests("not_created_by", None) searchPullRequests("not_created_by", None)
}) })
@@ -54,19 +63,13 @@ trait DashboardControllerBase extends ControllerBase {
val page = IssueSearchCondition.page(request) val page = IssueSearchCondition.page(request)
dashboard.html.issues( dashboard.html.issues(
issues.html.listparts( searchIssue(condition, filterUser, false, (page - 1) * IssueLimit, IssueLimit, userRepos: _*),
searchIssue(condition, filterUser, false, (page - 1) * IssueLimit, IssueLimit, userRepos: _*), page,
page, countIssue(condition.copy(state = "open" ), filterUser, false, userRepos: _*),
countIssue(condition.copy(state = "open" ), filterUser, false, userRepos: _*), countIssue(condition.copy(state = "closed"), filterUser, false, userRepos: _*),
countIssue(condition.copy(state = "closed"), filterUser, false, userRepos: _*), condition,
condition), filter,
countIssue(condition, Map.empty, false, userRepos: _*), getGroupNames(userName))
countIssue(condition, Map("assigned" -> userName), false, userRepos: _*),
countIssue(condition, Map("created_by" -> userName), false, userRepos: _*),
countIssueGroupByRepository(condition, filterUser, false, userRepos: _*),
condition,
filter)
} }
private def searchPullRequests(filter: String, repository: Option[String]) = { private def searchPullRequests(filter: String, repository: Option[String]) = {
@@ -80,30 +83,18 @@ trait DashboardControllerBase extends ControllerBase {
}.copy(repo = repository)) }.copy(repo = repository))
val userName = context.loginAccount.get.userName val userName = context.loginAccount.get.userName
val allRepos = getAllRepositories() val allRepos = getAllRepositories(userName)
val userRepos = getUserRepositories(userName, context.baseUrl, true).map(repo => repo.owner -> repo.name)
val filterUser = Map(filter -> userName) val filterUser = Map(filter -> userName)
val page = IssueSearchCondition.page(request) val page = IssueSearchCondition.page(request)
val counts = countIssueGroupByRepository(
IssueSearchCondition().copy(state = condition.state), Map.empty, true, userRepos: _*)
dashboard.html.pulls( dashboard.html.pulls(
pulls.html.listparts( searchIssue(condition, filterUser, true, (page - 1) * PullRequestLimit, PullRequestLimit, allRepos: _*),
searchIssue(condition, filterUser, true, (page - 1) * PullRequestLimit, PullRequestLimit, allRepos: _*), page,
page, countIssue(condition.copy(state = "open" ), filterUser, true, allRepos: _*),
countIssue(condition.copy(state = "open" ), filterUser, true, allRepos: _*), countIssue(condition.copy(state = "closed"), filterUser, true, allRepos: _*),
countIssue(condition.copy(state = "closed"), filterUser, true, allRepos: _*),
condition,
None,
false),
getPullRequestCountGroupByUser(condition.state == "closed", None, None),
userRepos.map { case (userName, repoName) =>
(userName, repoName, counts.find { x => x._1 == userName && x._2 == repoName }.map(_._3).getOrElse(0))
}.sortBy(_._3).reverse,
condition, condition,
filter) filter,
getGroupNames(userName))
} }

View File

@@ -21,7 +21,6 @@ trait IssuesControllerBase extends ControllerBase {
case class IssueCreateForm(title: String, content: Option[String], case class IssueCreateForm(title: String, content: Option[String],
assignedUserName: Option[String], milestoneId: Option[Int], labelNames: Option[String]) assignedUserName: Option[String], milestoneId: Option[Int], labelNames: Option[String])
case class IssueEditForm(title: String, content: Option[String])
case class CommentForm(issueId: Int, content: String) case class CommentForm(issueId: Int, content: String)
case class IssueStateForm(issueId: Int, content: Option[String]) case class IssueStateForm(issueId: Int, content: Option[String])
@@ -33,10 +32,12 @@ trait IssuesControllerBase extends ControllerBase {
"labelNames" -> trim(optional(text())) "labelNames" -> trim(optional(text()))
)(IssueCreateForm.apply) )(IssueCreateForm.apply)
val issueTitleEditForm = mapping(
"title" -> trim(label("Title", text(required)))
)(x => x)
val issueEditForm = mapping( val issueEditForm = mapping(
"title" -> trim(label("Title", text(required))), "content" -> trim(optional(text()))
"content" -> trim(optional(text())) )(x => x)
)(IssueEditForm.apply)
val commentForm = mapping( val commentForm = mapping(
"issueId" -> label("Issue Id", number()), "issueId" -> label("Issue Id", number()),
@@ -48,16 +49,8 @@ trait IssuesControllerBase extends ControllerBase {
"content" -> trim(optional(text())) "content" -> trim(optional(text()))
)(IssueStateForm.apply) )(IssueStateForm.apply)
get("/:owner/:repository/issues")(referrersOnly { get("/:owner/:repository/issues")(referrersOnly { repository =>
searchIssues("all", _) searchIssues(repository)
})
get("/:owner/:repository/issues/assigned/:userName")(referrersOnly {
searchIssues("assigned", _)
})
get("/:owner/:repository/issues/created_by/:userName")(referrersOnly {
searchIssues("created_by", _)
}) })
get("/:owner/:repository/issues/:id")(referrersOnly { repository => get("/:owner/:repository/issues/:id")(referrersOnly { repository =>
@@ -126,14 +119,29 @@ trait IssuesControllerBase extends ControllerBase {
} }
}) })
ajaxPost("/:owner/:repository/issues/edit/:id", issueEditForm)(readableUsersOnly { (form, repository) => ajaxPost("/:owner/:repository/issues/edit_title/:id", issueTitleEditForm)(readableUsersOnly { (title, repository) =>
defining(repository.owner, repository.name){ case (owner, name) => defining(repository.owner, repository.name){ case (owner, name) =>
getIssue(owner, name, params("id")).map { issue => getIssue(owner, name, params("id")).map { issue =>
if(isEditable(owner, name, issue.openedUserName)){ if(isEditable(owner, name, issue.openedUserName)){
// update issue // update issue
updateIssue(owner, name, issue.issueId, form.title, form.content) updateIssue(owner, name, issue.issueId, title, issue.content)
// extract references and create refer comment // extract references and create refer comment
createReferComment(owner, name, issue, form.title + " " + form.content.getOrElse("")) createReferComment(owner, name, issue.copy(title = title), title)
redirect(s"/${owner}/${name}/issues/_data/${issue.issueId}")
} else Unauthorized
} getOrElse NotFound
}
})
ajaxPost("/:owner/:repository/issues/edit/:id", issueEditForm)(readableUsersOnly { (content, repository) =>
defining(repository.owner, repository.name){ case (owner, name) =>
getIssue(owner, name, params("id")).map { issue =>
if(isEditable(owner, name, issue.openedUserName)){
// update issue
updateIssue(owner, name, issue.issueId, issue.title, content)
// extract references and create refer comment
createReferComment(owner, name, issue, content.getOrElse(""))
redirect(s"/${owner}/${name}/issues/_data/${issue.issueId}") redirect(s"/${owner}/${name}/issues/_data/${issue.issueId}")
} else Unauthorized } else Unauthorized
@@ -181,13 +189,13 @@ trait IssuesControllerBase extends ControllerBase {
if(isEditable(x.userName, x.repositoryName, x.openedUserName)){ if(isEditable(x.userName, x.repositoryName, x.openedUserName)){
params.get("dataType") collect { params.get("dataType") collect {
case t if t == "html" => issues.html.editissue( case t if t == "html" => issues.html.editissue(
x.title, x.content, x.issueId, x.userName, x.repositoryName) x.content, x.issueId, x.userName, x.repositoryName)
} getOrElse { } getOrElse {
contentType = formats("json") contentType = formats("json")
org.json4s.jackson.Serialization.write( org.json4s.jackson.Serialization.write(
Map("title" -> x.title, Map("title" -> x.title,
"content" -> view.Markdown.toHtml(x.content getOrElse "No description given.", "content" -> view.Markdown.toHtml(x.content getOrElse "No description given.",
repository, false, true) repository, false, true, true, isEditable(x.userName, x.repositoryName, x.openedUserName))
)) ))
} }
} else Unauthorized } else Unauthorized
@@ -204,7 +212,7 @@ trait IssuesControllerBase extends ControllerBase {
contentType = formats("json") contentType = formats("json")
org.json4s.jackson.Serialization.write( org.json4s.jackson.Serialization.write(
Map("content" -> view.Markdown.toHtml(x.content, Map("content" -> view.Markdown.toHtml(x.content,
repository, false, true) repository, false, true, true, isEditable(x.userName, x.repositoryName, x.commentedUserName))
)) ))
} }
} else Unauthorized } else Unauthorized
@@ -235,15 +243,17 @@ trait IssuesControllerBase extends ControllerBase {
milestoneId("milestoneId").map { milestoneId => milestoneId("milestoneId").map { milestoneId =>
getMilestonesWithIssueCount(repository.owner, repository.name) getMilestonesWithIssueCount(repository.owner, repository.name)
.find(_._1.milestoneId == milestoneId).map { case (_, openCount, closeCount) => .find(_._1.milestoneId == milestoneId).map { case (_, openCount, closeCount) =>
issues.milestones.html.progress(openCount + closeCount, closeCount, false) issues.milestones.html.progress(openCount + closeCount, closeCount)
} getOrElse NotFound } getOrElse NotFound
} getOrElse Ok() } getOrElse Ok()
}) })
post("/:owner/:repository/issues/batchedit/state")(collaboratorsOnly { repository => post("/:owner/:repository/issues/batchedit/state")(collaboratorsOnly { repository =>
defining(params.get("value")){ action => defining(params.get("value")){ action =>
executeBatch(repository) { action match {
handleComment(_, None, repository)( _ => action) case Some("open") => executeBatch(repository) { handleComment(_, None, repository)( _ => Some("reopen")) }
case Some("close") => executeBatch(repository) { handleComment(_, None, repository)( _ => Some("close")) }
case _ => // TODO BadRequest
} }
} }
}) })
@@ -293,7 +303,10 @@ trait IssuesControllerBase extends ControllerBase {
private def executeBatch(repository: RepositoryService.RepositoryInfo)(execute: Int => Unit) = { private def executeBatch(repository: RepositoryService.RepositoryInfo)(execute: Int => Unit) = {
params("checked").split(',') map(_.toInt) foreach execute params("checked").split(',') map(_.toInt) foreach execute
redirect(s"/${repository.owner}/${repository.name}/issues") params("from") match {
case "issues" => redirect(s"/${repository.owner}/${repository.name}/issues")
case "pulls" => redirect(s"/${repository.owner}/${repository.name}/pulls")
}
} }
private def createReferComment(owner: String, repository: String, fromIssue: Issue, message: String) = { private def createReferComment(owner: String, repository: String, fromIssue: Issue, message: String) = {
@@ -319,15 +332,15 @@ trait IssuesControllerBase extends ControllerBase {
val (action, recordActivity) = val (action, recordActivity) =
getAction(issue) getAction(issue)
.collect { .collect {
case "close" => true -> (Some("close") -> case "close" if(!issue.closed) => true ->
Some(if(issue.isPullRequest) recordClosePullRequestActivity _ else recordCloseIssueActivity _)) (Some("close") -> Some(if(issue.isPullRequest) recordClosePullRequestActivity _ else recordCloseIssueActivity _))
case "reopen" => false -> (Some("reopen") -> case "reopen" if(issue.closed) => false ->
Some(recordReopenIssueActivity _)) (Some("reopen") -> Some(recordReopenIssueActivity _))
} }
.map { case (closed, t) => .map { case (closed, t) =>
updateClosed(owner, name, issueId, closed) updateClosed(owner, name, issueId, closed)
t t
} }
.getOrElse(None -> None) .getOrElse(None -> None)
val commentId = content val commentId = content
@@ -337,7 +350,7 @@ trait IssuesControllerBase extends ControllerBase {
case (content, action) => createComment(owner, name, userName, issueId, content, action) case (content, action) => createComment(owner, name, userName, issueId, content, action)
} }
// record activity // record comment activity if comment is entered
content foreach { content foreach {
(if(issue.isPullRequest) recordCommentPullRequestActivity _ else recordCommentIssueActivity _) (if(issue.isPullRequest) recordCommentPullRequestActivity _ else recordCommentIssueActivity _)
(owner, name, userName, issueId, _) (owner, name, userName, issueId, _)
@@ -370,9 +383,8 @@ trait IssuesControllerBase extends ControllerBase {
} }
} }
private def searchIssues(filter: String, repository: RepositoryService.RepositoryInfo) = { private def searchIssues(repository: RepositoryService.RepositoryInfo) = {
defining(repository.owner, repository.name){ case (owner, repoName) => defining(repository.owner, repository.name){ case (owner, repoName) =>
val filterUser = Map(filter -> params.getOrElse("userName", ""))
val page = IssueSearchCondition.page(request) val page = IssueSearchCondition.page(request)
val sessionKey = Keys.Session.Issues(owner, repoName) val sessionKey = Keys.Session.Issues(owner, repoName)
@@ -383,19 +395,15 @@ trait IssuesControllerBase extends ControllerBase {
) )
issues.html.list( issues.html.list(
searchIssue(condition, filterUser, false, (page - 1) * IssueLimit, IssueLimit, owner -> repoName), "issues",
searchIssue(condition, Map.empty, false, (page - 1) * IssueLimit, IssueLimit, owner -> repoName),
page, page,
(getCollaborators(owner, repoName) :+ owner).sorted, (getCollaborators(owner, repoName) :+ owner).sorted,
getMilestones(owner, repoName), getMilestones(owner, repoName),
getLabels(owner, repoName), getLabels(owner, repoName),
countIssue(condition.copy(state = "open"), filterUser, false, owner -> repoName), countIssue(condition.copy(state = "open" ), Map.empty, false, owner -> repoName),
countIssue(condition.copy(state = "closed"), filterUser, false, owner -> repoName), countIssue(condition.copy(state = "closed"), Map.empty, false, owner -> repoName),
countIssue(condition, Map.empty, false, owner -> repoName),
context.loginAccount.map(x => countIssue(condition, Map("assigned" -> x.userName), false, owner -> repoName)),
context.loginAccount.map(x => countIssue(condition, Map("created_by" -> x.userName), false, owner -> repoName)),
countIssueGroupByLabels(owner, repoName, condition, filterUser),
condition, condition,
filter,
repository, repository,
hasWritePermission(owner, repoName, context.loginAccount)) hasWritePermission(owner, repoName, context.loginAccount))
} }

View File

@@ -2,51 +2,67 @@ package app
import jp.sf.amateras.scalatra.forms._ import jp.sf.amateras.scalatra.forms._
import service._ import service._
import util.CollaboratorsAuthenticator import util.{ReferrerAuthenticator, CollaboratorsAuthenticator}
import util.Implicits._ import util.Implicits._
import org.scalatra.i18n.Messages import org.scalatra.i18n.Messages
import org.scalatra.Ok
class LabelsController extends LabelsControllerBase class LabelsController extends LabelsControllerBase
with LabelsService with RepositoryService with AccountService with CollaboratorsAuthenticator with LabelsService with IssuesService with RepositoryService with AccountService
with ReferrerAuthenticator with CollaboratorsAuthenticator
trait LabelsControllerBase extends ControllerBase { trait LabelsControllerBase extends ControllerBase {
self: LabelsService with RepositoryService with CollaboratorsAuthenticator => self: LabelsService with IssuesService with RepositoryService
with ReferrerAuthenticator with CollaboratorsAuthenticator =>
case class LabelForm(labelName: String, color: String) case class LabelForm(labelName: String, color: String)
val newForm = mapping( val labelForm = mapping(
"newLabelName" -> trim(label("Label name", text(required, labelName, maxlength(100)))), "labelName" -> trim(label("Label name", text(required, labelName, maxlength(100)))),
"newColor" -> trim(label("Color", text(required, color))) "labelColor" -> trim(label("Color", text(required, color)))
)(LabelForm.apply) )(LabelForm.apply)
val editForm = mapping( get("/:owner/:repository/issues/labels")(referrersOnly { repository =>
"editLabelName" -> trim(label("Label name", text(required, labelName, maxlength(100)))), issues.labels.html.list(
"editColor" -> trim(label("Color", text(required, color))) getLabels(repository.owner, repository.name),
)(LabelForm.apply) countIssueGroupByLabels(repository.owner, repository.name, IssuesService.IssueSearchCondition(), Map.empty),
repository,
post("/:owner/:repository/issues/label/new", newForm)(collaboratorsOnly { (form, repository) => hasWritePermission(repository.owner, repository.name, context.loginAccount))
createLabel(repository.owner, repository.name, form.labelName, form.color.substring(1))
redirect(s"/${repository.owner}/${repository.name}/issues")
}) })
ajaxGet("/:owner/:repository/issues/label/edit")(collaboratorsOnly { repository => ajaxGet("/:owner/:repository/issues/labels/new")(collaboratorsOnly { repository =>
issues.labels.html.editlist(getLabels(repository.owner, repository.name), repository) issues.labels.html.edit(None, repository)
}) })
ajaxGet("/:owner/:repository/issues/label/:labelId/edit")(collaboratorsOnly { repository => ajaxPost("/:owner/:repository/issues/labels/new", labelForm)(collaboratorsOnly { (form, repository) =>
val labelId = createLabel(repository.owner, repository.name, form.labelName, form.color.substring(1))
issues.labels.html.label(
getLabel(repository.owner, repository.name, labelId).get,
// TODO futility
countIssueGroupByLabels(repository.owner, repository.name, IssuesService.IssueSearchCondition(), Map.empty),
repository,
hasWritePermission(repository.owner, repository.name, context.loginAccount))
})
ajaxGet("/:owner/:repository/issues/labels/:labelId/edit")(collaboratorsOnly { repository =>
getLabel(repository.owner, repository.name, params("labelId").toInt).map { label => getLabel(repository.owner, repository.name, params("labelId").toInt).map { label =>
issues.labels.html.edit(Some(label), repository) issues.labels.html.edit(Some(label), repository)
} getOrElse NotFound() } getOrElse NotFound()
}) })
ajaxPost("/:owner/:repository/issues/label/:labelId/edit", editForm)(collaboratorsOnly { (form, repository) => ajaxPost("/:owner/:repository/issues/labels/:labelId/edit", labelForm)(collaboratorsOnly { (form, repository) =>
updateLabel(repository.owner, repository.name, params("labelId").toInt, form.labelName, form.color.substring(1)) updateLabel(repository.owner, repository.name, params("labelId").toInt, form.labelName, form.color.substring(1))
issues.labels.html.editlist(getLabels(repository.owner, repository.name), repository) issues.labels.html.label(
getLabel(repository.owner, repository.name, params("labelId").toInt).get,
// TODO futility
countIssueGroupByLabels(repository.owner, repository.name, IssuesService.IssueSearchCondition(), Map.empty),
repository,
hasWritePermission(repository.owner, repository.name, context.loginAccount))
}) })
ajaxGet("/:owner/:repository/issues/label/:labelId/delete")(collaboratorsOnly { repository => ajaxPost("/:owner/:repository/issues/labels/:labelId/delete")(collaboratorsOnly { repository =>
deleteLabel(repository.owner, repository.name, params("labelId").toInt) deleteLabel(repository.owner, repository.name, params("labelId").toInt)
issues.labels.html.editlist(getLabels(repository.owner, repository.name), repository) Ok()
}) })
/** /**

View File

@@ -62,10 +62,6 @@ trait PullRequestsControllerBase extends ControllerBase {
searchPullRequests(None, repository) searchPullRequests(None, repository)
}) })
get("/:owner/:repository/pulls/:userName")(referrersOnly { repository =>
searchPullRequests(Some(params("userName")), repository)
})
get("/:owner/:repository/pull/:id")(referrersOnly { repository => get("/:owner/:repository/pull/:id")(referrersOnly { repository =>
params("id").toIntOpt.flatMap{ issueId => params("id").toIntOpt.flatMap{ issueId =>
val owner = repository.owner val owner = repository.owner
@@ -453,7 +449,6 @@ trait PullRequestsControllerBase extends ControllerBase {
private def searchPullRequests(userName: Option[String], repository: RepositoryService.RepositoryInfo) = private def searchPullRequests(userName: Option[String], repository: RepositoryService.RepositoryInfo) =
defining(repository.owner, repository.name){ case (owner, repoName) => defining(repository.owner, repository.name){ case (owner, repoName) =>
val filterUser = userName.map { x => Map("created_by" -> x) } getOrElse Map("all" -> "")
val page = IssueSearchCondition.page(request) val page = IssueSearchCondition.page(request)
val sessionKey = Keys.Session.Pulls(owner, repoName) val sessionKey = Keys.Session.Pulls(owner, repoName)
@@ -463,14 +458,15 @@ trait PullRequestsControllerBase extends ControllerBase {
else session.getAs[IssueSearchCondition](sessionKey).getOrElse(IssueSearchCondition()) else session.getAs[IssueSearchCondition](sessionKey).getOrElse(IssueSearchCondition())
) )
pulls.html.list( issues.html.list(
searchIssue(condition, filterUser, true, (page - 1) * PullRequestLimit, PullRequestLimit, owner -> repoName), "pulls",
getPullRequestCountGroupByUser(condition.state == "closed", Some(owner), Some(repoName)), searchIssue(condition, Map.empty, true, (page - 1) * PullRequestLimit, PullRequestLimit, owner -> repoName),
userName,
page, page,
countIssue(condition.copy(state = "open" ), filterUser, true, owner -> repoName), (getCollaborators(owner, repoName) :+ owner).sorted,
countIssue(condition.copy(state = "closed"), filterUser, true, owner -> repoName), getMilestones(owner, repoName),
countIssue(condition, Map.empty, true, owner -> repoName), getLabels(owner, repoName),
countIssue(condition.copy(state = "open" ), Map.empty, true, owner -> repoName),
countIssue(condition.copy(state = "closed"), Map.empty, true, owner -> repoName),
condition, condition,
repository, repository,
hasWritePermission(owner, repoName, context.loginAccount)) hasWritePermission(owner, repoName, context.loginAccount))

View File

@@ -2,10 +2,8 @@ package app
import service._ import service._
import util.Directory._ import util.Directory._
import util.ControlUtil._
import util.Implicits._ import util.Implicits._
import util.{LockUtil, UsersAuthenticator, OwnerAuthenticator} import util.{LockUtil, UsersAuthenticator, OwnerAuthenticator}
import util.JGitUtil.CommitInfo
import jp.sf.amateras.scalatra.forms._ import jp.sf.amateras.scalatra.forms._
import org.apache.commons.io.FileUtils import org.apache.commons.io.FileUtils
import org.scalatra.i18n.Messages import org.scalatra.i18n.Messages
@@ -13,6 +11,7 @@ import service.WebHookService.WebHookPayload
import util.JGitUtil.CommitInfo import util.JGitUtil.CommitInfo
import util.ControlUtil._ import util.ControlUtil._
import org.eclipse.jgit.api.Git import org.eclipse.jgit.api.Git
import org.eclipse.jgit.lib.Constants
class RepositorySettingsController extends RepositorySettingsControllerBase class RepositorySettingsController extends RepositorySettingsControllerBase
with RepositoryService with AccountService with WebHookService with RepositoryService with AccountService with WebHookService
@@ -71,11 +70,12 @@ trait RepositorySettingsControllerBase extends ControllerBase {
* Save the repository options. * Save the repository options.
*/ */
post("/:owner/:repository/settings/options", optionsForm)(ownerOnly { (form, repository) => post("/:owner/:repository/settings/options", optionsForm)(ownerOnly { (form, repository) =>
val defaultBranch = if(repository.branchList.isEmpty) "master" else form.defaultBranch
saveRepositoryOptions( saveRepositoryOptions(
repository.owner, repository.owner,
repository.name, repository.name,
form.description, form.description,
if(repository.branchList.isEmpty) "master" else form.defaultBranch, defaultBranch,
repository.repository.parentUserName.map { _ => repository.repository.parentUserName.map { _ =>
repository.repository.isPrivate repository.repository.isPrivate
} getOrElse form.isPrivate } getOrElse form.isPrivate
@@ -93,6 +93,10 @@ trait RepositorySettingsControllerBase extends ControllerBase {
FileUtils.moveDirectory(dir, getWikiRepositoryDir(repository.owner, form.repositoryName)) FileUtils.moveDirectory(dir, getWikiRepositoryDir(repository.owner, form.repositoryName))
} }
} }
// Change repository HEAD
using(Git.open(getRepositoryDir(repository.owner, form.repositoryName))) { git =>
git.getRepository.updateRef(Constants.HEAD, true).link(Constants.R_HEADS + defaultBranch)
}
flash += "info" -> "Repository settings has been updated." flash += "info" -> "Repository settings has been updated."
redirect(s"/${repository.owner}/${form.repositoryName}/settings/options") redirect(s"/${repository.owner}/${form.repositoryName}/settings/options")
}) })
@@ -131,7 +135,7 @@ trait RepositorySettingsControllerBase extends ControllerBase {
* Display the web hook page. * Display the web hook page.
*/ */
get("/:owner/:repository/settings/hooks")(ownerOnly { repository => get("/:owner/:repository/settings/hooks")(ownerOnly { repository =>
settings.html.hooks(getWebHookURLs(repository.owner, repository.name), repository, flash.get("info")) settings.html.hooks(getWebHookURLs(repository.owner, repository.name), flash.get("url"), repository, flash.get("info"))
}) })
/** /**
@@ -153,7 +157,7 @@ trait RepositorySettingsControllerBase extends ControllerBase {
/** /**
* Send the test request to registered web hook URLs. * Send the test request to registered web hook URLs.
*/ */
get("/:owner/:repository/settings/hooks/test")(ownerOnly { repository => post("/:owner/:repository/settings/hooks/test", webHookForm)(ownerOnly { (form, repository) =>
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git => using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
import scala.collection.JavaConverters._ import scala.collection.JavaConverters._
val commits = git.log val commits = git.log
@@ -161,15 +165,13 @@ trait RepositorySettingsControllerBase extends ControllerBase {
.setMaxCount(3) .setMaxCount(3)
.call.iterator.asScala.map(new CommitInfo(_)) .call.iterator.asScala.map(new CommitInfo(_))
getWebHookURLs(repository.owner, repository.name) match { getAccountByUserName(repository.owner).foreach { ownerAccount =>
case webHookURLs if(webHookURLs.nonEmpty) => callWebHook(repository.owner, repository.name,
for(ownerAccount <- getAccountByUserName(repository.owner)){ List(model.WebHook(repository.owner, repository.name, form.url)),
callWebHook(repository.owner, repository.name, webHookURLs, WebHookPayload(git, ownerAccount, "refs/heads/" + repository.repository.defaultBranch, repository, commits.toList, ownerAccount)
WebHookPayload(git, ownerAccount, "refs/heads/" + repository.repository.defaultBranch, repository, commits.toList, ownerAccount)) )
}
case _ =>
} }
flash += "url" -> form.url
flash += "info" -> "Test payload deployed!" flash += "info" -> "Test payload deployed!"
} }
redirect(s"/${repository.owner}/${repository.name}/settings/hooks") redirect(s"/${repository.owner}/${repository.name}/settings/hooks")

View File

@@ -77,7 +77,9 @@ trait RepositoryViewerControllerBase extends ControllerBase {
contentType = "text/html" contentType = "text/html"
view.helpers.markdown(params("content"), repository, view.helpers.markdown(params("content"), repository,
params("enableWikiLink").toBoolean, params("enableWikiLink").toBoolean,
params("enableRefsLink").toBoolean) params("enableRefsLink").toBoolean,
params("enableTaskList").toBoolean,
hasWritePermission(repository.owner, repository.name, context.loginAccount))
}) })
/** /**
@@ -112,7 +114,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
repo.html.commits(if(path.isEmpty) Nil else path.split("/").toList, branchName, repository, repo.html.commits(if(path.isEmpty) Nil else path.split("/").toList, branchName, repository,
logs.splitWith{ (commit1, commit2) => logs.splitWith{ (commit1, commit2) =>
view.helpers.date(commit1.commitTime) == view.helpers.date(commit2.commitTime) view.helpers.date(commit1.commitTime) == view.helpers.date(commit2.commitTime)
}, page, hasNext) }, page, hasNext, hasWritePermission(repository.owner, repository.name, context.loginAccount))
case Left(_) => NotFound case Left(_) => NotFound
} }
} }
@@ -239,6 +241,24 @@ trait RepositoryViewerControllerBase extends ControllerBase {
} }
}) })
/**
* Creates a branch.
*/
post("/:owner/:repository/branches")(collaboratorsOnly { repository =>
val newBranchName = params.getOrElse("new", halt(400))
val fromBranchName = params.getOrElse("from", halt(400))
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
JGitUtil.createBranch(git, fromBranchName, newBranchName)
} match {
case Right(message) =>
flash += "info" -> message
redirect(s"/${repository.owner}/${repository.name}/tree/${StringUtil.urlEncode(newBranchName).replace("%2F", "/")}")
case Left(message) =>
flash += "error" -> message
redirect(s"/${repository.owner}/${repository.name}/tree/${fromBranchName}")
}
})
/** /**
* Deletes branch. * Deletes branch.
*/ */
@@ -331,7 +351,8 @@ trait RepositoryViewerControllerBase extends ControllerBase {
repo.html.files(revision, repository, repo.html.files(revision, repository,
if(path == ".") Nil else path.split("/").toList, // current path if(path == ".") Nil else path.split("/").toList, // current path
new JGitUtil.CommitInfo(lastModifiedCommit), // last modified commit new JGitUtil.CommitInfo(lastModifiedCommit), // last modified commit
files, readme, hasWritePermission(repository.owner, repository.name, context.loginAccount)) files, readme, hasWritePermission(repository.owner, repository.name, context.loginAccount),
flash.get("info"), flash.get("error"))
} }
} getOrElse NotFound } getOrElse NotFound
} }

View File

@@ -85,41 +85,55 @@ trait SystemSettingsControllerBase extends ControllerBase {
}) })
get("/admin/plugins")(adminOnly { get("/admin/plugins")(adminOnly {
val installedPlugins = plugin.PluginSystem.plugins if(enablePluginSystem){
val updatablePlugins = getAvailablePlugins(installedPlugins).filter(_.status == "updatable") val installedPlugins = plugin.PluginSystem.plugins
admin.plugins.html.installed(installedPlugins, updatablePlugins) val updatablePlugins = getAvailablePlugins(installedPlugins).filter(_.status == "updatable")
admin.plugins.html.installed(installedPlugins, updatablePlugins)
} else NotFound
}) })
post("/admin/plugins/_update", pluginForm)(adminOnly { form => post("/admin/plugins/_update", pluginForm)(adminOnly { form =>
deletePlugins(form.pluginIds) if(enablePluginSystem){
installPlugins(form.pluginIds) deletePlugins(form.pluginIds)
redirect("/admin/plugins") installPlugins(form.pluginIds)
redirect("/admin/plugins")
} else NotFound
}) })
post("/admin/plugins/_delete", pluginForm)(adminOnly { form => post("/admin/plugins/_delete", pluginForm)(adminOnly { form =>
deletePlugins(form.pluginIds) if(enablePluginSystem){
redirect("/admin/plugins") deletePlugins(form.pluginIds)
redirect("/admin/plugins")
} else NotFound
}) })
get("/admin/plugins/available")(adminOnly { get("/admin/plugins/available")(adminOnly {
val installedPlugins = plugin.PluginSystem.plugins if(enablePluginSystem){
val availablePlugins = getAvailablePlugins(installedPlugins).filter(_.status == "available") val installedPlugins = plugin.PluginSystem.plugins
admin.plugins.html.available(availablePlugins) val availablePlugins = getAvailablePlugins(installedPlugins).filter(_.status == "available")
admin.plugins.html.available(availablePlugins)
} else NotFound
}) })
post("/admin/plugins/_install", pluginForm)(adminOnly { form => post("/admin/plugins/_install", pluginForm)(adminOnly { form =>
installPlugins(form.pluginIds) if(enablePluginSystem){
redirect("/admin/plugins") installPlugins(form.pluginIds)
redirect("/admin/plugins")
} else NotFound
}) })
get("/admin/plugins/console")(adminOnly { get("/admin/plugins/console")(adminOnly {
admin.plugins.html.console() if(enablePluginSystem){
admin.plugins.html.console()
} else NotFound
}) })
post("/admin/plugins/console")(adminOnly { post("/admin/plugins/console")(adminOnly {
val script = request.getParameter("script") if(enablePluginSystem){
val result = plugin.ScalaPlugin.eval(script) val script = request.getParameter("script")
Ok() val result = plugin.ScalaPlugin.eval(script)
Ok()
} else NotFound
}) })
// TODO Move these methods to PluginSystem or Service? // TODO Move these methods to PluginSystem or Service?

View File

@@ -49,7 +49,7 @@ trait UserManagementControllerBase extends AccountManagementControllerBase {
"url" -> trim(label("URL" ,optional(text(maxlength(200))))), "url" -> trim(label("URL" ,optional(text(maxlength(200))))),
"fileId" -> trim(label("File ID" ,optional(text()))), "fileId" -> trim(label("File ID" ,optional(text()))),
"clearImage" -> trim(label("Clear image" ,boolean())), "clearImage" -> trim(label("Clear image" ,boolean())),
"removed" -> trim(label("Disable" ,boolean())) "removed" -> trim(label("Disable" ,boolean(disableByNotYourself("userName"))))
)(EditUserForm.apply) )(EditUserForm.apply)
val newGroupForm = mapping( val newGroupForm = mapping(
@@ -190,4 +190,14 @@ trait UserManagementControllerBase extends AccountManagementControllerBase {
} }
} }
protected def disableByNotYourself(paramName: String): Constraint = new Constraint() {
override def validate(name: String, value: String, messages: Messages): Option[String] = {
params.get(paramName).flatMap { userName =>
if(userName == context.loginAccount.get.userName)
Some("You can't disable your account yourself")
else
None
}
}
}
} }

View File

@@ -31,7 +31,7 @@ case class Label(
if(Integer.parseInt(r, 16) + Integer.parseInt(g, 16) + Integer.parseInt(b, 16) > 408){ if(Integer.parseInt(r, 16) + Integer.parseInt(g, 16) + Integer.parseInt(b, 16) > 408){
"000000" "000000"
} else { } else {
"FFFFFF" "ffffff"
} }
} }
} }

View File

@@ -7,6 +7,7 @@ package object plugin {
case class Redirect(path: String) case class Redirect(path: String)
case class Fragment(html: Html) case class Fragment(html: Html)
case class RawData(contentType: String, content: Array[Byte])
object db { object db {
// TODO labelled place holder support // TODO labelled place holder support

View File

@@ -168,6 +168,11 @@ trait AccountService {
Repositories.filter(_.userName === userName.bind).delete Repositories.filter(_.userName === userName.bind).delete
} }
def getGroupNames(userName: String)(implicit s: Session): List[String] = {
List(userName) ++
Collaborators.filter(_.collaboratorName === userName.bind).sortBy(_.userName).map(_.userName).list
}
} }
object AccountService extends AccountService object AccountService extends AccountService

View File

@@ -43,7 +43,6 @@ trait IssuesService {
* Returns the count of the search result against issues. * Returns the count of the search result against issues.
* *
* @param condition the search condition * @param condition the search condition
* @param filterUser the filter user name (key is "all", "assigned" or "created_by", value is the user name)
* @param onlyPullRequest if true then counts only pull request, false then counts both of issue and pull request. * @param onlyPullRequest if true then counts only pull request, false then counts both of issue and pull request.
* @param repos Tuple of the repository owner and the repository name * @param repos Tuple of the repository owner and the repository name
* @return the count of the search result * @return the count of the search result
@@ -58,7 +57,6 @@ trait IssuesService {
* @param owner the repository owner * @param owner the repository owner
* @param repository the repository name * @param repository the repository name
* @param condition the search condition * @param condition the search condition
* @param filterUser the filter user name (key is "all", "assigned" or "created_by", value is the user name)
* @return the Map which contains issue count for each labels (key is label name, value is issue count) * @return the Map which contains issue count for each labels (key is label name, value is issue count)
*/ */
def countIssueGroupByLabels(owner: String, repository: String, condition: IssueSearchCondition, def countIssueGroupByLabels(owner: String, repository: String, condition: IssueSearchCondition,
@@ -79,47 +77,47 @@ trait IssuesService {
} }
.toMap .toMap
} }
/**
* Returns list which contains issue count for each repository. // /**
* If the issue does not exist, its repository is not included in the result. // * Returns list which contains issue count for each repository.
* // * If the issue does not exist, its repository is not included in the result.
* @param condition the search condition // *
* @param filterUser the filter user name (key is "all", "assigned" or "created_by", value is the user name) // * @param condition the search condition
* @param onlyPullRequest if true then returns only pull request, false then returns both of issue and pull request. // * @param onlyPullRequest if true then returns only pull request, false then returns both of issue and pull request.
* @param repos Tuple of the repository owner and the repository name // * @param repos Tuple of the repository owner and the repository name
* @return list which contains issue count for each repository // * @return list which contains issue count for each repository
*/ // */
def countIssueGroupByRepository( // def countIssueGroupByRepository(
condition: IssueSearchCondition, filterUser: Map[String, String], onlyPullRequest: Boolean, // condition: IssueSearchCondition, filterUser: Map[String, String], onlyPullRequest: Boolean,
repos: (String, String)*)(implicit s: Session): List[(String, String, Int)] = { // repos: (String, String)*)(implicit s: Session): List[(String, String, Int)] = {
searchIssueQuery(repos, condition.copy(repo = None), filterUser, onlyPullRequest) // searchIssueQuery(repos, condition.copy(repo = None), filterUser, onlyPullRequest)
.groupBy { t => // .groupBy { t =>
t.userName -> t.repositoryName // t.userName -> t.repositoryName
} // }
.map { case (repo, t) => // .map { case (repo, t) =>
(repo._1, repo._2, t.length) // (repo._1, repo._2, t.length)
} // }
.sortBy(_._3 desc) // .sortBy(_._3 desc)
.list // .list
} // }
/** /**
* Returns the search result against issues. * Returns the search result against issues.
* *
* @param condition the search condition * @param condition the search condition
* @param filterUser the filter user name (key is "all", "assigned", "created_by" or "not_created_by", value is the user name) * @param filterUser the filter user name (key is "all", "assigned", "created_by", "not_created_by" or "mentioned", value is the user name)
* @param onlyPullRequest if true then returns only pull request, false then returns both of issue and pull request. * @param pullRequest if true then returns only pull requests, false then returns only issues.
* @param offset the offset for pagination * @param offset the offset for pagination
* @param limit the limit for pagination * @param limit the limit for pagination
* @param repos Tuple of the repository owner and the repository name * @param repos Tuple of the repository owner and the repository name
* @return the search result (list of tuples which contain issue, labels and comment count) * @return the search result (list of tuples which contain issue, labels and comment count)
*/ */
def searchIssue(condition: IssueSearchCondition, filterUser: Map[String, String], onlyPullRequest: Boolean, def searchIssue(condition: IssueSearchCondition, filterUser: Map[String, String], pullRequest: Boolean,
offset: Int, limit: Int, repos: (String, String)*) offset: Int, limit: Int, repos: (String, String)*)
(implicit s: Session): List[(Issue, List[Label], Int)] = { (implicit s: Session): List[IssueInfo] = {
// get issues and comment count and labels // get issues and comment count and labels
searchIssueQuery(repos, condition, filterUser, onlyPullRequest) searchIssueQuery(repos, condition, filterUser, pullRequest)
.innerJoin(IssueOutline).on { (t1, t2) => t1.byIssue(t2.userName, t2.repositoryName, t2.issueId) } .innerJoin(IssueOutline).on { (t1, t2) => t1.byIssue(t2.userName, t2.repositoryName, t2.issueId) }
.sortBy { case (t1, t2) => .sortBy { case (t1, t2) =>
(condition.sort match { (condition.sort match {
@@ -136,21 +134,23 @@ trait IssuesService {
.drop(offset).take(limit) .drop(offset).take(limit)
.leftJoin (IssueLabels) .on { case ((t1, t2), t3) => t1.byIssue(t3.userName, t3.repositoryName, t3.issueId) } .leftJoin (IssueLabels) .on { case ((t1, t2), t3) => t1.byIssue(t3.userName, t3.repositoryName, t3.issueId) }
.leftJoin (Labels) .on { case (((t1, t2), t3), t4) => t3.byLabel(t4.userName, t4.repositoryName, t4.labelId) } .leftJoin (Labels) .on { case (((t1, t2), t3), t4) => t3.byLabel(t4.userName, t4.repositoryName, t4.labelId) }
.map { case (((t1, t2), t3), t4) => .leftJoin (Milestones) .on { case ((((t1, t2), t3), t4), t5) => t1.byMilestone(t5.userName, t5.repositoryName, t5.milestoneId) }
(t1, t2.commentCount, t4.labelId.?, t4.labelName.?, t4.color.?) .map { case ((((t1, t2), t3), t4), t5) =>
(t1, t2.commentCount, t4.labelId.?, t4.labelName.?, t4.color.?, t5.title.?)
} }
.list .list
.splitWith { (c1, c2) => .splitWith { (c1, c2) =>
c1._1.userName == c2._1.userName && c1._1.userName == c2._1.userName &&
c1._1.repositoryName == c2._1.repositoryName && c1._1.repositoryName == c2._1.repositoryName &&
c1._1.issueId == c2._1.issueId c1._1.issueId == c2._1.issueId
} }
.map { issues => issues.head match { .map { issues => issues.head match {
case (issue, commentCount, _,_,_) => case (issue, commentCount, _, _, _, milestone) =>
(issue, IssueInfo(issue,
issues.flatMap { t => t._3.map ( issues.flatMap { t => t._3.map (
Label(issue.userName, issue.repositoryName, _, t._4.get, t._5.get) Label(issue.userName, issue.repositoryName, _, t._4.get, t._5.get)
)} toList, )} toList,
milestone,
commentCount) commentCount)
}} toList }} toList
} }
@@ -159,7 +159,7 @@ trait IssuesService {
* Assembles query for conditional issue searching. * Assembles query for conditional issue searching.
*/ */
private def searchIssueQuery(repos: Seq[(String, String)], condition: IssueSearchCondition, private def searchIssueQuery(repos: Seq[(String, String)], condition: IssueSearchCondition,
filterUser: Map[String, String], onlyPullRequest: Boolean)(implicit s: Session) = filterUser: Map[String, String], pullRequest: Boolean)(implicit s: Session) =
Issues filter { t1 => Issues filter { t1 =>
condition.repo condition.repo
.map { _.split('/') match { case array => Seq(array(0) -> array(1)) } } .map { _.split('/') match { case array => Seq(array(0) -> array(1)) } }
@@ -172,7 +172,10 @@ trait IssuesService {
(t1.assignedUserName === filterUser("assigned").bind, filterUser.get("assigned").isDefined) && (t1.assignedUserName === filterUser("assigned").bind, filterUser.get("assigned").isDefined) &&
(t1.openedUserName === filterUser("created_by").bind, filterUser.get("created_by").isDefined) && (t1.openedUserName === filterUser("created_by").bind, filterUser.get("created_by").isDefined) &&
(t1.openedUserName =!= filterUser("not_created_by").bind, filterUser.get("not_created_by").isDefined) && (t1.openedUserName =!= filterUser("not_created_by").bind, filterUser.get("not_created_by").isDefined) &&
(t1.pullRequest === true.bind, onlyPullRequest) && (t1.assignedUserName === condition.assigned.get.bind, condition.assigned.isDefined) &&
(t1.openedUserName === condition.author.get.bind, condition.author.isDefined) &&
(t1.pullRequest === pullRequest.bind) &&
// Label filter
(IssueLabels filter { t2 => (IssueLabels filter { t2 =>
(t2.byIssue(t1.userName, t1.repositoryName, t1.issueId)) && (t2.byIssue(t1.userName, t1.repositoryName, t1.issueId)) &&
(t2.labelId in (t2.labelId in
@@ -180,7 +183,19 @@ trait IssuesService {
(t3.byRepository(t1.userName, t1.repositoryName)) && (t3.byRepository(t1.userName, t1.repositoryName)) &&
(t3.labelName inSetBind condition.labels) (t3.labelName inSetBind condition.labels)
} map(_.labelId))) } map(_.labelId)))
} exists, condition.labels.nonEmpty) } exists, condition.labels.nonEmpty) &&
// Visibility filter
(Repositories filter { t2 =>
(t2.byRepository(t1.userName, t1.repositoryName)) &&
(t2.isPrivate === (condition.visibility == Some("private")).bind)
} exists, condition.visibility.nonEmpty) &&
// Organization (group) filter
(t1.userName inSetBind condition.groups, condition.groups.nonEmpty) &&
// Mentioned filter
((t1.openedUserName === filterUser("mentioned").bind) || t1.assignedUserName === filterUser("mentioned").bind ||
(IssueComments filter { t2 =>
(t2.byIssue(t1.userName, t1.repositoryName, t1.issueId)) && (t2.commentedUserName === filterUser("mentioned").bind)
} exists), filterUser.get("mentioned").isDefined)
} }
def createIssue(owner: String, repository: String, loginUser: String, title: String, content: Option[String], def createIssue(owner: String, repository: String, loginUser: String, title: String, content: Option[String],
@@ -337,10 +352,21 @@ object IssuesService {
case class IssueSearchCondition( case class IssueSearchCondition(
labels: Set[String] = Set.empty, labels: Set[String] = Set.empty,
milestoneId: Option[Option[Int]] = None, milestoneId: Option[Option[Int]] = None,
author: Option[String] = None,
assigned: Option[String] = None,
repo: Option[String] = None, repo: Option[String] = None,
state: String = "open", state: String = "open",
sort: String = "created", sort: String = "created",
direction: String = "desc"){ direction: String = "desc",
visibility: Option[String] = None,
groups: Set[String] = Set.empty){
def isEmpty: Boolean = {
labels.isEmpty && milestoneId.isEmpty && author.isEmpty && assigned.isEmpty &&
state == "open" && sort == "created" && direction == "desc" && visibility.isEmpty
}
def nonEmpty: Boolean = !isEmpty
def toURL: String = def toURL: String =
"?" + List( "?" + List(
@@ -349,10 +375,15 @@ object IssuesService {
case Some(x) => x.toString case Some(x) => x.toString
case None => "none" case None => "none"
})}, })},
author .map(x => "author=" + urlEncode(x)),
assigned.map(x => "assigned=" + urlEncode(x)),
repo.map("for=" + urlEncode(_)), repo.map("for=" + urlEncode(_)),
Some("state=" + urlEncode(state)), Some("state=" + urlEncode(state)),
Some("sort=" + urlEncode(sort)), Some("sort=" + urlEncode(sort)),
Some("direction=" + urlEncode(direction))).flatten.mkString("&") Some("direction=" + urlEncode(direction)),
visibility.map(x => "visibility=" + urlEncode(x)),
if(groups.isEmpty) None else Some("groups=" + urlEncode(groups.mkString(",")))
).flatten.mkString("&")
} }
@@ -366,14 +397,19 @@ object IssuesService {
def apply(request: HttpServletRequest): IssueSearchCondition = def apply(request: HttpServletRequest): IssueSearchCondition =
IssueSearchCondition( IssueSearchCondition(
param(request, "labels").map(_.split(",").toSet).getOrElse(Set.empty), param(request, "labels").map(_.split(",").toSet).getOrElse(Set.empty),
param(request, "milestone").map{ param(request, "milestone").map {
case "none" => None case "none" => None
case x => x.toIntOpt case x => x.toIntOpt
}, },
param(request, "author"),
param(request, "assigned"),
param(request, "for"), param(request, "for"),
param(request, "state", Seq("open", "closed")).getOrElse("open"), param(request, "state", Seq("open", "closed")).getOrElse("open"),
param(request, "sort", Seq("created", "comments", "updated")).getOrElse("created"), param(request, "sort", Seq("created", "comments", "updated")).getOrElse("created"),
param(request, "direction", Seq("asc", "desc")).getOrElse("desc")) param(request, "direction", Seq("asc", "desc")).getOrElse("desc"),
param(request, "visibility"),
param(request, "groups").map(_.split(",").toSet).getOrElse(Set.empty)
)
def page(request: HttpServletRequest) = try { def page(request: HttpServletRequest) = try {
val i = param(request, "page").getOrElse("1").toInt val i = param(request, "page").getOrElse("1").toInt
@@ -383,4 +419,6 @@ object IssuesService {
} }
} }
case class IssueInfo(issue: Issue, labels: List[Label], milestone: Option[String], commentCount: Int)
} }

View File

@@ -12,8 +12,8 @@ trait LabelsService {
def getLabel(owner: String, repository: String, labelId: Int)(implicit s: Session): Option[Label] = def getLabel(owner: String, repository: String, labelId: Int)(implicit s: Session): Option[Label] =
Labels.filter(_.byPrimaryKey(owner, repository, labelId)).firstOption Labels.filter(_.byPrimaryKey(owner, repository, labelId)).firstOption
def createLabel(owner: String, repository: String, labelName: String, color: String)(implicit s: Session): Unit = def createLabel(owner: String, repository: String, labelName: String, color: String)(implicit s: Session): Int =
Labels insert Label( Labels returning Labels.map(_.labelId) += Label(
userName = owner, userName = owner,
repositoryName = repository, repositoryName = repository,
labelName = labelName, labelName = labelName,

View File

@@ -36,6 +36,24 @@ trait PullRequestService { self: IssuesService =>
.list .list
.map { x => PullRequestCount(x._1, x._2) } .map { x => PullRequestCount(x._1, x._2) }
// def getAllPullRequestCountGroupByUser(closed: Boolean, userName: String)(implicit s: Session): List[PullRequestCount] =
// PullRequests
// .innerJoin(Issues).on { (t1, t2) => t1.byPrimaryKey(t2.userName, t2.repositoryName, t2.issueId) }
// .innerJoin(Repositories).on { case ((t1, t2), t3) => t2.byRepository(t3.userName, t3.repositoryName) }
// .filter { case ((t1, t2), t3) =>
// (t2.closed === closed.bind) &&
// (
// (t3.isPrivate === false.bind) ||
// (t3.userName === userName.bind) ||
// (Collaborators.filter { t4 => t4.byRepository(t3.userName, t3.repositoryName) && (t4.collaboratorName === userName.bind)} exists)
// )
// }
// .groupBy { case ((t1, t2), t3) => t2.openedUserName }
// .map { case (userName, t) => userName -> t.length }
// .sortBy(_._2 desc)
// .list
// .map { x => PullRequestCount(x._1, x._2) }
def createPullRequest(originUserName: String, originRepositoryName: String, issueId: Int, def createPullRequest(originUserName: String, originRepositoryName: String, issueId: Int,
originBranch: String, requestUserName: String, requestRepositoryName: String, requestBranch: String, originBranch: String, requestUserName: String, requestRepositoryName: String, requestBranch: String,
commitIdFrom: String, commitIdTo: String)(implicit s: Session): Unit = commitIdFrom: String, commitIdTo: String)(implicit s: Session): Unit =

View File

@@ -166,8 +166,19 @@ trait RepositoryService { self: AccountService =>
} }
} }
def getAllRepositories()(implicit s: Session): List[(String, String)] = { /**
Repositories.sortBy(_.lastActivityDate desc).map{ t => * Returns the repositories without private repository that user does not have access right.
* Include public repository, private own repository and private but collaborator repository.
*
* @param userName the user name of collaborator
* @return the repository infomation list
*/
def getAllRepositories(userName: String)(implicit s: Session): List[(String, String)] = {
Repositories.filter { t1 =>
(t1.isPrivate === false.bind) ||
(t1.userName === userName.bind) ||
(Collaborators.filter { t2 => t2.byRepository(t1.userName, t1.repositoryName) && (t2.collaboratorName === userName.bind)} exists)
}.sortBy(_.lastActivityDate desc).map{ t =>
(t.userName, t.repositoryName) (t.userName, t.repositoryName)
}.list }.list
} }

View File

@@ -191,4 +191,7 @@ object SystemSettingsService {
else value else value
} }
// TODO temporary flag
val enablePluginSystem = Option(System.getProperty("enable.plugin")).getOrElse("false").toBoolean
} }

View File

@@ -11,6 +11,7 @@ import util.ControlUtil._
import org.eclipse.jgit.api.Git import org.eclipse.jgit.api.Git
import util.Directory import util.Directory
import plugin.PluginUpdateJob import plugin.PluginUpdateJob
import service.SystemSettingsService
object AutoUpdate { object AutoUpdate {
@@ -52,6 +53,8 @@ object AutoUpdate {
* The history of versions. A head of this sequence is the current BitBucket version. * The history of versions. A head of this sequence is the current BitBucket version.
*/ */
val versions = Seq( val versions = Seq(
new Version(2, 5),
new Version(2, 4),
new Version(2, 3) { new Version(2, 3) {
override def update(conn: Connection): Unit = { override def update(conn: Connection): Unit = {
super.update(conn) super.update(conn)
@@ -168,18 +171,15 @@ object AutoUpdate {
*/ */
class AutoUpdateListener extends ServletContextListener { class AutoUpdateListener extends ServletContextListener {
import org.quartz.impl.StdSchedulerFactory import org.quartz.impl.StdSchedulerFactory
import org.quartz.JobBuilder._
import org.quartz.TriggerBuilder._
import org.quartz.SimpleScheduleBuilder._
import AutoUpdate._ import AutoUpdate._
private val logger = LoggerFactory.getLogger(classOf[AutoUpdateListener]) private val logger = LoggerFactory.getLogger(classOf[AutoUpdateListener])
private val scheduler = StdSchedulerFactory.getDefaultScheduler private val scheduler = StdSchedulerFactory.getDefaultScheduler
override def contextInitialized(event: ServletContextEvent): Unit = { override def contextInitialized(event: ServletContextEvent): Unit = {
val datadir = event.getServletContext.getInitParameter("gitbucket.home") val dataDir = event.getServletContext.getInitParameter("gitbucket.home")
if(datadir != null){ if(dataDir != null){
System.setProperty("gitbucket.home", datadir) System.setProperty("gitbucket.home", dataDir)
} }
org.h2.Driver.load() org.h2.Driver.load()
@@ -210,21 +210,23 @@ class AutoUpdateListener extends ServletContextListener {
logger.debug("End schema update") logger.debug("End schema update")
} }
getDatabase(context).withSession { implicit session => if(SystemSettingsService.enablePluginSystem){
logger.debug("Starting plugin system...") getDatabase(context).withSession { implicit session =>
try { logger.debug("Starting plugin system...")
plugin.PluginSystem.init() try {
plugin.PluginSystem.init()
scheduler.start() scheduler.start()
PluginUpdateJob.schedule(scheduler) PluginUpdateJob.schedule(scheduler)
logger.debug("PluginUpdateJob is started.") logger.debug("PluginUpdateJob is started.")
logger.debug("Plugin system is initialized.") logger.debug("Plugin system is initialized.")
} catch { } catch {
case ex: Throwable => { case ex: Throwable => {
logger.error("Failed to initialize plugin system", ex) logger.error("Failed to initialize plugin system", ex)
ex.printStackTrace() ex.printStackTrace()
throw ex throw ex
}
} }
} }
} }

View File

@@ -7,7 +7,7 @@ import play.twirl.api.Html
import service.{AccountService, RepositoryService, SystemSettingsService} import service.{AccountService, RepositoryService, SystemSettingsService}
import model.{Account, Session} import model.{Account, Session}
import util.{JGitUtil, Keys} import util.{JGitUtil, Keys}
import plugin.{Fragment, PluginConnectionHolder, Redirect} import plugin.{RawData, Fragment, PluginConnectionHolder, Redirect}
import service.RepositoryService.RepositoryInfo import service.RepositoryService.RepositoryInfo
import plugin.Security._ import plugin.Security._
@@ -21,7 +21,7 @@ class PluginActionInvokeFilter extends Filter with SystemSettingsService with Re
(req, res) match { (req, res) match {
case (request: HttpServletRequest, response: HttpServletResponse) => { case (request: HttpServletRequest, response: HttpServletResponse) => {
Database(req.getServletContext) withTransaction { implicit session => Database(req.getServletContext) withTransaction { implicit session =>
val path = req.asInstanceOf[HttpServletRequest].getRequestURI val path = request.getRequestURI.substring(request.getServletContext.getContextPath.length)
if(!processGlobalAction(path, request, response) && !processRepositoryAction(path, request, response)){ if(!processGlobalAction(path, request, response) && !processRepositoryAction(path, request, response)){
chain.doFilter(req, res) chain.doFilter(req, res)
} }
@@ -46,13 +46,7 @@ class PluginActionInvokeFilter extends Filter with SystemSettingsService with Re
} finally { } finally {
PluginConnectionHolder.threadLocal.remove() PluginConnectionHolder.threadLocal.remove()
} }
result match { processActionResult(result, request, response, context)
case x: String => renderGlobalHtml(request, response, context, x)
case x: Html => renderGlobalHtml(request, response, context, x.toString)
case x: Fragment => renderFragmentHtml(request, response, context, x.html.toString)
case x: Redirect => response.sendRedirect(x.path)
case x: AnyRef => renderJson(request, response, x)
}
} else { } else {
// TODO NotFound or Error? // TODO NotFound or Error?
} }
@@ -81,13 +75,7 @@ class PluginActionInvokeFilter extends Filter with SystemSettingsService with Re
} finally { } finally {
PluginConnectionHolder.threadLocal.remove() PluginConnectionHolder.threadLocal.remove()
} }
result match { processActionResult(result, request, response, context)
case x: String => renderRepositoryHtml(request, response, context, repository, x)
case x: Html => renderGlobalHtml(request, response, context, x.toString)
case x: Fragment => renderFragmentHtml(request, response, context, x.html.toString)
case x: Redirect => response.sendRedirect(x.path)
case x: AnyRef => renderJson(request, response, x)
}
} else { } else {
// TODO NotFound or Error? // TODO NotFound or Error?
} }
@@ -97,6 +85,24 @@ class PluginActionInvokeFilter extends Filter with SystemSettingsService with Re
} else false } else false
} }
private def processActionResult(result: Any, request: HttpServletRequest, response: HttpServletResponse,
context: app.Context): Unit = {
result match {
case null|None => renderError(request, response, context, 404)
case x: String => renderGlobalHtml(request, response, context, x)
case Some(x: String) => renderGlobalHtml(request, response, context, x)
case x: Html => renderGlobalHtml(request, response, context, x.toString)
case Some(x: Html) => renderGlobalHtml(request, response, context, x.toString)
case x: Fragment => renderFragmentHtml(request, response, context, x.html.toString)
case Some(x: Fragment) => renderFragmentHtml(request, response, context, x.html.toString)
case x: RawData => renderRawData(request, response, context, x)
case Some(x: RawData) => renderRawData(request, response, context, x)
case x: Redirect => response.sendRedirect(x.path)
case Some(x: Redirect) => response.sendRedirect(x.path)
case x: AnyRef => renderJson(request, response, x)
}
}
/** /**
* Authentication for global action * Authentication for global action
*/ */
@@ -145,6 +151,10 @@ class PluginActionInvokeFilter extends Filter with SystemSettingsService with Re
} }
} }
private def renderError(request: HttpServletRequest, response: HttpServletResponse, context: app.Context, error: Int): Unit = {
response.sendError(error)
}
private def renderGlobalHtml(request: HttpServletRequest, response: HttpServletResponse, context: app.Context, body: String): Unit = { private def renderGlobalHtml(request: HttpServletRequest, response: HttpServletResponse, context: app.Context, body: String): Unit = {
response.setContentType("text/html; charset=UTF-8") response.setContentType("text/html; charset=UTF-8")
val html = _root_.html.main("GitBucket", None)(Html(body))(context) val html = _root_.html.main("GitBucket", None)(Html(body))(context)
@@ -162,6 +172,11 @@ class PluginActionInvokeFilter extends Filter with SystemSettingsService with Re
IOUtils.write(body.getBytes("UTF-8"), response.getOutputStream) IOUtils.write(body.getBytes("UTF-8"), response.getOutputStream)
} }
private def renderRawData(request: HttpServletRequest, response: HttpServletResponse, context: app.Context, rawData: RawData): Unit = {
response.setContentType(rawData.contentType)
IOUtils.write(rawData.content, response.getOutputStream)
}
private def renderJson(request: HttpServletRequest, response: HttpServletResponse, obj: AnyRef): Unit = { private def renderJson(request: HttpServletRequest, response: HttpServletResponse, obj: AnyRef): Unit = {
import org.json4s._ import org.json4s._
import org.json4s.jackson.Serialization import org.json4s.jackson.Serialization

View File

@@ -4,6 +4,7 @@ import org.eclipse.jgit.api.Git
import util.Directory._ import util.Directory._
import util.StringUtil._ import util.StringUtil._
import util.ControlUtil._ import util.ControlUtil._
import scala.annotation.tailrec
import scala.collection.JavaConverters._ import scala.collection.JavaConverters._
import org.eclipse.jgit.lib._ import org.eclipse.jgit.lib._
import org.eclipse.jgit.revwalk._ import org.eclipse.jgit.revwalk._
@@ -13,7 +14,7 @@ import org.eclipse.jgit.treewalk.filter._
import org.eclipse.jgit.diff.DiffEntry.ChangeType import org.eclipse.jgit.diff.DiffEntry.ChangeType
import org.eclipse.jgit.errors.{ConfigInvalidException, MissingObjectException} import org.eclipse.jgit.errors.{ConfigInvalidException, MissingObjectException}
import java.util.Date import java.util.Date
import org.eclipse.jgit.api.errors.NoHeadException import org.eclipse.jgit.api.errors.{JGitInternalException, InvalidRefNameException, RefAlreadyExistsException, NoHeadException}
import service.RepositoryService import service.RepositoryService
import org.eclipse.jgit.dircache.DirCacheEntry import org.eclipse.jgit.dircache.DirCacheEntry
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
@@ -190,38 +191,23 @@ object JGitUtil {
* @return HTML of the file list * @return HTML of the file list
*/ */
def getFileList(git: Git, revision: String, path: String = "."): List[FileInfo] = { def getFileList(git: Git, revision: String, path: String = "."): List[FileInfo] = {
val list = new scala.collection.mutable.ListBuffer[(ObjectId, FileMode, String, String, Option[String])] var list = new scala.collection.mutable.ListBuffer[(ObjectId, FileMode, String, String, Option[String])]
using(new RevWalk(git.getRepository)){ revWalk => using(new RevWalk(git.getRepository)){ revWalk =>
val objectId = git.getRepository.resolve(revision) val objectId = git.getRepository.resolve(revision)
val revCommit = revWalk.parseCommit(objectId) val revCommit = revWalk.parseCommit(objectId)
using(new TreeWalk(git.getRepository)){ treeWalk => val treeWalk = if (path == ".") {
val treeWalk = new TreeWalk(git.getRepository)
treeWalk.addTree(revCommit.getTree) treeWalk.addTree(revCommit.getTree)
if(path != "."){ treeWalk
treeWalk.setRecursive(true) } else {
treeWalk.setFilter(new TreeFilter(){ val treeWalk = TreeWalk.forPath(git.getRepository, path, revCommit.getTree)
treeWalk.enterSubtree()
treeWalk
}
var stopRecursive = false using(treeWalk) { treeWalk =>
def include(walker: TreeWalk): Boolean = {
val targetPath = walker.getPathString
if((path + "/").startsWith(targetPath)){
true
} else if(targetPath.startsWith(path + "/") && targetPath.substring(path.length + 1).indexOf('/') < 0){
stopRecursive = true
treeWalk.setRecursive(false)
true
} else {
false
}
}
def shouldBeRecursive(): Boolean = !stopRecursive
override def clone: TreeFilter = return this
})
}
while (treeWalk.next()) { while (treeWalk.next()) {
// submodule // submodule
val linkUrl = if(treeWalk.getFileMode(0) == FileMode.GITLINK){ val linkUrl = if(treeWalk.getFileMode(0) == FileMode.GITLINK){
@@ -230,6 +216,31 @@ object JGitUtil {
list.append((treeWalk.getObjectId(0), treeWalk.getFileMode(0), treeWalk.getPathString, treeWalk.getNameString, linkUrl)) list.append((treeWalk.getObjectId(0), treeWalk.getFileMode(0), treeWalk.getPathString, treeWalk.getNameString, linkUrl))
} }
list = list.map(tuple =>
if (tuple._2 != FileMode.TREE)
tuple
else
simplifyPath(tuple)
)
@tailrec
def simplifyPath(tuple: (ObjectId, FileMode, String, String, Option[String])): (ObjectId, FileMode, String, String, Option[String]) = {
val list = new scala.collection.mutable.ListBuffer[(ObjectId, FileMode, String, String, Option[String])]
using(new TreeWalk(git.getRepository)) { walk =>
walk.addTree(tuple._1)
while (walk.next() && list.size < 2) {
val linkUrl = if (walk.getFileMode(0) == FileMode.GITLINK) {
getSubmodules(git, revCommit.getTree).find(_.path == walk.getPathString).map(_.url)
} else None
list.append((walk.getObjectId(0), walk.getFileMode(0), tuple._3 + "/" + walk.getPathString, tuple._4 + "/" + walk.getNameString, linkUrl))
}
}
if (list.size != 1 || list.exists(_._2 != FileMode.TREE))
tuple
else
simplifyPath(list(0))
}
} }
} }
@@ -496,6 +507,17 @@ object JGitUtil {
}.find(_._1 != null) }.find(_._1 != null)
} }
def createBranch(git: Git, fromBranch: String, newBranch: String) = {
try {
git.branchCreate().setStartPoint(fromBranch).setName(newBranch).call()
Right("Branch created.")
} catch {
case e: RefAlreadyExistsException => Left("Sorry, that branch already exists.")
// JGitInternalException occurs when new branch name is 'a' and the branch whose name is 'a/*' exists.
case _: InvalidRefNameException | _: JGitInternalException => Left("Sorry, that name is invalid.")
}
}
def createDirCacheEntry(path: String, mode: FileMode, objectId: ObjectId): DirCacheEntry = { def createDirCacheEntry(path: String, mode: FileMode, objectId: ObjectId): DirCacheEntry = {
val entry = new DirCacheEntry(path) val entry = new DirCacheEntry(path)
entry.setFileMode(mode) entry.setFileMode(mode)

View File

@@ -9,6 +9,7 @@ import org.pegdown.ast._
import org.pegdown.LinkRenderer.Rendering import org.pegdown.LinkRenderer.Rendering
import java.text.Normalizer import java.text.Normalizer
import java.util.Locale import java.util.Locale
import java.util.regex.Pattern
import scala.collection.JavaConverters._ import scala.collection.JavaConverters._
import service.{RequestCache, WikiService} import service.{RequestCache, WikiService}
@@ -18,17 +19,23 @@ object Markdown {
* Converts Markdown of Wiki pages to HTML. * Converts Markdown of Wiki pages to HTML.
*/ */
def toHtml(markdown: String, repository: service.RepositoryService.RepositoryInfo, def toHtml(markdown: String, repository: service.RepositoryService.RepositoryInfo,
enableWikiLink: Boolean, enableRefsLink: Boolean)(implicit context: app.Context): String = { enableWikiLink: Boolean, enableRefsLink: Boolean,
enableTaskList: Boolean = false, hasWritePermission: Boolean = false)(implicit context: app.Context): String = {
// escape issue id // escape issue id
val source = if(enableRefsLink){ val s = if(enableRefsLink){
markdown.replaceAll("(?<=(\\W|^))#(\\d+)(?=(\\W|$))", "issue:$2") markdown.replaceAll("(?<=(\\W|^))#(\\d+)(?=(\\W|$))", "issue:$2")
} else markdown } else markdown
// escape task list
val source = if(enableTaskList){
GitBucketHtmlSerializer.escapeTaskList(s)
} else s
val rootNode = new PegDownProcessor( val rootNode = new PegDownProcessor(
Extensions.AUTOLINKS | Extensions.WIKILINKS | Extensions.FENCED_CODE_BLOCKS | Extensions.TABLES | Extensions.HARDWRAPS Extensions.AUTOLINKS | Extensions.WIKILINKS | Extensions.FENCED_CODE_BLOCKS | Extensions.TABLES | Extensions.HARDWRAPS
).parseMarkdown(source.toCharArray) ).parseMarkdown(source.toCharArray)
new GitBucketHtmlSerializer(markdown, repository, enableWikiLink, enableRefsLink).toHtml(rootNode) new GitBucketHtmlSerializer(markdown, repository, enableWikiLink, enableRefsLink, enableTaskList, hasWritePermission).toHtml(rootNode)
} }
} }
@@ -82,15 +89,18 @@ class GitBucketHtmlSerializer(
markdown: String, markdown: String,
repository: service.RepositoryService.RepositoryInfo, repository: service.RepositoryService.RepositoryInfo,
enableWikiLink: Boolean, enableWikiLink: Boolean,
enableRefsLink: Boolean enableRefsLink: Boolean,
enableTaskList: Boolean,
hasWritePermission: Boolean
)(implicit val context: app.Context) extends ToHtmlSerializer( )(implicit val context: app.Context) extends ToHtmlSerializer(
new GitBucketLinkRender(context, repository, enableWikiLink), new GitBucketLinkRender(context, repository, enableWikiLink),
Map[String, VerbatimSerializer](VerbatimSerializer.DEFAULT -> new GitBucketVerbatimSerializer).asJava Map[String, VerbatimSerializer](VerbatimSerializer.DEFAULT -> new GitBucketVerbatimSerializer).asJava
) with LinkConverter with RequestCache { ) with LinkConverter with RequestCache {
override protected def printImageTag(imageNode: SuperNode, url: String): Unit = override protected def printImageTag(imageNode: SuperNode, url: String): Unit = {
printer.print("<a target=\"_blank\" href=\"").print(fixUrl(url)).print("\">") printer.print("<a target=\"_blank\" href=\"").print(fixUrl(url, true)).print("\">")
.print("<img src=\"").print(fixUrl(url)).print("\" alt=\"").printEncoded(printChildrenToString(imageNode)).print("\"/></a>") .print("<img src=\"").print(fixUrl(url, true)).print("\" alt=\"").printEncoded(printChildrenToString(imageNode)).print("\"/></a>")
}
override protected def printLink(rendering: LinkRenderer.Rendering): Unit = { override protected def printLink(rendering: LinkRenderer.Rendering): Unit = {
printer.print('<').print('a') printer.print('<').print('a')
@@ -101,9 +111,21 @@ class GitBucketHtmlSerializer(
printer.print('>').print(rendering.text).print("</a>") printer.print('>').print(rendering.text).print("</a>")
} }
private def fixUrl(url: String): String = { private def fixUrl(url: String, isImage: Boolean = false): String = {
if(!enableWikiLink || url.startsWith("http://") || url.startsWith("https://") || url.startsWith("#")){ if(url.startsWith("http://") || url.startsWith("https://") || url.startsWith("#") || url.startsWith("/")){
url url
} else if(!enableWikiLink){
if(context.currentPath.contains("/blob/")){
url + (if(isImage) "?raw=true" else "")
} else if(context.currentPath.contains("/tree/")){
val paths = context.currentPath.split("/")
val branch = if(paths.length > 3) paths.drop(4).mkString("/") else repository.repository.defaultBranch
repository.httpUrl.replaceFirst("/git/", "/").stripSuffix(".git") + "/blob/" + branch + "/" + url + (if(isImage) "?raw=true" else "")
} else {
val paths = context.currentPath.split("/")
val branch = if(paths.length > 3) paths.last else repository.repository.defaultBranch
repository.httpUrl.replaceFirst("/git/", "/").stripSuffix(".git") + "/blob/" + branch + "/" + url + (if(isImage) "?raw=true" else "")
}
} else { } else {
repository.httpUrl.replaceFirst("/git/", "/").stripSuffix(".git") + "/wiki/_blob/" + url repository.httpUrl.replaceFirst("/git/", "/").stripSuffix(".git") + "/wiki/_blob/" + url
} }
@@ -130,7 +152,10 @@ class GitBucketHtmlSerializer(
override def visit(node: TextNode): Unit = { override def visit(node: TextNode): Unit = {
// convert commit id and username to link. // convert commit id and username to link.
val text = if(enableRefsLink) convertRefsLinks(node.getText, repository, "issue:") else node.getText val t = if(enableRefsLink) convertRefsLinks(node.getText, repository, "issue:") else node.getText
// convert task list to checkbox.
val text = if(enableTaskList) GitBucketHtmlSerializer.convertCheckBox(t, hasWritePermission) else t
if (abbreviations.isEmpty) { if (abbreviations.isEmpty) {
printer.print(text) printer.print(text)
@@ -138,6 +163,28 @@ class GitBucketHtmlSerializer(
printWithAbbreviations(text) printWithAbbreviations(text)
} }
} }
override def visit(node: BulletListNode): Unit = {
if (printChildrenToString(node).contains("""class="task-list-item-checkbox" """)) {
printer.println().print("""<ul class="task-list">""").indent(+2)
visitChildren(node)
printer.indent(-2).println().print("</ul>")
} else {
printIndentedTag(node, "ul")
}
}
override def visit(node: ListItemNode): Unit = {
if (printChildrenToString(node).contains("""class="task-list-item-checkbox" """)) {
printer.println()
printer.print("""<li class="task-list-item">""")
visitChildren(node)
printer.print("</li>")
} else {
printer.println()
printTag(node, "li")
}
}
} }
object GitBucketHtmlSerializer { object GitBucketHtmlSerializer {
@@ -150,4 +197,14 @@ object GitBucketHtmlSerializer {
val noSpecialChars = StringUtil.urlEncode(normalized) val noSpecialChars = StringUtil.urlEncode(normalized)
noSpecialChars.toLowerCase(Locale.ENGLISH) noSpecialChars.toLowerCase(Locale.ENGLISH)
} }
def escapeTaskList(text: String): String = {
Pattern.compile("""^( *)- \[([x| ])\] """, Pattern.MULTILINE).matcher(text).replaceAll("$1* task:$2: ")
}
def convertCheckBox(text: String, hasWritePermission: Boolean): String = {
val disabled = if (hasWritePermission) "" else "disabled"
text.replaceAll("task:x:", """<input type="checkbox" class="task-list-item-checkbox" checked="checked" """ + disabled + "/>")
.replaceAll("task: :", """<input type="checkbox" class="task-list-item-checkbox" """ + disabled + "/>")
}
} }

View File

@@ -1,5 +1,5 @@
package view package view
import java.util.{Date, TimeZone} import java.util.{Locale, Date, TimeZone}
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import play.twirl.api.Html import play.twirl.api.Html
import util.StringUtil import util.StringUtil
@@ -15,6 +15,47 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
*/ */
def datetime(date: Date): String = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(date) def datetime(date: Date): String = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(date)
val timeUnits = List(
(1000L, "second"),
(1000L * 60, "minute"),
(1000L * 60 * 60, "hour"),
(1000L * 60 * 60 * 24, "day"),
(1000L * 60 * 60 * 24 * 30, "month"),
(1000L * 60 * 60 * 24 * 365, "year")
).reverse
/**
* Format java.util.Date to "x {seconds/minutes/hours/days/months/years} ago"
*/
def datetimeAgo(date: Date): String = {
val duration = new Date().getTime - date.getTime
timeUnits.find(tuple => duration / tuple._1 > 0) match {
case Some((unitValue, unitString)) =>
val value = duration / unitValue
s"${value} ${unitString}${if (value > 1) "s" else ""} ago"
case None => "just now"
}
}
/**
*
* Format java.util.Date to "x {seconds/minutes/hours/days} ago"
* If duration over 1 month, format to "d MMM (yyyy)"
*
*/
def datetimeAgoRecentOnly(date: Date): String = {
val duration = new Date().getTime - date.getTime
timeUnits.find(tuple => duration / tuple._1 > 0) match {
case Some((_, "month")) => s"on ${new SimpleDateFormat("d MMM", Locale.ENGLISH).format(date)}"
case Some((_, "year")) => s"on ${new SimpleDateFormat("d MMM yyyy", Locale.ENGLISH).format(date)}"
case Some((unitValue, unitString)) =>
val value = duration / unitValue
s"${value} ${unitString}${if (value > 1) "s" else ""} ago"
case None => "just now"
}
}
/** /**
* Format java.util.Date to "yyyy-MM-dd'T'hh:mm:ss'Z'". * Format java.util.Date to "yyyy-MM-dd'T'hh:mm:ss'Z'".
*/ */
@@ -48,8 +89,8 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
* Converts Markdown of Wiki pages to HTML. * Converts Markdown of Wiki pages to HTML.
*/ */
def markdown(value: String, repository: service.RepositoryService.RepositoryInfo, def markdown(value: String, repository: service.RepositoryService.RepositoryInfo,
enableWikiLink: Boolean, enableRefsLink: Boolean)(implicit context: app.Context): Html = enableWikiLink: Boolean, enableRefsLink: Boolean, enableTaskList: Boolean = false, hasWritePermission: Boolean = false)(implicit context: app.Context): Html =
Html(Markdown.toHtml(value, repository, enableWikiLink, enableRefsLink)) Html(Markdown.toHtml(value, repository, enableWikiLink, enableRefsLink, enableTaskList, hasWritePermission))
def renderMarkup(filePath: List[String], fileContent: String, branch: String, def renderMarkup(filePath: List[String], fileContent: String, branch: String,
repository: service.RepositoryService.RepositoryInfo, repository: service.RepositoryService.RepositoryInfo,

View File

@@ -25,7 +25,7 @@
@if(repository.repository.description.isDefined){ @if(repository.repository.description.isDefined){
<div>@repository.repository.description</div> <div>@repository.repository.description</div>
} }
<div><span class="muted small">Last updated: @datetime(repository.repository.lastActivityDate)</span></div> <div><span class="muted small">Updated @helper.html.datetimeago(repository.repository.lastActivityDate)</span></div>
</div> </div>
</div> </div>
} }

View File

@@ -11,9 +11,11 @@
<li@if(active=="system"){ class="active"}> <li@if(active=="system"){ class="active"}>
<a href="@path/admin/system">System Settings</a> <a href="@path/admin/system">System Settings</a>
</li> </li>
<li@if(active=="plugins"){ class="active"}> @if(service.SystemSettingsService.enablePluginSystem){
<a href="@path/admin/plugins">Plugins</a> <li@if(active=="plugins"){ class="active"}>
</li> <a href="@path/admin/plugins">Plugins</a>
</li>
}
<li> <li>
<a href="@path/console/login.jsp">H2 Console</a> <a href="@path/console/login.jsp">H2 Console</a>
</li> </li>

View File

@@ -16,6 +16,9 @@
<input type="checkbox" name="removed" id="removed" value="true" @if(account.get.isRemoved){checked}/> <input type="checkbox" name="removed" id="removed" value="true" @if(account.get.isRemoved){checked}/>
Disable Disable
</label> </label>
<div>
<span id="error-removed" class="error"></span>
</div>
} }
</fieldset> </fieldset>
@if(account.map(_.password.nonEmpty).getOrElse(true)){ @if(account.map(_.password.nonEmpty).getOrElse(true)){

View File

@@ -0,0 +1,74 @@
@(openCount: Int,
closedCount: Int,
condition: service.IssuesService.IssueSearchCondition,
groups: List[String])(implicit context: app.Context)
@import context._
@import view.helpers._
<span class="small">
<a class="button-link@if(condition.state == "open"){ selected}" href="@condition.copy(state = "open").toURL">
<img src="@assets/common/images/status-open@(if(condition.state == "open"){"-active"}).png"/>
@openCount Open
</a>&nbsp;&nbsp;
<a class="button-link@if(condition.state == "closed"){ selected}" href="@condition.copy(state = "closed").toURL">
<img src="@assets/common/images/status-closed@(if(condition.state == "closed"){"-active"}).png"/>
@closedCount Closed
</a>
</span>
<div class="pull-right" id="table-issues-control">
@helper.html.dropdown("Visibility", flat = true){
<li>
<a href="@(condition.copy(visibility = (if(condition.visibility == Some("private")) None else Some("private"))).toURL)">
@helper.html.checkicon(condition.visibility == Some("private"))
Private repository only
</a>
</li>
<li>
<a href="@(condition.copy(visibility = (if(condition.visibility == Some("public")) None else Some("public"))).toURL)">
@helper.html.checkicon(condition.visibility == Some("public"))
Public repository only
</a>
</li>
}
@helper.html.dropdown("Organization", flat = true){
@groups.map { group =>
<li>
<a href="@((if(condition.groups.contains(group)) condition.copy(groups = condition.groups - group) else condition.copy(groups = condition.groups + group)).toURL)">
@helper.html.checkicon(condition.groups.contains(group))
@avatar(group, 20) @group
</a>
</li>
}
}
@helper.html.dropdown("Sort", flat = true){
<li>
<a href="@condition.copy(sort="created", direction="desc").toURL">
@helper.html.checkicon(condition.sort == "created" && condition.direction == "desc") Newest
</a>
</li>
<li>
<a href="@condition.copy(sort="created", direction="asc" ).toURL">
@helper.html.checkicon(condition.sort == "created" && condition.direction == "asc") Oldest
</a>
</li>
<li>
<a href="@condition.copy(sort="comments", direction="desc").toURL">
@helper.html.checkicon(condition.sort == "comments" && condition.direction == "desc") Most commented
</a>
</li>
<li>
<a href="@condition.copy(sort="comments", direction="asc" ).toURL">
@helper.html.checkicon(condition.sort == "comments" && condition.direction == "asc") Least commented
</a>
</li>
<li>
<a href="@condition.copy(sort="updated", direction="desc").toURL">
@helper.html.checkicon(condition.sort == "updated" && condition.direction == "desc") Recently updated
</a>
</li>
<li>
<a href="@condition.copy(sort="updated", direction="asc" ).toURL">
@helper.html.checkicon(condition.sort == "updated" && condition.direction == "asc") Least recently updated
</a>
</li>
}
</div>

View File

@@ -1,50 +1,15 @@
@(listparts: play.twirl.api.Html, @(issues: List[service.IssuesService.IssueInfo],
allCount: Int, page: Int,
assignedCount: Int, openCount: Int,
createdByCount: Int, closedCount: Int,
repositories: List[(String, String, Int)],
condition: service.IssuesService.IssueSearchCondition, condition: service.IssuesService.IssueSearchCondition,
filter: String)(implicit context: app.Context) filter: String,
groups: List[String])(implicit context: app.Context)
@import context._ @import context._
@import view.helpers._ @import view.helpers._
@html.main("Your Issues"){ @html.main("Issues"){
<div class="container">
@dashboard.html.tab("issues") @dashboard.html.tab("issues")
<div class="row-fluid"> <div class="container">
<div class="span3"> @issueslist(issues, page, openCount, closedCount, condition, filter, groups)
<ul class="nav nav-pills nav-stacked">
<li@if(filter == "all"){ class="active"}>
<a href="@path/dashboard/issues/repos@condition.toURL">
<span class="count-right">@allCount</span>
In your repositories
</a>
</li>
<li@if(filter == "assigned"){ class="active"}>
<a href="@path/dashboard/issues/assigned@condition.toURL">
<span class="count-right">@assignedCount</span>
Assigned to you
</a>
</li>
<li@if(filter == "created_by"){ class="active"}>
<a href="@path/dashboard/issues/created_by@condition.toURL">
<span class="count-right">@createdByCount</span>
Created by you
</a>
</li>
</ul>
<hr/>
<ul class="nav nav-pills nav-stacked small">
@repositories.map { case (owner, name, count) =>
<li@if(condition.repo == Some(owner + "/" + name)){ class="active"}>
<a href="@condition.copy(repo = Some(owner + "/" + name)).toURL">
<span class="count-right">@count</span>
@owner/@name
</a>
</li>
}
</ul>
</div>
@listparts
</div> </div>
</div>
} }

View File

@@ -0,0 +1,65 @@
@(issues: List[service.IssuesService.IssueInfo],
page: Int,
openCount: Int,
closedCount: Int,
condition: service.IssuesService.IssueSearchCondition,
filter: String,
groups: List[String])(implicit context: app.Context)
@import context._
@import view.helpers._
@import service.IssuesService.IssueInfo
<ul class="nav nav-pills-group pull-left fill-width">
<li class="@if(filter == "created_by"){active} first"><a href="@path/dashboard/issues/created_by@condition.toURL">Created</a></li>
<li class="@if(filter == "assigned"){active}"><a href="@path/dashboard/issues/assigned@condition.toURL">Assigned</a></li>
<li class="@if(filter == "mentioned"){active} last"><a href="@path/dashboard/issues/mentioned@condition.toURL">Mentioned</a></li>
</ul>
<table class="table table-bordered table-hover table-issues">
<tr>
<th style="background-color: #eee;">
@dashboard.html.header(openCount, closedCount, condition, groups)
</th>
</tr>
@issues.map { case IssueInfo(issue, labels, milestone, commentCount) =>
<tr>
<td style="padding-top: 15px; padding-bottom: 15px;">
@if(issue.isPullRequest){
<img src="@assets/common/images/pullreq-@(if(issue.closed) "closed" else "open").png"/>
} else {
<img src="@assets/common/images/issue-@(if(issue.closed) "closed" else "open").png"/>
}
<a href="@path/@issue.userName/@issue.repositoryName">@issue.repositoryName</a>&nbsp;&#xFF65;
@if(issue.isPullRequest){
<a href="@path/@issue.userName/@issue.repositoryName/pull/@issue.issueId" class="issue-title">@issue.title</a>
} else {
<a href="@path/@issue.userName/@issue.repositoryName/issues/@issue.issueId" class="issue-title">@issue.title</a>
}
@labels.map { label =>
<span class="label-color small" style="background-color: #@label.color; color: #@label.fontColor; padding-left: 4px; padding-right: 4px">@label.labelName</span>
}
<span class="pull-right muted">
@issue.assignedUserName.map { userName =>
@avatar(userName, 20, tooltip = true)
}
@if(commentCount > 0){
<a href="@path/@issue.userName/@issue.repositoryName/issues/@issue.issueId" class="issue-comment-count">
<img src="@assets/common/images/comment-active.png"> @commentCount
</a>
} else {
<a href="@path/@issue.userName/@issue.repositoryName/issues/@issue.issueId" class="issue-comment-count" style="color: silver;">
<img src="@assets/common/images/comment.png"> @commentCount
</a>
}
</span>
<div class="small muted" style="margin-left: 20px; margin-top: 5px;">
#@issue.issueId opened by @user(issue.openedUserName, styleClass="username") @datetime(issue.registeredDate)
@milestone.map { milestone =>
<span style="margin: 20px;"><a href="@condition.copy(milestoneId = Some(Some(1))).toURL" class="username"><img src="@assets/common/images/milestone.png"> @milestone</a></span>
}
</div>
</td>
</tr>
}
</table>
<div class="pull-right">
@helper.html.paginator(page, (if(condition.state == "open") openCount else closedCount), service.IssuesService.IssueLimit, 10, condition.toURL)
</div>

View File

@@ -1,42 +1,15 @@
@(listparts: play.twirl.api.Html, @(issues: List[service.IssuesService.IssueInfo],
counts: List[service.PullRequestService.PullRequestCount], page: Int,
repositories: List[(String, String, Int)], openCount: Int,
closedCount: Int,
condition: service.IssuesService.IssueSearchCondition, condition: service.IssuesService.IssueSearchCondition,
filter: String)(implicit context: app.Context) filter: String,
groups: List[String])(implicit context: app.Context)
@import context._ @import context._
@import view.helpers._ @import view.helpers._
@html.main("Your Issues"){ @html.main("Pull Requests"){
<div class="container">
@dashboard.html.tab("pulls") @dashboard.html.tab("pulls")
<div class="row-fluid"> <div class="container">
<div class="span3"> @issueslist(issues, page, openCount, closedCount, condition, filter, groups)
<ul class="nav nav-pills nav-stacked">
<li@if(filter == "created_by"){ class="active"}>
<a href="@path/dashboard/pulls/owned@condition.toURL">
<span class="count-right">@counts.find(_.userName == loginAccount.get.userName).map(_.count).getOrElse(0)</span>
Yours
</a>
</li>
<li@if(filter == "not_created_by"){ class="active"}>
<a href="@path/dashboard/pulls/public@condition.toURL">
<span class="count-right">@counts.filter(_.userName != loginAccount.get.userName).map(_.count).sum</span>
Public
</a>
</li>
</ul>
<hr/>
<ul class="nav nav-pills nav-stacked small">
@repositories.map { case (owner, name, count) =>
<li@if(condition.repo == Some(owner + "/" + name)){ class="active"}>
<a href="@path/dashboard/pulls/for/@owner/@name">
<span class="count-right">@count</span>
@owner/@name
</a>
</li>
}
</ul>
</div>
@listparts
</div> </div>
</div>
} }

View File

@@ -0,0 +1,47 @@
@(issues: List[service.IssuesService.IssueInfo],
page: Int,
openCount: Int,
closedCount: Int,
condition: service.IssuesService.IssueSearchCondition,
filter: String,
groups: List[String])(implicit context: app.Context)
@import context._
@import view.helpers._
@import service.IssuesService.IssueInfo
<ul class="nav nav-pills-group pull-left fill-width">
<li class="@if(filter == "created_by"){active} first"><a href="@path/dashboard/pulls/created_by@condition.toURL">Created</a></li>
<li class="@if(filter == "assigned"){active}"><a href="@path/dashboard/pulls/assigned@condition.toURL">Assigned</a></li>
<li class="@if(filter == "mentioned"){active} last"><a href="@path/dashboard/pulls/mentioned@condition.toURL">Mentioned</a></li>
</ul>
<table class="table table-bordered table-hover table-issues">
<tr>
<th style="background-color: #eee;">
@dashboard.html.header(openCount, closedCount, condition, groups)
</th>
</tr>
@issues.map { case IssueInfo(issue, labels, milestone, commentCount) =>
<tr>
<td>
<img src="@assets/common/images/pullreq-@(if(issue.closed) "closed" else "open").png"/>
<a href="@path/@issue.userName/@issue.repositoryName/pull/@issue.issueId" class="issue-title">@issue.title</a>
<span class="pull-right muted">#@issue.issueId</span>
<div style="margin-left: 20px;">
@issue.content.map { content =>
@cut(content, 90)
}.getOrElse {
<span class="muted">No description available</span>
}
</div>
<div class="small muted" style="margin-left: 20px;">
@avatarLink(issue.openedUserName, 20) by @user(issue.openedUserName, styleClass="username") @datetime(issue.registeredDate)&nbsp;
@if(commentCount > 0){
<i class="icon-comment"></i><a href="@path/@issue.userName/@issue.repositoryName/issues/@issue.issueId" class="issue-comment-count">@commentCount @plural(commentCount, "comment")</a>
}
</div>
</td>
</tr>
}
</table>
<div class="pull-right">
@helper.html.paginator(page, (if(condition.state == "open") openCount else closedCount), service.PullRequestService.PullRequestLimit, 10, condition.toURL)
</div>

View File

@@ -1,13 +1,47 @@
@(active: String = "")(implicit context: app.Context) @(active: String = "")(implicit context: app.Context)
@import context._ @import context._
@import view.helpers._ @import view.helpers._
<ul class="nav nav-tabs"> <div class="dashboard-nav">
<li@if(active == ""){ class="active"}><a href="@path/">News Feed</a></li> <div class="container">
@if(loginAccount.isDefined){ <a href="@path/" @if(active == ""){ class="active"}>
<li@if(active == "pulls" ){ class="active"}><a href="@path/dashboard/pulls">Pull Requests</a></li> <img src="@assets/common/images/menu-feed.png">
<li@if(active == "issues"){ class="active"}><a href="@path/dashboard/issues/repos">Issues</a></li> News Feed
} </a>
@if(active == ""){ @if(loginAccount.isDefined){
<li class="pull-right"><a href="@path/activities.atom"><img src="@assets/common/images/feed.png" alt="activities"></a></li> <a href="@path/dashboard/pulls" @if(active == "pulls" ){ class="active"}>
} <img src="@assets/common/images/menu-pulls.png">
</ul> Pull Requests
</a>
<a href="@path/dashboard/issues/repos" @if(active == "issues"){ class="active"}>
<img src="@assets/common/images/menu-issues.png">
Issues
</a>
}
</div>
</div>
<style type="text/css">
div.dashboard-nav {
border-bottom: 1px solid #ddd;
text-align: right;
height: 32px;
margin-bottom: 20px;
}
div.dashboard-nav a {
line-height: 10px;
margin-left: 20px;
padding-bottom: 13px;
padding-left: 4px;
padding-right: 4px;
color: #888;
}
div.dashboard-nav a:hover {
text-decoration: none;
}
div.dashboard-nav a.active {
border-bottom: 2px solid #bb4444;
color: #333;
}
</style>

View File

@@ -62,7 +62,7 @@
@detailActivity(activity: model.Activity, image: String) = { @detailActivity(activity: model.Activity, image: String) = {
<div class="activity-icon-large"><img src="@assets/common/images/@image"/></div> <div class="activity-icon-large"><img src="@assets/common/images/@image"/></div>
<div class="activity-content"> <div class="activity-content">
<div class="muted small">@datetime(activity.activityDate)</div> <div class="muted small">@helper.html.datetimeago(activity.activityDate)</div>
<div class="strong"> <div class="strong">
@avatar(activity.activityUserName, 16) @avatar(activity.activityUserName, 16)
@activityMessage(activity.message) @activityMessage(activity.message)
@@ -76,7 +76,7 @@
@customActivity(activity: model.Activity, image: String)(additionalInfo: Any) = { @customActivity(activity: model.Activity, image: String)(additionalInfo: Any) = {
<div class="activity-icon-large"><img src="@assets/common/images/@image"/></div> <div class="activity-icon-large"><img src="@assets/common/images/@image"/></div>
<div class="activity-content"> <div class="activity-content">
<div class="muted small">@datetime(activity.activityDate)</div> <div class="muted small">@helper.html.datetimeago(activity.activityDate)</div>
<div class="strong"> <div class="strong">
@avatar(activity.activityUserName, 16) @avatar(activity.activityUserName, 16)
@activityMessage(activity.message) @activityMessage(activity.message)
@@ -91,7 +91,7 @@
<div> <div>
@avatar(activity.activityUserName, 16) @avatar(activity.activityUserName, 16)
@activityMessage(activity.message) @activityMessage(activity.message)
<span class="muted small">@datetime(activity.activityDate)</span> <span class="muted small">@helper.html.datetimeago(activity.activityDate)</span>
</div> </div>
</div> </div>
} }

View File

@@ -0,0 +1,62 @@
@(branch: String = "",
repository: service.RepositoryService.RepositoryInfo,
hasWritePermission: Boolean)(body: Html)(implicit context: app.Context)
@import context._
@import view.helpers._
@helper.html.dropdown(
value = if(branch.length == 40) branch.substring(0, 10) else branch,
prefix = if(branch.length == 40) "tree" else if(repository.branchList.contains(branch)) "branch" else "tree",
mini = true
) {
<li><div id="branch-control-title">Switch branches<button id="branch-control-close" class="pull-right">&times</button></div></li>
<li><input id="branch-control-input" type="text" placeholder="Find or create branch ..."/></li>
@body
@if(hasWritePermission) {
<li id="create-branch" style="display: none;">
<a><form action="@url(repository)/branches" method="post" style="margin: 0;">
<span class="new-branch-name">Create branch:&nbsp;<span class="new-branch"></span></span>
<br><span style="padding-left: 17px;">from&nbsp;'@branch'</span>
<input type="hidden" name="new">
<input type="hidden" name="from" value="@branch">
</form></a>
</li>
}
}
<script>
$(function(){
$('#branch-control-input').parent().click(function(e) {
e.stopPropagation();
});
$('#branch-control-close').click(function() {
$('[data-toggle="dropdown"]').parent().removeClass('open');
});
$('#branch-control-input').keyup(function() {
var inputVal = $('#branch-control-input').val();
$.each($('#branch-control-input').parent().parent().find('a'), function(index, elem) {
if (!inputVal || !elem.text.trim() || elem.text.trim().lastIndexOf(inputVal, 0) >= 0) {
$(elem).parent().show();
} else {
$(elem).parent().hide();
}
});
@if(hasWritePermission) {
if (inputVal) {
$('#create-branch').parent().find('li:last-child').show().find('.new-branch').text(inputVal);
} else {
$('#create-branch').parent().find('li:last-child').hide();
}
}
});
@if(hasWritePermission) {
$('#create-branch').click(function() {
$(this).find('input[name="new"]').val($('.dropdown-menu input').val())
$(this).find('form').submit()
});
}
$('.btn-group').click(function() {
$('#branch-control-input').val('');
$('.dropdown-menu li').show();
$('#create-branch').hide();
});
});
</script>

View File

@@ -0,0 +1,10 @@
@(latestUpdatedDate: java.util.Date,
recentOnly: Boolean = true)
@import view.helpers._
<span data-toggle="tooltip" title="@datetime(latestUpdatedDate)">
@if(recentOnly){
@datetimeAgoRecentOnly(latestUpdatedDate)
}else{
@datetimeAgo(latestUpdatedDate)
}
</span>

View File

@@ -9,9 +9,12 @@
@if(showIndex){ @if(showIndex){
<div> <div>
<div class="pull-right" style="margin-bottom: 10px;"> <div class="pull-right" style="margin-bottom: 10px;">
<input id="toggle-file-list" type="button" class="btn" value="Show file list"/> <div class="btn-group" data-toggle="buttons-radio">
<input type="button" id="btn-unified" class="btn btn-default btn-small active" value="Unified">
<input type="button" id="btn-split" class="btn btn-default btn-small" value="Split">
</div>
</div> </div>
Showing @diffs.size changed @plural(diffs.size, "file") Showing <a href="javascript:void(0);" id="toggle-file-list">@diffs.size changed @plural(diffs.size, "file")</a>
</div> </div>
<ul id="commit-file-list" style="display: none;"> <ul id="commit-file-list" style="display: none;">
@diffs.zipWithIndex.map { case (diff, i) => @diffs.zipWithIndex.map { case (diff, i) =>
@@ -38,7 +41,7 @@
<a name="diff-@i"></a> <a name="diff-@i"></a>
<table class="table table-bordered"> <table class="table table-bordered">
<tr> <tr>
<th style="font-weight: normal;" class="box-header"> <th style="font-weight: normal; line-height: 27px;" class="box-header">
@if(diff.changeType == ChangeType.COPY || diff.changeType == ChangeType.RENAME){ @if(diff.changeType == ChangeType.COPY || diff.changeType == ChangeType.RENAME){
@diff.oldPath -> @diff.newPath @diff.oldPath -> @diff.newPath
@if(newCommitId.isDefined){ @if(newCommitId.isDefined){
@@ -66,7 +69,7 @@
</th> </th>
</tr> </tr>
<tr> <tr>
<td> <td style="padding: 0;">
@if(diff.newContent != None || diff.oldContent != None){ @if(diff.newContent != None || diff.oldContent != None){
<div id="diffText-@i"></div> <div id="diffText-@i"></div>
<textarea id="newText-@i" style="display: none;">@diff.newContent.getOrElse("")</textarea> <textarea id="newText-@i" style="display: none;">@diff.newContent.getOrElse("")</textarea>
@@ -94,10 +97,25 @@ $(function(){
}); });
} }
@diffs.zipWithIndex.map { case (diff, i) => // Render diffs as unified mode initially
@if(diff.newContent != None || diff.oldContent != None){ renderDiffs(1);
if($('#oldText-@i').length > 0){
diffUsingJS('oldText-@i', 'newText-@i', 'diffText-@i'); $('#btn-unified').click(function(){
$('.container-wide').removeClass('container-wide').addClass('container');
renderDiffs(1);
});
$('#btn-split').click(function(){
$('.container').removeClass('container').addClass('container-wide');
renderDiffs(0);
});
function renderDiffs(viewType){
@diffs.zipWithIndex.map { case (diff, i) =>
@if(diff.newContent != None || diff.oldContent != None){
if($('#oldText-@i').length > 0){
diffUsingJS('oldText-@i', 'newText-@i', 'diffText-@i', viewType);
}
} }
} }
} }

View File

@@ -1,6 +1,13 @@
@(value: String = "", prefix: String = "", mini: Boolean = true, style: String = "", right: Boolean = false)(body: Html) @(value : String = "",
<div class="btn-group"@if(style.nonEmpty){ style="@style"}> prefix: String = "",
<button class="btn dropdown-toggle@if(mini){ btn-mini} else { btn-small}" data-toggle="dropdown"> mini : Boolean = true,
style : String = "",
right : Boolean = false,
flat : Boolean = false)(body: Html)
<div class="btn-group" @if(style.nonEmpty){style="@style"}>
<button
@if(flat){style="border: none; background-color: #eee;"}
class="dropdown-toggle @if(!flat){btn} else {flat} @if(mini){btn-mini} else {btn-small}" data-toggle="dropdown">
@if(value.isEmpty){ @if(value.isEmpty){
<i class="icon-cog"></i> <i class="icon-cog"></i>
} else { } else {

View File

@@ -0,0 +1,7 @@
@(error: Option[Any])
@if(error.isDefined){
<div class='alert alert-danger'>
<button type="button" class="close" data-dismiss="alert">&times;</button>
@error
</div>
}

View File

@@ -1,7 +1,7 @@
@(info: Option[Any]) @(info: Option[Any])
@if(info.isDefined){ @if(info.isDefined){
<div class="alert alert-info"> <div class="alert alert-info">
<button type="button" class="close" data-dismiss="alert">×</button> <button type="button" class="close" data-dismiss="alert">&times;</button>
@info @info
</div> </div>
} }

View File

@@ -1,4 +1,4 @@
@(repository: service.RepositoryService.RepositoryInfo, content: String, enableWikiLink: Boolean, enableRefsLink: Boolean, @(repository: service.RepositoryService.RepositoryInfo, content: String, enableWikiLink: Boolean, enableRefsLink: Boolean, enableTaskList: Boolean, hasWritePermission: Boolean,
style: String = "", placeholder: String = "Leave a comment", elastic: Boolean = false)(implicit context: app.Context) style: String = "", placeholder: String = "Leave a comment", elastic: Boolean = false)(implicit context: app.Context)
@import context._ @import context._
@import view.helpers._ @import view.helpers._
@@ -38,7 +38,8 @@ $(function(){
$.post('@url(repository)/_preview', { $.post('@url(repository)/_preview', {
content : $('#content').val(), content : $('#content').val(),
enableWikiLink : @enableWikiLink, enableWikiLink : @enableWikiLink,
enableRefsLink : @enableRefsLink enableRefsLink : @enableRefsLink,
enableTaskList : @enableTaskList
}, function(data){ }, function(data){
$('#preview-area').html(data); $('#preview-area').html(data);
prettyPrint(); prettyPrint();

View File

@@ -4,10 +4,13 @@
@import context._ @import context._
@import view.helpers._ @import view.helpers._
@main("GitBucket"){ @main("GitBucket"){
@dashboard.html.tab()
<div class="container"> <div class="container">
@dashboard.html.tab()
<div class="row-fluid"> <div class="row-fluid">
<div class="span8"> <div class="span8">
<div class="pull-right">
<a href="@path/activities.atom"><img src="@assets/common/images/feed.png" alt="activities"></a>
</div>
@helper.html.activities(activities) @helper.html.activities(activities)
</div> </div>
<div class="span4"> <div class="span4">

View File

@@ -5,11 +5,12 @@
@import context._ @import context._
@import view.helpers._ @import view.helpers._
@if(loginAccount.isDefined){ @if(loginAccount.isDefined){
<hr/><br/>
<form method="POST" validate="true"> <form method="POST" validate="true">
<div class="issue-avatar-image">@avatar(loginAccount.get.userName, 48)</div> <div class="issue-avatar-image">@avatar(loginAccount.get.userName, 48)</div>
<div class="box issue-comment-box"> <div class="box issue-comment-box">
<div class="box-content"> <div class="box-content">
@helper.html.preview(repository, "", false, true, "width: 635px; height: 100px; max-height: 150px;", elastic = true) @helper.html.preview(repository, "", false, true, true, hasWritePermission, "width: 635px; height: 100px; max-height: 150px;", elastic = true)
</div> </div>
</div> </div>
<div class="pull-right"> <div class="pull-right">
@@ -27,4 +28,4 @@ $(function(){
$('<input type="hidden">').attr('name', 'action').val($(this).val().toLowerCase()).appendTo('form'); $('<input type="hidden">').attr('name', 'action').val($(this).val().toLowerCase()).appendTo('form');
}); });
}); });
</script> </script>

View File

@@ -5,20 +5,36 @@
pullreq: Option[model.PullRequest] = None)(implicit context: app.Context) pullreq: Option[model.PullRequest] = None)(implicit context: app.Context)
@import context._ @import context._
@import view.helpers._ @import view.helpers._
<div class="issue-avatar-image">@avatar(issue.openedUserName, 48)</div>
<div class="box issue-comment-box">
<div class="box-header-small">
@user(issue.openedUserName, styleClass="username strong") <span class="muted">commented @helper.html.datetimeago(issue.registeredDate)</span>
<span class="pull-right">
@if(hasWritePermission || loginAccount.map(_.userName == issue.openedUserName).getOrElse(false)){
<a href="#" data-issue-id="@issue.issueId"><i class="icon-pencil"></i></a>
}
</span>
</div>
<div class="box-content issue-content" id="issueContent">
@markdown(issue.content getOrElse "No description provided.", repository, false, true, true, hasWritePermission)
</div>
</div>
@comments.map { comment => @comments.map { comment =>
@if(comment.action != "close" && comment.action != "reopen" && comment.action != "delete_branch"){ @if(comment.action != "close" && comment.action != "reopen" && comment.action != "delete_branch"){
<div class="issue-avatar-image">@avatar(comment.commentedUserName, 48)</div> <div class="issue-avatar-image">@avatar(comment.commentedUserName, 48)</div>
<div class="box issue-comment-box" id="comment-@comment.commentId"> <div class="box issue-comment-box" id="comment-@comment.commentId">
<div class="box-header-small"> <div class="box-header-small">
<i class="icon-comment"></i>
@user(comment.commentedUserName, styleClass="username strong") @user(comment.commentedUserName, styleClass="username strong")
@if(comment.action == "comment"){ <span class="muted">
commented @if(comment.action == "comment"){
} else { commented
@if(pullreq.isEmpty){ referenced the issue } else { referenced the pull request } } else {
} @if(pullreq.isEmpty){ referenced the issue } else { referenced the pull request }
}
@helper.html.datetimeago(comment.registeredDate)
</span>
<span class="pull-right"> <span class="pull-right">
@datetime(comment.registeredDate)
@if(comment.action != "commit" && comment.action != "merge" && comment.action != "refer" && @if(comment.action != "commit" && comment.action != "merge" && comment.action != "refer" &&
(hasWritePermission || loginAccount.map(_.userName == comment.commentedUserName).getOrElse(false))){ (hasWritePermission || loginAccount.map(_.userName == comment.commentedUserName).getOrElse(false))){
<a href="#" data-comment-id="@comment.commentId"><i class="icon-pencil"></i></a>&nbsp; <a href="#" data-comment-id="@comment.commentId"><i class="icon-pencil"></i></a>&nbsp;
@@ -30,7 +46,7 @@
@if(comment.action == "commit" && comment.content.split(" ").last.matches("[a-f0-9]{40}")){ @if(comment.action == "commit" && comment.content.split(" ").last.matches("[a-f0-9]{40}")){
@defining(comment.content.substring(comment.content.length - 40)){ id => @defining(comment.content.substring(comment.content.length - 40)){ id =>
<div class="pull-right"><a href="@path/@repository.owner/@repository.name/commit/@id" class="monospace">@id.substring(0, 7)</a></div> <div class="pull-right"><a href="@path/@repository.owner/@repository.name/commit/@id" class="monospace">@id.substring(0, 7)</a></div>
@markdown(comment.content.substring(0, comment.content.length - 41), repository, false, true) @markdown(comment.content.substring(0, comment.content.length - 41), repository, false, true, true, hasWritePermission)
} }
} else { } else {
@if(comment.action == "refer"){ @if(comment.action == "refer"){
@@ -38,7 +54,7 @@
<strong><a href="@path/@repository.owner/@repository.name/issues/@issueId">Issue #@issueId</a>: @rest.mkString(":")</strong> <strong><a href="@path/@repository.owner/@repository.name/issues/@issueId">Issue #@issueId</a>: @rest.mkString(":")</strong>
} }
} else { } else {
@markdown(comment.content, repository, false, true) @markdown(comment.content, repository, false, true, true, hasWritePermission)
} }
} }
</div> </div>
@@ -54,7 +70,7 @@
} else { } else {
<span class="label label-info monospace">@pullreq.map(_.userName):@pullreq.map(_.branch)</span> to <span class="label label-info monospace">@pullreq.map(_.requestUserName):@pullreq.map(_.requestBranch)</span> <span class="label label-info monospace">@pullreq.map(_.userName):@pullreq.map(_.branch)</span> to <span class="label label-info monospace">@pullreq.map(_.requestUserName):@pullreq.map(_.requestBranch)</span>
} }
@datetime(comment.registeredDate) @helper.html.datetimeago(comment.registeredDate)
</div> </div>
} }
@if(comment.action == "close" || comment.action == "close_comment"){ @if(comment.action == "close" || comment.action == "close_comment"){
@@ -62,9 +78,9 @@
<span class="label label-important">Closed</span> <span class="label label-important">Closed</span>
@avatar(comment.commentedUserName, 20) @avatar(comment.commentedUserName, 20)
@if(issue.isPullRequest){ @if(issue.isPullRequest){
@user(comment.commentedUserName, styleClass="username strong") closed the pull request @datetime(comment.registeredDate) @user(comment.commentedUserName, styleClass="username strong") closed the pull request @helper.html.datetimeago(comment.registeredDate)
} else { } else {
@user(comment.commentedUserName, styleClass="username strong") closed the issue @datetime(comment.registeredDate) @user(comment.commentedUserName, styleClass="username strong") closed the issue @helper.html.datetimeago(comment.registeredDate)
} }
</div> </div>
} }
@@ -72,27 +88,36 @@
<div class="small issue-comment-action"> <div class="small issue-comment-action">
<span class="label label-success">Reopened</span> <span class="label label-success">Reopened</span>
@avatar(comment.commentedUserName, 20) @avatar(comment.commentedUserName, 20)
@user(comment.commentedUserName, styleClass="username strong") reopened the issue @datetime(comment.registeredDate) @user(comment.commentedUserName, styleClass="username strong") reopened the issue @helper.html.datetimeago(comment.registeredDate)
</div> </div>
} }
@if(comment.action == "delete_branch"){ @if(comment.action == "delete_branch"){
<div class="small issue-comment-action"> <div class="small issue-comment-action">
<span class="label">Deleted</span> <span class="label">Deleted</span>
@avatar(comment.commentedUserName, 20) @avatar(comment.commentedUserName, 20)
@user(comment.commentedUserName, styleClass="username strong") deleted the <span class="label label-info monospace">@pullreq.map(_.requestBranch)</span> branch @datetime(comment.registeredDate) @user(comment.commentedUserName, styleClass="username strong") deleted the <span class="label label-info monospace">@pullreq.map(_.requestBranch)</span> branch @helper.html.datetimeago(comment.registeredDate)
</div> </div>
} }
} }
<script> <script>
$(function(){ $(function(){
$('i.icon-pencil').click(function(){ $('i.icon-pencil').click(function(){
var id = $(this).closest('a').data('comment-id'); var id = $(this).closest('a').data('comment-id');
$.get('@url(repository)/issue_comments/_data/' + id, var url = '@url(repository)/issue_comments/_data/' + id;
var $content = $('#commentContent-' + id);
if(!id){
id = $(this).closest('a').data('issue-id');
url = '@url(repository)/issues/_data/' + id;
$content = $('#issueContent');
}
$.get(url,
{ {
dataType : 'html' dataType : 'html'
}, },
function(data){ function(data){
$('#commentContent-' + id).empty().html(data); $content.empty().html(data);
}); });
return false; return false;
}); });
@@ -109,5 +134,67 @@ $(function(){
} }
return false; return false;
}); });
var extractMarkdown = function(data){
$('body').append('<div id="tmp"></div>');
$('#tmp').html(data);
var markdown = $('#tmp textarea').val();
$('#tmp').remove();
return markdown;
};
var replaceTaskList = function(issueContentHtml, checkboxes) {
var ss = [],
markdown = extractMarkdown(issueContentHtml),
xs = markdown.split(/- \[[x| ]\]/g);
for (var i=0; i<xs.length; i++) {
ss.push(xs[i]);
if (checkboxes.eq(i).prop('checked')) ss.push('- [x]');
else ss.push('- [ ]');
}
ss.pop();
return ss.join('');
};
$('#issueContent').on('click', ':checkbox', function(ev){
var checkboxes = $('#issueContent :checkbox');
$.get('@url(repository)/issues/_data/@issue.issueId',
{
dataType : 'html'
},
function(responseContent){
$.ajax({
url: '@url(repository)/issues/edit/@issue.issueId',
type: 'POST',
data: {
title : $('#issueTitle').text(),
content : replaceTaskList(responseContent, checkboxes)
}
});
}
);
});
$('div[id^=commentContent-]').on('click', ':checkbox', function(ev){
var $commentContent = $(ev.target).parents('div[id^=commentContent-]'),
commentId = $commentContent.attr('id').replace(/commentContent-/, ''),
checkboxes = $commentContent.find(':checkbox');
$.get('@url(repository)/issue_comments/_data/' + commentId,
{
dataType : 'html'
},
function(responseContent){
$.ajax({
url: '@url(repository)/issue_comments/edit/' + commentId,
type: 'POST',
data: {
issueId : 0,
content : replaceTaskList(responseContent, checkboxes)
}
});
}
);
});
}); });
</script> </script>

View File

@@ -7,7 +7,8 @@
@import view.helpers._ @import view.helpers._
@html.main(s"New Issue - ${repository.owner}/${repository.name}", Some(repository)){ @html.main(s"New Issue - ${repository.owner}/${repository.name}", Some(repository)){
@html.menu("issues", repository){ @html.menu("issues", repository){
@tab("", true, repository) @tab("issues", false, repository)
<br/><br/><hr style="margin-bottom: 10px;">
<form action="@url(repository)/issues/new" method="POST" validate="true"> <form action="@url(repository)/issues/new" method="POST" validate="true">
<div class="row-fluid"> <div class="row-fluid">
<div class="span9"> <div class="span9">
@@ -32,7 +33,7 @@
@if(hasWritePermission){ @if(hasWritePermission){
<input type="hidden" name="milestoneId" value=""/> <input type="hidden" name="milestoneId" value=""/>
@helper.html.dropdown() { @helper.html.dropdown() {
<li><a href="javascript:void(0);" class="milestone" data-id=""><i class="icon-remove-circle"></i> No milestone</a></li> <li><a href="javascript:void(0);" class="milestone" data-id=""><i class="icon-remove-circle"></i> Clear this milestone</a></li>
@milestones.filter(_.closedDate.isEmpty).map { milestone => @milestones.filter(_.closedDate.isEmpty).map { milestone =>
<li> <li>
<a href="javascript:void(0);" class="milestone" data-id="@milestone.milestoneId" data-title="@milestone.title"> <a href="javascript:void(0);" class="milestone" data-id="@milestone.milestoneId" data-title="@milestone.title">
@@ -40,9 +41,9 @@
<div class="small" style="padding-left: 20px;"> <div class="small" style="padding-left: 20px;">
@milestone.dueDate.map { dueDate => @milestone.dueDate.map { dueDate =>
@if(isPast(dueDate)){ @if(isPast(dueDate)){
<img src="@assets/common/images/alert_mono.png"/>Due in @date(dueDate) <img src="@assets/common/images/alert.png"/><span class="milestone-alert">Due by @date(dueDate)</span>
} else { } else {
<span class="muted">Due in @date(dueDate)</span> <span class="muted">Due by @date(dueDate)</span>
} }
}.getOrElse { }.getOrElse {
<span class="muted">No due date</span> <span class="muted">No due date</span>
@@ -56,7 +57,7 @@
</div> </div>
</div> </div>
<hr> <hr>
@helper.html.preview(repository, "", false, true, "width: 565px; height: 200px; max-height: 250px;", elastic = true) @helper.html.preview(repository, "", false, true, true, hasWritePermission, "width: 565px; height: 200px; max-height: 250px;", elastic = true)
</div> </div>
</div> </div>
<div class="pull-right"> <div class="pull-right">
@@ -65,7 +66,7 @@
</div> </div>
<div class="span3"> <div class="span3">
@if(hasWritePermission){ @if(hasWritePermission){
<span class="strong">Add Labels</span> <span class="strong">Labels</span>
<div> <div>
<div id="label-list"> <div id="label-list">
<ul class="label-list nav nav-pills nav-stacked"> <ul class="label-list nav nav-pills nav-stacked">
@@ -112,7 +113,7 @@ $(function(){
if(milestoneId == ''){ if(milestoneId == ''){
$('#label-milestone').text('No milestone'); $('#label-milestone').text('No milestone');
} else { } else {
$('#label-milestone').html($('<span>').append('Milestone: ').append($('<span class="strong">').text(title))); $('#label-milestone').html($('<span class="strong">').text(title));
$('a.milestone[data-id=' + milestoneId + '] i').attr('class', 'icon-ok'); $('a.milestone[data-id=' + milestoneId + '] i').attr('class', 'icon-ok');
} }
$('input[name=milestoneId]').val(milestoneId); $('input[name=milestoneId]').val(milestoneId);

View File

@@ -5,8 +5,8 @@
<textarea style="width: 635px; height: 100px;" id="edit-content-@commentId">@content</textarea> <textarea style="width: 635px; height: 100px;" id="edit-content-@commentId">@content</textarea>
} }
<div> <div>
<input type="button" id="update-comment-@commentId" class="btn btn-small" value="Update Comment"/> <input type="button" id="cancel-comment-@commentId" class="btn btn-small btn-danger" value="Cancel"/>
<input type="button" id="cancel-comment-@commentId" class="btn btn-small btn-danger pull-right" value="Cancel"/> <input type="button" id="update-comment-@commentId" class="btn btn-small pull-right" value="Update comment"/>
</div> </div>
<script> <script>
$(function(){ $(function(){

View File

@@ -1,42 +1,35 @@
@(title: String, content: Option[String], issueId: Int, owner: String, repository: String)(implicit context: app.Context) @(content: Option[String], issueId: Int, owner: String, repository: String)(implicit context: app.Context)
@import context._ @import context._
<span id="error-edit-title" class="error"></span>
<input type="text" style="width: 635px;" id="edit-title" value="@title"/>
@helper.html.attached(owner, repository){ @helper.html.attached(owner, repository){
<textarea style="width: 635px; height: 100px; max-height: 300px;" id="edit-content">@content.getOrElse("")</textarea> <textarea style="width: 635px; height: 100px; max-height: 300px;" id="edit-content">@content.getOrElse("")</textarea>
} }
<div> <div>
<input type="button" id="update" class="btn btn-small" value="Update Issue"/> <input type="button" id="cancel-issue" class="btn btn-small btn-danger" value="Cancel"/>
<input type="button" id="cancel" class="btn btn-small btn-danger pull-right" value="Cancel"/> <input type="button" id="update-issue" class="btn btn-small pull-right" value="Update comment"/>
</div> </div>
<script> <script>
$(function(){ $(function(){
$('#edit-content').elastic();
var callback = function(data){ var callback = function(data){
$('#update, #cancel').removeAttr('disabled'); $('#update, #cancel').removeAttr('disabled');
$('#issueTitle').empty().text(data.title);
$('#issueContent').empty().html(data.content); $('#issueContent').empty().html(data.content);
}; };
$('#update').click(function(){ $('#update-issue').click(function(){
$('#update, #cancel').attr('disabled', 'disabled'); $('#update, #cancel').attr('disabled', 'disabled');
$.ajax({ $.ajax({
url: '@path/@owner/@repository/issues/edit/@issueId', url: '@path/@owner/@repository/issues/edit/@issueId',
type: 'POST', type: 'POST',
data: { data: {
title : $('#edit-title').val(),
content : $('#edit-content').val() content : $('#edit-content').val()
} }
}).done( }).done(
callback callback
).fail(function(req) { ).fail(function(req) {
$('#update, #cancel').removeAttr('disabled'); $('#update, #cancel').removeAttr('disabled');
$('#error-edit-title').text($.parseJSON(req.responseText).title);
}); });
}); });
$('#cancel').click(function(){ $('#cancel-issue').click(function(){
$('#update, #cancel').attr('disabled', 'disabled'); $('#update, #cancel').attr('disabled', 'disabled');
$.get('@path/@owner/@repository/issues/_data/@issueId', callback); $.get('@path/@owner/@repository/issues/_data/@issueId', callback);
return false; return false;

View File

@@ -10,31 +10,86 @@
@import view.helpers._ @import view.helpers._
@html.main(s"${issue.title} - Issue #${issue.issueId} - ${repository.owner}/${repository.name}", Some(repository)){ @html.main(s"${issue.title} - Issue #${issue.issueId} - ${repository.owner}/${repository.name}", Some(repository)){
@html.menu("issues", repository){ @html.menu("issues", repository){
@tab("issues", false, repository)
<ul class="nav nav-tabs pull-left fill-width"> <ul class="nav nav-tabs pull-left fill-width">
<li class="pull-left"><a href="@url(repository)/issues"><i class="icon-arrow-left"></i> Back to issue list</a></li> <li class="pull-left">
<li class="pull-right">Issue #@issue.issueId</li> <h1>
</ul> <span class="show-title">
<div class="row-fluid"> <span id="show-title">@issue.title</span>
<div class="span10"> <span class="muted">#@issue.issueId</span>
@issuedetail(issue, comments, collaborators, milestones, hasWritePermission, repository) </span>
@commentlist(issue, comments, hasWritePermission, repository) <span class="edit-title" style="display: none;">
@commentform(issue, true, hasWritePermission, repository) <span id="error-edit-title" class="error"></span>
</div> <input type="text" class="span9" id="edit-title" value="@issue.title"/>
<div class="span2"> </span>
</h1>
@if(issue.closed) { @if(issue.closed) {
<span class="label label-important issue-status">Closed</span> <span class="label label-important issue-status">Closed</span>
} else { } else {
<span class="label label-success issue-status">Open</span> <span class="label label-success issue-status">Open</span>
} }
<div class="small" style="text-align: center;"> <span class="muted">
@defining(comments.filter( _.action.contains("comment") ).size){ count => @user(issue.openedUserName, styleClass="username strong") opened this issue @helper.html.datetimeago(issue.registeredDate) - @defining(
<span class="strong">@count</span> @plural(count, "comment") comments.filter( _.action.contains("comment") ).size
){ count =>
@count @plural(count, "comment")
} }
</span>
<br/><br/>
</li>
<li class="pull-right">
<div class="show-title">
@if(hasWritePermission || loginAccount.map(_.userName == issue.openedUserName).getOrElse(false)){
<a class="btn btn-small" href="#" id="edit">Edit</a>
}
<a class="btn btn-small btn-success" href="@url(repository)/issues/new">New issue</a>
</div> </div>
<hr/> <div class="edit-title" style="display: none;">
@issues.html.labels(issue, issueLabels, labels, hasWritePermission, repository) <a class="btn" href="#" id="update">Save</a> <a href="#" id="cancel">Cancel</a>
</div>
</li>
</ul>
<div class="row-fluid">
<div class="span10">
@commentlist(issue, comments, hasWritePermission, repository)
@commentform(issue, true, hasWritePermission, repository)
</div>
<div class="span2">
@issueinfo(issue, comments, issueLabels, collaborators, milestones, labels, hasWritePermission, repository)
</div> </div>
</div> </div>
} }
} }
<script>
$(function(){
$('#edit').click(function(){
$('.edit-title').show();
$('.show-title').hide();
return false;
});
$('#update').click(function(){
$(this).attr('disabled', 'disabled');
$.ajax({
url: '@url(repository)/issues/edit_title/@issue.issueId',
type: 'POST',
data: {
title : $('#edit-title').val()
}
}).done(function(data){
$('#show-title').empty().text(data.title);
$('#cancel').click();
$(this).removeAttr('disabled');
}).fail(function(req){
$(this).removeAttr('disabled');
$('#error-edit-title').text($.parseJSON(req.responseText).title);
});
return false;
});
$('#cancel').click(function(){
$('.edit-title').hide();
$('.show-title').show();
return false;
});
});
</script>

View File

@@ -1,145 +0,0 @@
@(issue: model.Issue,
comments: List[model.IssueComment],
collaborators: List[String],
milestones: List[(model.Milestone, Int, Int)],
hasWritePermission: Boolean,
repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
@import context._
@import view.helpers._
<div class="issue-avatar-image">@avatar(issue.openedUserName, 48)</div>
<div class="box issue-box">
<div class="box-content" style="padding: 0px;">
<div class="issue-header">
@if(hasWritePermission || loginAccount.map(_.userName == issue.openedUserName).getOrElse(false)){
<span class="pull-right"><a class="btn btn-small" href="#" id="edit">Edit</a></span>
}
<div class="small muted">
@user(issue.openedUserName, styleClass="username strong") opened this issue @datetime(issue.registeredDate)
</div>
<h4 id="issueTitle">@issue.title</h4>
</div>
<div class="issue-info">
<span id="label-assigned">
@issue.assignedUserName.map { userName =>
@avatar(userName, 20) @user(userName, styleClass="username strong") is assigned
}.getOrElse("No one is assigned")
</span>
@if(hasWritePermission){
@helper.html.dropdown() {
<li><a href="javascript:void(0);" class="assign" data-name=""><i class="icon-remove-circle"></i> Clear assignee</a></li>
@collaborators.map { collaborator =>
<li>
<a href="javascript:void(0);" class="assign" data-name="@collaborator">
@helper.html.checkicon(Some(collaborator) == issue.assignedUserName)@avatar(collaborator, 20) @collaborator
</a>
</li>
}
}
}
<div class="pull-right">
<span id="label-milestone">
@issue.milestoneId.map { milestoneId =>
@milestones.collect { case (milestone, _, _) if(milestone.milestoneId == milestoneId) =>
Milestone: <span class="strong">@milestone.title</span>
}
}.getOrElse("No milestone")
</span>
<div id="milestone-progress-area">
@issue.milestoneId.map { milestoneId =>
@milestones.collect { case (milestone, openCount, closeCount) if(milestone.milestoneId == milestoneId) =>
@issues.milestones.html.progress(openCount + closeCount, closeCount, false)
}
}
</div>
@if(hasWritePermission){
@helper.html.dropdown() {
<li><a href="javascript:void(0);" class="milestone" data-id=""><i class="icon-remove-circle"></i> Clear this milestone</a></li>
@milestones.filter(_._1.closedDate.isEmpty).map { case (milestone, _, _) =>
<li>
<a href="javascript:void(0);" class="milestone" data-id="@milestone.milestoneId" data-title="@milestone.title">
@helper.html.checkicon(Some(milestone.milestoneId) == issue.milestoneId) @milestone.title
<div class="small" style="padding-left: 20px;">
@milestone.dueDate.map { dueDate =>
@if(isPast(dueDate)){
<img src="@assets/common/images/alert_mono.png"/>Due in @date(dueDate)
} else {
<span class="muted">Due in @date(dueDate)</span>
}
}.getOrElse {
<span class="muted">No due date</span>
}
</div>
</a>
</li>
}
}
}
</div>
</div>
<div class="issue-content" id="issueContent">
@markdown(issue.content getOrElse "No description given.", repository, false, true)
</div>
</div>
</div>
<div class="issue-participants">
@defining((issue.openedUserName :: comments.map(_.commentedUserName)).distinct){ participants =>
<span class="strong">@participants.size</span> @plural(participants.size, "participant")
@participants.map { participant => @avatarLink(participant, 20, tooltip = true) }
}
</div>
<script>
$(function(){
$('#edit').click(function(){
$.get('@url(repository)/issues/_data/@issue.issueId',
{
dataType : 'html'
},
function(data){
$('#issueContent').empty().html(data);
});
return false;
});
$('a.assign').click(function(){
var $this = $(this);
var userName = $this.data('name');
$.post('@url(repository)/issues/@issue.issueId/assign',
{
assignedUserName: userName
},
function(){
$('a.assign i.icon-ok').attr('class', 'icon-white');
if(userName == ''){
$('#label-assigned').text('No one is assigned');
} else {
$('#label-assigned').empty()
.append($this.find('img.avatar').clone(false)).append(' ')
.append($('<a class="username strong">').attr('href', '@path/' + userName).text(userName))
.append(' is assigned');
$('a.assign[data-name=' + jqSelectorEscape(userName) + '] i').attr('class', 'icon-ok');
}
});
});
$('a.milestone').click(function(){
var title = $(this).data('title');
var milestoneId = $(this).data('id');
$.post('@url(repository)/issues/@issue.issueId/milestone',
{
milestoneId: milestoneId
},
function(data){
console.log(data);
$('a.milestone i.icon-ok').attr('class', 'icon-white');
if(milestoneId == ''){
$('#label-milestone').text('No milestone');
$('#milestone-progress-area').empty();
} else {
$('#label-milestone').html($('<span>').append('Milestone: ').append($('<span class="strong">').text(title)));
$('#milestone-progress-area').html(data);
$('a.milestone[data-id=' + milestoneId + '] i').attr('class', 'icon-ok');
}
});
});
});
</script>

View File

@@ -0,0 +1,173 @@
@(issue: model.Issue,
comments: List[model.IssueComment],
issueLabels: List[model.Label],
collaborators: List[String],
milestones: List[(model.Milestone, Int, Int)],
labels: List[model.Label],
hasWritePermission: Boolean,
repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
@import view.helpers._
<div style="margin-bottom: 8px;">
<span class="muted small strong">Labels</span>
@if(hasWritePermission){
<div class="pull-right">
@helper.html.dropdown(right = true) {
@labels.map { label =>
<li>
<a href="#" class="toggle-label" data-label-id="@label.labelId">
@helper.html.checkicon(issueLabels.exists(_.labelId == label.labelId))
<span class="label" style="background-color: #@label.color;">&nbsp;</span>
@label.labelName
</a>
</li>
}
}
</div>
}
</div>
<ul class="label-list nav nav-pills nav-stacked">
@labellist(issueLabels)
</ul>
<hr/>
<div style="margin-bottom: 8px;">
<span class="muted small strong">Milestone</span>
@if(hasWritePermission){
<div class="pull-right">
@helper.html.dropdown(right = true) {
<li><a href="javascript:void(0);" class="milestone" data-id=""><i class="icon-remove-circle"></i> Clear this milestone</a></li>
@milestones.filter(_._1.closedDate.isEmpty).map { case (milestone, _, _) =>
<li>
<a href="javascript:void(0);" class="milestone" data-id="@milestone.milestoneId" data-title="@milestone.title">
@helper.html.checkicon(Some(milestone.milestoneId) == issue.milestoneId) @milestone.title
<div class="small" style="padding-left: 20px;">
@milestone.dueDate.map { dueDate =>
@if(isPast(dueDate)){
<img src="@assets/common/images/alert.png"/><span class="milestone-alert">Due by @date(dueDate)</span>
} else {
<span class="muted">Due by @date(dueDate)</span>
}
}.getOrElse {
<span class="muted">No due date</span>
}
</div>
</a>
</li>
}
}
</div>
}
</div>
<div id="milestone-progress-area">
@issue.milestoneId.map { milestoneId =>
@milestones.collect { case (milestone, openCount, closeCount) if(milestone.milestoneId == milestoneId) =>
@issues.milestones.html.progress(openCount + closeCount, closeCount)
}
}
</div>
<span id="label-milestone">
@issue.milestoneId.map { milestoneId =>
@milestones.collect { case (milestone, _, _) if(milestone.milestoneId == milestoneId) =>
<span class="strong small">@milestone.title</span>
}
}.getOrElse {
<span class="muted small">No milestone</span>
}
</span>
<hr/>
<div style="margin-bottom: 8px;">
<span class="muted small strong">Assignee</span>
@if(hasWritePermission){
<div class="pull-right">
@helper.html.dropdown(right = true) {
<li><a href="javascript:void(0);" class="assign" data-name=""><i class="icon-remove-circle"></i> Clear assignee</a></li>
@collaborators.map { collaborator =>
<li>
<a href="javascript:void(0);" class="assign" data-name="@collaborator">
@helper.html.checkicon(Some(collaborator) == issue.assignedUserName)@avatar(collaborator, 20) @collaborator
</a>
</li>
}
}
</div>
}
</div>
<span id="label-assigned">
@issue.assignedUserName.map { userName =>
@avatar(userName, 20) @user(userName, styleClass="username strong small")
}.getOrElse{
<span class="muted small">No one</span>
}
</span>
<hr/>
<div style="margin-bottom: 8px;">
@defining((issue.openedUserName :: comments.map(_.commentedUserName)).distinct){ participants =>
<div class="muted small strong">@participants.size @plural(participants.size, "participant")</div>
@participants.map { participant => @avatarLink(participant, 20, tooltip = true) }
}
</div>
<script>
$(function(){
$('a.toggle-label').click(function(){
var path, icon;
var i = $(this).children('i');
if(i.hasClass('icon-ok')){
path = 'delete';
icon = 'icon-white';
} else {
path = 'new';
icon = 'icon-ok';
}
$.post('@url(repository)/issues/@issue.issueId/label/' + path,
{
labelId : $(this).data('label-id')
},
function(data){
i.removeClass().addClass(icon);
$('ul.label-list').empty().html(data);
});
return false;
});
$('a.milestone').click(function(){
var title = $(this).data('title');
var milestoneId = $(this).data('id');
$.post('@url(repository)/issues/@issue.issueId/milestone',
{
milestoneId: milestoneId
},
function(data){
console.log(data);
$('a.milestone i.icon-ok').attr('class', 'icon-white');
if(milestoneId == ''){
$('#label-milestone').html($('<span class="muted small">').text('No milestone'));
$('#milestone-progress-area').empty();
} else {
$('#label-milestone').html($('<span class="strong small">').text(title));
$('#milestone-progress-area').html(data);
$('a.milestone[data-id=' + milestoneId + '] i').attr('class', 'icon-ok');
}
});
});
$('a.assign').click(function(){
var $this = $(this);
var userName = $this.data('name');
$.post('@url(repository)/issues/@issue.issueId/assign',
{
assignedUserName: userName
},
function(){
$('a.assign i.icon-ok').attr('class', 'icon-white');
if(userName == ''){
$('#label-assigned').html($('<span class="muted small">').text('No one'));
} else {
$('#label-assigned').empty()
.append($this.find('img.avatar-mini').clone(false)).append(' ')
.append($('<a class="username strong small">').attr('href', '@context.path/' + userName).text(userName));
$('a.assign[data-name=' + jqSelectorEscape(userName) + '] i').attr('class', 'icon-ok');
}
});
});
});
</script>

View File

@@ -1,4 +1,7 @@
@(issueLabels: List[model.Label]) @(issueLabels: List[model.Label])
@if(issueLabels.isEmpty){
<li><span class="muted small">None yet</span></li>
}
@issueLabels.map { label => @issueLabels.map { label =>
<li><span class="issue-label" style="background-color: #@label.color; color: #@label.fontColor;">@label.labelName</span></li> <li><span class="issue-label" style="background-color: #@label.color; color: #@label.fontColor;">@label.labelName</span></li>
} }

View File

@@ -1,51 +0,0 @@
@(issue: model.Issue,
issueLabels: List[model.Label],
labels: List[model.Label],
hasWritePermission: Boolean,
repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
@import view.helpers._
<div style="margin-bottom: 8px;">
<span class="strong">Labels</span>
@if(hasWritePermission){
<div class="pull-right">
@helper.html.dropdown(right = true) {
@labels.map { label =>
<li>
<a href="#" class="toggle-label" data-label-id="@label.labelId">
@helper.html.checkicon(issueLabels.exists(_.labelId == label.labelId))
<span class="label" style="background-color: #@label.color;">&nbsp;</span>
@label.labelName
</a>
</li>
}
}
</div>
}
</div>
<ul class="label-list nav nav-pills nav-stacked">
@labellist(issueLabels)
</ul>
<script>
$(function(){
$('a.toggle-label').click(function(){
var path, icon;
var i = $(this).children('i');
if(i.hasClass('icon-ok')){
path = 'delete';
icon = 'icon-white';
} else {
path = 'new';
icon = 'icon-ok';
}
$.post('@url(repository)/issues/@issue.issueId/label/' + path,
{
labelId : $(this).data('label-id')
},
function(data){
i.removeClass().addClass(icon);
$('ul.label-list').empty().html(data);
});
return false;
});
});
</script>

View File

@@ -1,45 +1,61 @@
@(label: Option[model.Label], repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context) @(label: Option[model.Label], repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
@import context._ @import context._
@import view.helpers._ @import view.helpers._
@defining((if(label.isEmpty) ("new", 190, 4) else ("edit", 180, 8))){ case (mode, width, margin) => @defining(label.map(_.labelId).getOrElse("new")){ labelId =>
<div id="@(mode)LabelArea"> <div id="edit-label-area-@labelId">
<form method="POST" id="edit-label-form" validate="true" style="margin-bottom: 8px;" <form style="margin-bottom: 0px;">
action="@url(repository)/issues/label/@{if(mode == "new") "new" else label.get.labelId + "/edit"}"> <input type="text" id="labelName-@labelId" style="width: 300px; margin-bottom: 0px;" value="@label.map(_.labelName)"@if(labelId == "new"){ placeholder="New label name"}/>
<span id="error-@(mode)LabelName" class="error"></span> <div id="label-color-@labelId" class="input-append color bscp" data-color="#@label.map(_.color).getOrElse("888888")" data-color-format="hex" style="width: 100px; margin-bottom: 0px;">
<input type="text" name="@(mode)LabelName" id="@(mode)LabelName" style="width: @(width)px; margin-left: @(margin)px; margin-bottom: 0px;" value="@label.map(_.labelName)"@if(mode == "new"){ placeholder="New label name"}/> <input type="text" class="span3" id="labelColor-@labelId" value="#@label.map(_.color).getOrElse("888888")" readonly style="width: 100px;">
<span id="error-@(mode)Color" class="error"></span>
<div class="input-append color bscp" data-color="#@label.map(_.color).getOrElse("888888")" data-color-format="hex" id="@(mode)Color" style="width: @(width)px; margin-bottom: 0px;">
<input type="text" class="span3" name="@(mode)Color" value="#@label.map(_.color)" readonly style="width: @(width - 12)px; margin-left: @(margin)px;">
<span class="add-on"><i style="background-color: #@label.map(_.color).getOrElse("888888");"></i></span> <span class="add-on"><i style="background-color: #@label.map(_.color).getOrElse("888888");"></i></span>
</div> </div>
<input type="submit" class="btn" style="margin-left: @(margin)px; margin-bottom: 0px;" value="@if(mode == "new"){Create} else {Save}"/> <script>
@if(mode == "edit"){ $('div#label-color-@labelId').colorpicker();
<input type="hidden" name="editLabelId" value="@label.map(_.labelId)"/> </script>
} <span class="pull-right">
<span id="label-error-@labelId" class="error"></span>
<input type="button" id="cancel-@labelId" class="btn label-edit-cancel" value="Cancel">
<input type="button" id="submit-@labelId" class="btn btn-success" style="margin-bottom: 0px;" value="@(if(labelId == "new") "Create label" else "Save changes")"/>
</span>
</form> </form>
<script> </div>
$(function(){ <script>
@if(mode == "new"){ $(function(){
$('#newColor').colorpicker(); $('#submit-@labelId').click(function(e){
$.post('@url(repository)/issues/labels/@{if(labelId == "new") "new" else labelId + "/edit"}', {
'labelName' : $('#labelName-@labelId').val(),
'labelColor': $('#labelColor-@labelId').val()
}, function(data, status){
$('div#edit-label-area-@labelId').remove();
@if(labelId == "new"){
$('#new-label-table').hide();
// Insert row into the top of table
$('#label-row-header').after(data);
} else {
// Replace table row
$('#label-row-@labelId').after(data).remove();
}
}).fail(function(xhr, status, error){
var errors = JSON.parse(xhr.responseText);
if(errors.labelName){
$('span#label-error-@labelId').text(errors.labelName);
} else if(errors.labelColor){
$('span#label-error-@labelId').text(errors.labelColor);
} else {
$('span#label-error-@labelId').text('error');
}
});
return false;
});
$('#cancel-@labelId').click(function(e){
$('div#edit-label-area-@labelId').remove();
@if(labelId == "new"){
$('#new-label-table').hide();
} else { } else {
$('#editColor').colorpicker(); $('#label-@labelId').show();
$('#edit-label-form').submit(function(e){
$.ajax($(this).attr('action'), {
type: 'POST',
data: $(this).serialize()
})
.done(function(data){
$('#label-edit').parent().empty().html(data);
})
.fail(function(data, status){
displayErrors($.parseJSON(data.responseText));
});
return false;
});
} }
}); });
</script> });
</div> </script>
} }

View File

@@ -1,47 +0,0 @@
@(labels: List[model.Label], repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
@import context._
@import view.helpers._
<div id="label-edit">
<ul class="label-list nav nav-pills nav-stacked">
@labels.map { label =>
<li style="border: 1px solid white;">
<a href="javascript:void(0);" class="label-edit-link" data-label-id="@label.labelId">
<span class="count-right"><i class="icon-remove-circle"></i></span>
<span style="background-color: #@label.color;" class="label-color">&nbsp;&nbsp;</span>
@label.labelName
</a>
</li>
}
</ul>
<script>
$(function(){
$('i.icon-remove-circle').click(function(e){
e.stopPropagation();
if(confirm('Are you sure you want to delete this?')){
$.get('@url(repository)/issues/label/' + $(this).parents('a').data('label-id') + '/delete',
function(data){
$('#label-edit').parent().empty().html(data);
}
);
}
});
$('a.label-edit-link').click(function(e){
if($('input[name=editLabelId]').val() != $(this).data('label-id')){
$('#editLabelArea').remove();
var element = this;
$.get('@url(repository)/issues/label/' + $(this).data('label-id') + '/edit',
function(data){
$(element).parent().append(data);
$('div#label-edit li').css('border', '1px solid white');
$(element).parent().css('border', '1px solid #eee');
}
);
} else {
$('#editLabelArea').remove();
$('div#label-edit li').css('border', '1px solid white');
}
});
});
</script>
</div>

View File

@@ -0,0 +1,36 @@
@(label: model.Label,
counts: Map[String, Int],
repository: service.RepositoryService.RepositoryInfo,
hasWritePermission: Boolean)(implicit context: app.Context)
@import context._
@import view.helpers._
<tr id="label-row-@label.labelId">
<td style="padding-top: 15px; padding-bottom: 15px;">
<div class="milestone row-fluid" id="label-@label.labelId">
<div class="span8">
<div style="margin-top: 6px">
<a href="@url(repository)/issues?labels=@urlEncode(label.labelName)" id="label-row-content-@label.labelId">
<span style="background-color: #@label.color; color: #@label.fontColor; padding: 8px; font-size: 120%; border-radius: 4px;">
<img src="@assets/common/images/label_@(if(label.fontColor == "ffffff") "white" else "black").png" style="width: 12px;"/>
@label.labelName
</span>
</a>
</div>
</div>
<div class="@if(hasWritePermission){span2} else {span4}">
<div class="pull-right">
<span class="muted">@counts.get(label.labelName).getOrElse(0) open issues</span>
</div>
</div>
@if(hasWritePermission){
<div class="span2">
<div class="pull-right">
<a href="javascript:void(0);" onclick="editLabel(@label.labelId)">Edit</a>
&nbsp;&nbsp;
<a href="javascript:void(0);" onclick="deleteLabel(@label.labelId)">Delete</a>
</div>
</div>
}
</div>
</td>
</tr>

View File

@@ -0,0 +1,67 @@
@(labels: List[model.Label],
counts: Map[String, Int],
repository: service.RepositoryService.RepositoryInfo,
hasWritePermission: Boolean)(implicit context: app.Context)
@import context._
@import view.helpers._
@html.main(s"Labels - ${repository.owner}/${repository.name}"){
@html.menu("issues", repository){
@issues.html.tab("labels", hasWritePermission, repository)
<br>
<table class="table table-bordered table-hover table-issues" id="new-label-table" style="display: none;">
<tr><td></td></tr>
</table>
<table class="table table-bordered table-hover table-issues">
<tr id="label-row-header">
<th style="background-color: #eee;">
<span class="small">@labels.size labels</span>
</th>
</tr>
@labels.map { label =>
@_root_.issues.labels.html.label(label, counts, repository, hasWritePermission)
}
@if(labels.isEmpty){
<tr>
<td style="padding: 20px; background-color: #eee; text-align: center;">
No labels to show.
@if(hasWritePermission){
<a href="@url(repository)/issues/labels/new">Create a new label.</a>
}
</td>
</tr>
}
</table>
}
}
<script>
$(function(){
$('#new-label-button').click(function(e){
if($('#edit-label-area-new').size() != 0){
$('div#edit-label-area-new').remove();
$('#new-label-table').hide();
} else {
$.get('@url(repository)/issues/labels/new',
function(data){
$('#new-label-table').show().find('tr td').append(data);
}
);
}
});
});
function deleteLabel(labelId){
if(confirm('Once you delete this label, there is no going back.\nAre you sure?')){
$.post('@url(repository)/issues/labels/' + labelId + '/delete', function(){
$('tr#label-row-' + labelId).remove();
});
}
}
function editLabel(labelId){
$.get('@url(repository)/issues/labels/' + labelId + '/edit',
function(data){
$('#label-' + labelId).hide().parent().append(data);
}
);
}
</script>

View File

@@ -1,143 +1,25 @@
@(issues: List[(model.Issue, List[model.Label], Int)], @(target: String,
issues: List[service.IssuesService.IssueInfo],
page: Int, page: Int,
collaborators: List[String], collaborators: List[String],
milestones: List[model.Milestone], milestones: List[model.Milestone],
labels: List[model.Label], labels: List[model.Label],
openCount: Int, openCount: Int,
closedCount: Int, closedCount: Int,
allCount: Int,
assignedCount: Option[Int],
createdByCount: Option[Int],
labelCounts: Map[String, Int],
condition: service.IssuesService.IssueSearchCondition, condition: service.IssuesService.IssueSearchCondition,
filter: String,
repository: service.RepositoryService.RepositoryInfo, repository: service.RepositoryService.RepositoryInfo,
hasWritePermission: Boolean)(implicit context: app.Context) hasWritePermission: Boolean)(implicit context: app.Context)
@import context._ @import context._
@import view.helpers._ @import view.helpers._
@html.main(s"Issues - ${repository.owner}/${repository.name}", Some(repository)){ @html.main((if(target == "issues") "Issues" else "Pull requests") + s" - ${repository.owner}/${repository.name}", Some(repository)){
@html.menu("issues", repository){ @html.menu(target, repository){
@tab("issues", false, repository) @tab(target, true, repository)
<div class="row-fluid"> @listparts(target, issues, page, openCount, closedCount, condition, collaborators, milestones, labels, Some(repository), hasWritePermission)
<div class="span3">
<ul class="nav nav-pills nav-stacked">
<li@if(filter == "all"){ class="active"}>
<a href="@url(repository)/issues@condition.toURL">
<span class="count-right">@allCount</span>
Everyone's Issues
</a>
</li>
@if(loginAccount.isDefined){
<li@if(filter == "assigned"){ class="active"}>
<a href="@url(repository)/issues/assigned/@loginAccount.map(_.userName)@condition.toURL">
<span class="count-right">@assignedCount</span>
Assigned to you
</a>
</li>
<li@if(filter == "created_by"){ class="active"}>
<a href="@url(repository)/issues/created_by/@loginAccount.map(_.userName)@condition.toURL">
<span class="count-right">@createdByCount</span>
Created by you
</a>
</li>
}
</ul>
<hr/>
@if(condition.milestoneId.isEmpty){
<span class="muted small">No milestone selected</span>
} else {
@if(condition.milestoneId.get.isEmpty){
<span class="muted small">Issues with no milestone</span>
} else {
<span class="muted small">Milestone:</span> @milestones.find(_.milestoneId == condition.milestoneId.get.get).map(_.title)
}
}
@helper.html.dropdown() {
@if(condition.milestoneId.isDefined){
<li>
<a href="@condition.copy(milestoneId = None).toURL">
<i class="icon-remove-circle"></i> Clear milestone filter
</a>
</li>
}
<li>
<a href="@condition.copy(milestoneId = Some(None)).toURL">
@helper.html.checkicon(condition.milestoneId == Some(None)) Issues with no milestone
</a>
</li>
@milestones.filter(_.closedDate.isEmpty).map { milestone =>
<li>
<a href="@condition.copy(milestoneId = Some(Some(milestone.milestoneId))).toURL">
@helper.html.checkicon(condition.milestoneId == Some(Some(milestone.milestoneId))) @milestone.title
<div class="small" style="padding-left: 20px;">
@milestone.dueDate.map { dueDate =>
@if(isPast(dueDate)){
<img src="@assets/common/images/alert_mono.png"/>Due in @date(dueDate)
} else {
<span class="muted">Due in @date(dueDate)</span>
}
}.getOrElse {
<span class="muted">No due date</span>
}
</div>
</a>
</li>
}
}
@if(condition.milestoneId.isDefined && condition.milestoneId.get.isDefined){
@milestones.find(_.milestoneId == condition.milestoneId.get.get).map { milestone =>
<div style="margin-top: 4px;">
@_root_.issues.milestones.html.progress(openCount + closedCount, closedCount, false)
</div>
<span class="muted small">@openCount open issues</span>
@if(milestone.closedDate.isDefined){
@milestone.closedDate.map { closedDate =>
<span class="small">Closed in @date(closedDate)</span>
}
} else {
@milestone.dueDate.map { dueDate =>
@if(isPast(dueDate)){
<img src="@assets/common/images/alert.png"/><span class="small milestone-alert">Due in @date(dueDate)</span>
} else {
<span class="small">Due in @date(dueDate)</span>
}
}
}
}
}
<hr/>
<span class="strong">Labels</span>
<div>
<div id="label-list">
<ul class="label-list nav nav-pills nav-stacked">
@labels.map { label =>
<li>
<a href="@condition.copy(labels = (if(condition.labels.contains(label.labelName)) condition.labels - label.labelName else condition.labels + label.labelName)).toURL"
@if(condition.labels.contains(label.labelName)){style="background-color: #@label.color; color: #@label.fontColor;"}>
<span class="count-right">@labelCounts.getOrElse(label.labelName, 0)</span>
<span style="background-color: #@label.color;" class="label-color">&nbsp;&nbsp;</span>
@label.labelName
</a>
</li>
}
</ul>
</div>
</div>
@if(hasWritePermission){
<hr/>
<input type="button" class="btn btn-block" id="manageLabel" data-toggle="button" value="Manage Labels"/>
<br/>
<span class="strong">New label</span>
@_root_.issues.labels.html.edit(None, repository)
}
</div>
@***** show issue list *****@
@listparts(issues, page, openCount, closedCount, condition, collaborators, milestones, labels, Some(repository), hasWritePermission)
</div>
@if(hasWritePermission){ @if(hasWritePermission){
<form id="batcheditForm" method="POST"> <form id="batcheditForm" method="POST">
<input type="hidden" name="value"/> <input type="hidden" name="value"/>
<input type="hidden" name="checked"/> <input type="hidden" name="checked"/>
<input type="hidden" name="from" value="@target"/>
</form> </form>
} }
} }
@@ -145,20 +27,34 @@
@if(hasWritePermission){ @if(hasWritePermission){
<script> <script>
$(function(){ $(function(){
$('#manageLabel').click(function(){ $('a.header-link').mouseover(function(e){
if($(this).data('toggle-state')){ var target = e.target;
location.href = '@url(repository)/issues'; if(e.target.tagName != 'A'){
} else { target = e.target.parentElement;
$(this).data('toggle-state', 'on');
$.get('@url(repository)/issues/label/edit', function(data){
$('#label-list').parent().empty().html(data);
});
} }
$(target).children('strong' ).css('color', '#0088cc');
$(target).children('img.header-icon-hover').css('display', 'inline');
$(target).children('img.header-icon' ).css('display', 'none');
});
$('a.header-link').mouseout(function(e){
var target = e.target;
if(e.target.tagName != 'A'){
target = e.target.parentElement;
}
$(target).children('strong' ).css('color', 'black');
$(target).children('img.header-icon-hover').css('display', 'none');
$(target).children('img.header-icon' ).css('display', 'inline');
}); });
$('.table-issues input[type=checkbox]').change(function(){ $('.table-issues input[type=checkbox]').change(function(){
$('.table-issues button').prop('disabled', if($('.table-issues input[type=checkbox]').filter(':checked').length == 0){
!$('.table-issues input[type=checkbox]').filter(':checked').length); $('#table-issues-control').show();
$('#table-issues-batchedit').hide();
} else {
$('#table-issues-control').hide();
$('#table-issues-batchedit').show();
}
}).filter(':first').change(); }).filter(':first').change();
var submitBatchEdit = function(action, value) { var submitBatchEdit = function(action, value) {
@@ -170,8 +66,8 @@ $(function(){
form.submit(); form.submit();
}; };
$('#state').click(function(){ $('a.toggle-state').click(function(){
submitBatchEdit('@url(repository)/issues/batchedit/state', $(this).text().toLowerCase()); submitBatchEdit('@url(repository)/issues/batchedit/state', $(this).data('id'));
}); });
$('a.toggle-label').click(function(){ $('a.toggle-label').click(function(){
submitBatchEdit('@url(repository)/issues/batchedit/label', $(this).data('id')); submitBatchEdit('@url(repository)/issues/batchedit/label', $(this).data('id'));

View File

@@ -1,4 +1,5 @@
@(issues: List[(model.Issue, List[model.Label], Int)], @(target: String,
issues: List[service.IssuesService.IssueInfo],
page: Int, page: Int,
openCount: Int, openCount: Int,
closedCount: Int, closedCount: Int,
@@ -10,175 +11,208 @@
hasWritePermission: Boolean = false)(implicit context: app.Context) hasWritePermission: Boolean = false)(implicit context: app.Context)
@import context._ @import context._
@import view.helpers._ @import view.helpers._
@import service.IssuesService.IssueInfo
<div class="span9"> <br>
@if(condition.labels.nonEmpty || condition.milestoneId.isDefined){ @if(condition.nonEmpty){
<a href="@condition.copy(labels = Set.empty, milestoneId = None).toURL" id="clear-filter"> <div>
<i class="icon-remove-circle"></i> Clear milestone and label filters <a href="@service.IssuesService.IssueSearchCondition().toURL" class="header-link">
<img src="@assets/common/images/clear.png" class="header-icon"/>
<img src="@assets/common/images/clear_hover.png" class="header-icon-hover" style="display: none;"/>
<span class="strong">Clear current search query, filters, and sorts</span>
</a>
</div>
}
<table class="table table-bordered table-hover table-issues">
<tr>
<th style="background-color: #eee;">
<input type="checkbox"/>
<span class="small">
<a class="button-link@if(condition.state == "open"){ selected}" href="@condition.copy(state = "open").toURL">
<img src="@assets/common/images/status-open@(if(condition.state == "open"){"-active"}).png"/>
@openCount Open
</a>&nbsp;&nbsp;
<a class="button-link@if(condition.state == "closed"){ selected}" href="@condition.copy(state = "closed").toURL">
<img src="@assets/common/images/status-closed@(if(condition.state == "closed"){"-active"}).png"/>
@closedCount Closed
</a> </a>
} </span>
@if(condition.repo.isDefined){ <div class="pull-right" id="table-issues-control">
<a href="@condition.copy(repo = None).toURL" id="clear-filter"> @helper.html.dropdown("Author", flat = true) {
<i class="icon-remove-circle"></i> Clear filter on @condition.repo @collaborators.map { collaborator =>
</a> <li>
} <a href="@condition.copy(author = (if(condition.author == Some(collaborator)) None else Some(collaborator))).toURL">
<div class="pull-right"> @helper.html.checkicon(condition.author == Some(collaborator))
@helper.html.paginator(page, (if(condition.state == "open") openCount else closedCount), service.IssuesService.IssueLimit, 7, condition.toURL) @avatar(collaborator, 20) @collaborator
</div> </a>
<div class="btn-group"> </li>
<a class="btn btn-small@if(condition.state == "open"){ active}" href="@condition.copy(state = "open").toURL">@openCount Open</a> }
<a class="btn btn-small@if(condition.state == "closed"){ active}" href="@condition.copy(state = "closed").toURL">@closedCount Closed</a> }
</div> @helper.html.dropdown("Label", flat = true) {
@helper.html.dropdown( @labels.map { label =>
value = (condition.sort, condition.direction) match { <li>
case ("created" , "desc") => "Newest" <a href="@condition.copy(labels = (if(condition.labels.contains(label.labelName)) condition.labels - label.labelName else condition.labels + label.labelName)).toURL">
case ("created" , "asc" ) => "Oldest" @helper.html.checkicon(condition.labels.contains(label.labelName))
case ("comments", "desc") => "Most commented" <span style="background-color: #@label.color;" class="label-color">&nbsp;&nbsp;</span>
case ("comments", "asc" ) => "Least commented" @label.labelName
case ("updated" , "desc") => "Recently updated" </a>
case ("updated" , "asc" ) => "Least recently updated" </li>
}, }
prefix = "Sort", }
mini = false @helper.html.dropdown("Milestone", flat = true) {
){ <li>
<li> <a href="@condition.copy(milestoneId = Some(None)).toURL">
<a href="@condition.copy(sort="created", direction="desc").toURL"> @helper.html.checkicon(condition.milestoneId == Some(None)) Issues with no milestone
@helper.html.checkicon(condition.sort == "created" && condition.direction == "desc") Newest </a>
</a> </li>
</li> @milestones.filter(_.closedDate.isEmpty).map { milestone =>
<li> <li>
<a href="@condition.copy(sort="created", direction="asc" ).toURL"> <a href="@condition.copy(milestoneId = Some(Some(milestone.milestoneId))).toURL">
@helper.html.checkicon(condition.sort == "created" && condition.direction == "asc") Oldest @helper.html.checkicon(condition.milestoneId == Some(Some(milestone.milestoneId))) @milestone.title
</a> </a>
</li> </li>
<li> }
<a href="@condition.copy(sort="comments", direction="desc").toURL"> }
@helper.html.checkicon(condition.sort == "comments" && condition.direction == "desc") Most commented @helper.html.dropdown("Assignee", flat = true) {
</a> @collaborators.map { collaborator =>
</li> <li>
<li> <a href="@condition.copy(assigned = Some(collaborator)).toURL">
<a href="@condition.copy(sort="comments", direction="asc" ).toURL"> @helper.html.checkicon(condition.assigned == Some(collaborator))
@helper.html.checkicon(condition.sort == "comments" && condition.direction == "asc") Least commented @avatar(collaborator, 20) @collaborator
</a> </a>
</li> </li>
<li> }
<a href="@condition.copy(sort="updated", direction="desc").toURL"> }
@helper.html.checkicon(condition.sort == "updated" && condition.direction == "desc") Recently updated @helper.html.dropdown("Sort", flat = true){
</a> <li>
</li> <a href="@condition.copy(sort="created", direction="desc").toURL">
<li> @helper.html.checkicon(condition.sort == "created" && condition.direction == "desc") Newest
<a href="@condition.copy(sort="updated", direction="asc" ).toURL"> </a>
@helper.html.checkicon(condition.sort == "updated" && condition.direction == "asc") Least recently updated </li>
</a> <li>
</li> <a href="@condition.copy(sort="created", direction="asc" ).toURL">
} @helper.html.checkicon(condition.sort == "created" && condition.direction == "asc") Oldest
<table class="table table-bordered table-hover table-issues"> </a>
@if(issues.isEmpty){ </li>
<tr> <li>
<td style="padding: 20px; background-color: #eee; text-align: center;"> <a href="@condition.copy(sort="comments", direction="desc").toURL">
No issues to show. @helper.html.checkicon(condition.sort == "comments" && condition.direction == "desc") Most commented
@if(condition.labels.nonEmpty || condition.milestoneId.isDefined){ </a>
<a href="@condition.copy(labels = Set.empty, milestoneId = None).toURL">Clear active filters.</a> </li>
} else { <li>
@if(repository.isDefined){ <a href="@condition.copy(sort="comments", direction="asc" ).toURL">
<a href="@url(repository.get)/issues/new">Create a new issue.</a> @helper.html.checkicon(condition.sort == "comments" && condition.direction == "asc") Least commented
} </a>
} </li>
</td> <li>
</tr> <a href="@condition.copy(sort="updated", direction="desc").toURL">
} else { @helper.html.checkicon(condition.sort == "updated" && condition.direction == "desc") Recently updated
@if(hasWritePermission){ </a>
<tr> </li>
<td style="background-color: #eee;"> <li>
<div class="btn-group"> <a href="@condition.copy(sort="updated", direction="asc" ).toURL">
<button class="btn btn-mini strong" id="state">@{if(condition.state == "open") "Close" else "Reopen"}</button> @helper.html.checkicon(condition.sort == "updated" && condition.direction == "asc") Least recently updated
</div> </a>
@helper.html.dropdown("Label") { </li>
@labels.map { label =>
<li>
<a href="javascript:void(0);" class="toggle-label" data-id="@label.labelId">
<i class="icon-white"></i>
<span class="label" style="background-color: #@label.color;">&nbsp;</span>
@label.labelName
</a>
</li>
}
}
@helper.html.dropdown("Assignee") {
<li><a href="javascript:void(0);" class="toggle-assign" data-name=""><i class="icon-remove-circle"></i> Clear assignee</a></li>
@collaborators.map { collaborator =>
<li><a href="javascript:void(0);" class="toggle-assign" data-name="@collaborator"><i class="icon-white"></i>@avatar(collaborator, 20) @collaborator</a></li>
}
}
@helper.html.dropdown("Milestone") {
<li><a href="javascript:void(0);" class="toggle-milestone" data-id=""><i class="icon-remove-circle"></i> Clear this milestone</a></li>
@milestones.map { milestone =>
<li>
<a href="javascript:void(0);" class="toggle-milestone" data-id="@milestone.milestoneId">
<i class="icon-white"></i> @milestone.title
<div class="small" style="padding-left: 20px;">
@milestone.dueDate.map { dueDate =>
@if(isPast(dueDate)){
<img src="@assets/common/images/alert_mono.png"/>Due in @date(dueDate)
} else {
<span class="muted">Due in @date(dueDate)</span>
}
}.getOrElse {
<span class="muted">No due date</span>
}
</div>
</a>
</li>
}
}
</td>
</tr>
} }
}
@issues.map { case (issue, labels, commentCount) =>
<tr>
<td>
@if(hasWritePermission){
<label class="checkbox" style="cursor: default;">
<input type="checkbox" value="@issue.issueId"/>
}
@if(issue.isPullRequest){
<img src="@assets/common/images/pullreq-@(if(issue.closed) "closed" else "open").png"/>
} else {
<img src="@assets/common/images/issue-@(if(issue.closed) "closed" else "open").png"/>
}
@if(repository.isEmpty){
<a href="@path/@issue.userName/@issue.repositoryName">@issue.repositoryName</a>&nbsp;&#xFF65;
}
@if(issue.isPullRequest){
<a href="@path/@issue.userName/@issue.repositoryName/pull/@issue.issueId" class="issue-title">@issue.title</a>
} else {
<a href="@path/@issue.userName/@issue.repositoryName/issues/@issue.issueId" class="issue-title">@issue.title</a>
}
@labels.map { label =>
<span class="label-color small" style="background-color: #@label.color; color: #@label.fontColor; padding-left: 4px; padding-right: 4px">@label.labelName</span>
}
<span class="pull-right muted">
@issue.assignedUserName.map { userName =>
@avatar(userName, 20, tooltip = true)
}
#@issue.issueId
</span>
<div class="small muted" style="margin-left: 20px;">
Opened by @user(issue.openedUserName, styleClass="username") @datetime(issue.registeredDate)&nbsp;
@if(commentCount > 0){
<i class="icon-comment"></i><a href="@path/@issue.userName/@issue.repositoryName/issues/@issue.issueId" class="issue-comment-count">@commentCount @plural(commentCount, "comment")</a>
}
</div>
@if(hasWritePermission){
</label>
}
</td>
</tr>
}
</table>
<div class="pull-right">
@helper.html.paginator(page, (if(condition.state == "open") openCount else closedCount), service.IssuesService.IssueLimit, 10, condition.toURL)
</div> </div>
</div> @if(hasWritePermission){
<div class="pull-right" id="table-issues-batchedit">
@helper.html.dropdown("Mark as", flat = true) {
<li><a href="javascript:void(0);" class="toggle-state" data-id="open">Open</a></li>
<li><a href="javascript:void(0);" class="toggle-state" data-id="close">Close</a></li>
}
@helper.html.dropdown("Label", flat = true) {
@labels.map { label =>
<li>
<a href="javascript:void(0);" class="toggle-label" data-id="@label.labelId">
<i class="icon-white"></i>
<span class="label" style="background-color: #@label.color;">&nbsp;</span>
@label.labelName
</a>
</li>
}
}
@helper.html.dropdown("Milestone", flat = true) {
<li><a href="javascript:void(0);" class="toggle-milestone" data-id="">No milestone</a></li>
@milestones.filter(_.closedDate.isEmpty).map { milestone =>
<li><a href="javascript:void(0);" class="toggle-milestone" data-id="@milestone.milestoneId">@milestone.title</a></li>
}
}
@helper.html.dropdown("Assignee", flat = true) {
<li><a href="javascript:void(0);" class="toggle-assign" data-name=""><i class="icon-remove-circle"></i> Clear assignee</a></li>
@collaborators.map { collaborator =>
<li><a href="javascript:void(0);" class="toggle-assign" data-name="@collaborator"><i class="icon-white"></i>@avatar(collaborator, 20) @collaborator</a></li>
}
}
</div>
}
</th>
</tr>
@if(issues.isEmpty){
<tr>
<td style="padding: 20px; background-color: #eee; text-align: center;">
@if(target == "issues"){
No issues to show.
} else {
No pull requests to show.
}
@if(condition.labels.nonEmpty || condition.milestoneId.isDefined){
<a href="@condition.copy(labels = Set.empty, milestoneId = None).toURL">Clear active filters.</a>
} else {
@if(repository.isDefined){
@if(target == "issues"){
<a href="@url(repository.get)/issues/new">Create a new issue.</a>
} else {
<a href="@url(repository.get)/compare">Create a new pull request.</a>
}
}
}
</td>
</tr>
}
@issues.map { case IssueInfo(issue, labels, milestone, commentCount) =>
<tr>
<td style="padding-top: 15px; padding-bottom: 15px;">
@if(hasWritePermission){
<input type="checkbox" value="@issue.issueId"/>
}
<img src="@assets/common/images/issue-@(if(issue.closed) "closed" else "open").png" style="margin-right: 20px;"/>
@if(repository.isEmpty){
<a href="@path/@issue.userName/@issue.repositoryName">@issue.repositoryName</a>&nbsp;&#xFF65;
}
@if(target == "issues"){
<a href="@path/@issue.userName/@issue.repositoryName/issues/@issue.issueId" class="issue-title">@issue.title</a>
} else {
<a href="@path/@issue.userName/@issue.repositoryName/pull/@issue.issueId" class="issue-title">@issue.title</a>
}
@labels.map { label =>
<span class="label-color small" style="background-color: #@label.color; color: #@label.fontColor; padding-left: 4px; padding-right: 4px">@label.labelName</span>
}
<span class="pull-right small">
@issue.assignedUserName.map { userName =>
@avatar(userName, 20, tooltip = true)
}
@if(commentCount > 0){
<a href="@path/@issue.userName/@issue.repositoryName/issues/@issue.issueId" class="issue-comment-count">
<img src="@assets/common/images/comment-active.png"> @commentCount
</a>
} else {
<a href="@path/@issue.userName/@issue.repositoryName/issues/@issue.issueId" class="issue-comment-count" style="color: silver;">
<img src="@assets/common/images/comment.png"> @commentCount
</a>
}
</span>
<div class="small muted" style="margin-left: 40px; margin-top: 5px;">
#@issue.issueId opened @helper.html.datetimeago(issue.registeredDate) by @user(issue.openedUserName, styleClass="username")
@milestone.map { milestone =>
<span style="margin: 20px;"><a href="@condition.copy(milestoneId = Some(Some(1))).toURL" class="username"><img src="@assets/common/images/milestone.png"> @milestone</a></span>
}
</div>
</td>
</tr>
}
</table>
<div class="pull-right">
@helper.html.paginator(page, (if(condition.state == "open") openCount else closedCount), service.IssuesService.IssueLimit, 10, condition.toURL)
</div>

View File

@@ -2,12 +2,18 @@
@import context._ @import context._
@import view.helpers._ @import view.helpers._
@html.main(s"Milestones - ${repository.owner}/${repository.name}"){ @html.main(s"Milestones - ${repository.owner}/${repository.name}"){
@html.menu("milestones", repository){ @html.menu("issues", repository){
@issues.html.tab("milestones", false, repository) @if(milestone.isEmpty){
<h4>New milestone</h4>
<div class="muted">Create a new milestone to help organize your issues and pull requests.</div>
} else {
@issues.html.tab("milestones", false, repository)
<br><br>
}
<hr style="margin-top: 12px; margin-bottom: 18px;" class="fill-width"/>
<form method="POST" action="@url(repository)/issues/milestones/@if(milestone.isEmpty){new}else{@milestone.get.milestoneId/edit}" validate="true"> <form method="POST" action="@url(repository)/issues/milestones/@if(milestone.isEmpty){new}else{@milestone.get.milestoneId/edit}" validate="true">
<fieldset> <fieldset>
<label for="title"><string>Title</string></label> <input type="text" id="title" name="title" style="width: 500px;" value="@milestone.map(_.title)" placeholder="Title"/>
<input type="text" id="title" name="title" style="width: 400px;" value="@milestone.map(_.title)"/>
<span id="error-title" class="error"></span> <span id="error-title" class="error"></span>
</fieldset> </fieldset>
<fieldset> <fieldset>

View File

@@ -6,94 +6,94 @@
@import view.helpers._ @import view.helpers._
@html.main(s"Milestones - ${repository.owner}/${repository.name}"){ @html.main(s"Milestones - ${repository.owner}/${repository.name}"){
@html.menu("issues", repository){ @html.menu("issues", repository){
@issues.html.tab("milestones", false, repository) @issues.html.tab("milestones", hasWritePermission, repository)
<div class="row-fluid"> <br>
<div class="span3"> <table class="table table-bordered table-hover table-issues">
<ul class="nav nav-pills nav-stacked"> <tr>
<li@if(state == "open"){ class="active"}> <th style="background-color: #eee;">
<a href="?state=open"> <span class="small">
<span class="count-right">@milestones.filter(_._1.closedDate.isEmpty).size</span> <a class="button-link@if(state == "open"){ selected}" href="?state=open">
Open Milestones <img src="@assets/common/images/milestone@(if(state == "open"){"-active"}).png"/>
@milestones.filter(_._1.closedDate.isEmpty).size Open
</a>&nbsp;&nbsp;
<a class="button-link@if(state == "closed"){ selected}" href="?state=closed">
<img src="@assets/common/images/milestone@(if(state == "closed"){"-active"}).png"/>
@milestones.filter(_._1.closedDate.isDefined).size Closed
</a> </a>
</li> </span>
<li@if(state == "closed"){ class="active"}> </th>
<a href="?state=closed"> </tr>
<span class="count-right">@milestones.filter(_._1.closedDate.isDefined).size</span> @defining(milestones.filter { case (milestone, _, _) =>
Closed Milestones milestone.closedDate.map(_ => state == "closed").getOrElse(state == "open")
</a> }){ milestones =>
</li> @milestones.map { case (milestone, openCount, closedCount) =>
</ul> <tr>
@if(hasWritePermission){ <td style="padding-top: 15px; padding-bottom: 15px;">
<hr> <div class="milestone row-fluid">
<a href="@url(repository)/issues/milestones/new" class="btn btn-block">Create a new milestone</a> <div class="span4">
} <a href="@url(repository)/issues?milestone=@milestone.milestoneId&state=open" class="milestone-title">@milestone.title</a>
</div> <div style="margin-top: 6px">
<div class="span9"> @if(milestone.closedDate.isDefined){
<table class="table table-bordered table-hover"> <span class="muted">Closed @helper.html.datetimeago(milestone.closedDate.get)</span>
@defining(milestones.filter { case (milestone, _, _) => } else {
milestone.closedDate.map(_ => state == "closed").getOrElse(state == "open") @milestone.dueDate.map { dueDate =>
}){ milestones => @if(isPast(dueDate)){
@milestones.map { case (milestone, openCount, closedCount) => <img src="@assets/common/images/alert.png"/><span class="muted milestone-alert">Due by @date(dueDate)</span>
<tr> } else {
<td> <span class="muted">Due by @date(dueDate)</span>
<div class="milestone row-fluid">
<div class="span4">
<a href="@url(repository)/issues?milestone=@milestone.milestoneId&state=open" class="milestone-title">@milestone.title</a><br>
@if(milestone.closedDate.isDefined){
<span class="muted">Closed @datetime(milestone.closedDate.get)</span>
} else {
@milestone.dueDate.map { dueDate =>
@if(isPast(dueDate)){
<img src="@assets/common/images/alert.png"/><span class="muted milestone-alert">Due in @date(dueDate)</span>
} else {
<span class="muted">Due in @date(dueDate)</span>
}
}.getOrElse {
<span class="muted">No due date</span>
} }
}.getOrElse {
<span class="muted">No due date</span>
}
}
</div>
</div>
<div class="span8">
@progress(openCount + closedCount, closedCount)
<div>
<div>
@if(closedCount == 0){
0%
} else {
@((closedCount.toDouble / (openCount + closedCount).toDouble * 100).toInt)%
} <span class="muted">complete</span> &nbsp;&nbsp;
@openCount <span class="muted">open</span> &nbsp;&nbsp;
@closedCount <span class="muted">closed</span>
</div>
<div class="milestone-menu">
@if(hasWritePermission){
<a href="@url(repository)/issues/milestones/@milestone.milestoneId/edit">Edit</a> &nbsp;&nbsp;
@if(milestone.closedDate.isDefined){
<a href="@url(repository)/issues/milestones/@milestone.milestoneId/open">Open</a> &nbsp;&nbsp;
} else {
<a href="@url(repository)/issues/milestones/@milestone.milestoneId/close">Close</a> &nbsp;&nbsp;
}
<a href="@url(repository)/issues/milestones/@milestone.milestoneId/delete" class="delete">Delete</a>
} }
</div> </div>
<div class="span8">
<div class="milestone-menu">
<div class="pull-right">
@if(hasWritePermission){
<a href="@url(repository)/issues/milestones/@milestone.milestoneId/edit">Edit
@if(milestone.closedDate.isDefined){
<a href="@url(repository)/issues/milestones/@milestone.milestoneId/open">Open</a>
} else {
<a href="@url(repository)/issues/milestones/@milestone.milestoneId/close">Close</a>
}
<a href="@url(repository)/issues/milestones/@milestone.milestoneId/delete" class="delete">Delete</a>
}
<a href="@url(repository)/issues?milestone=@milestone.milestoneId&state=open">Browse issues</a>
</div>
<span class="muted">@closedCount closed - @openCount open</span>
</div>
@progress(openCount + closedCount, closedCount, true)
</div>
</div> </div>
@if(milestone.description.isDefined){ </div>
<div class="milestone-description"> </div>
@markdown(milestone.description.get, repository, false, false) @if(milestone.description.isDefined){
</div> <div class="milestone-description">
} @markdown(milestone.description.get, repository, false, false)
</td> </div>
</tr>
} }
@if(milestones.isEmpty){ </td>
<tr> </tr>
<td style="padding: 20px; background-color: #eee; text-align: center;"> }
No milestones to show. @if(milestones.isEmpty){
@if(hasWritePermission){ <tr>
<a href="@url(repository)/issues/milestones/new">Create a new milestone.</a> <td style="padding: 20px; background-color: #eee; text-align: center;">
} No milestones to show.
</td> @if(hasWritePermission){
</tr> <a href="@url(repository)/issues/milestones/new">Create a new milestone.</a>
} }
} </td>
</table> </tr>
</div> }
</div> }
</table>
} }
} }
<script> <script>

View File

@@ -1,15 +1,6 @@
@(total: Int, progress: Int, showPercentage: Boolean) @(total: Int, progress: Int)
<div class="milestone-progress"> <div class="milestone-progress">
@if(progress > 0){ @if(progress > 0){
<span class="milestone-progress" style="width: @((progress.toDouble / total.toDouble * 100).toInt)%;"></span> <span class="milestone-progress" style="width: @((progress.toDouble / total.toDouble * 100).toInt)%;"></span>
} }
@if(showPercentage){
<span class="milestone-percentage">
@if(progress == 0){
0%
} else {
@((progress.toDouble / total.toDouble * 100).toInt)%
}
</span>
}
</div> </div>

View File

@@ -1,16 +1,28 @@
@(active: String, create: Boolean, repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context) @(active: String, newButton: Boolean,
repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
@import context._ @import context._
@import view.helpers._ @import view.helpers._
<ul class="nav nav-tabs pull-left fill-width"> <ul class="nav nav-pills-group pull-left fill-width">
<li@if(active == "issues"){ class="active"}><a href="@url(repository)/issues">Browse Issues</a></li> <li class="@if(active == "issues" ){active} first"><a href="@url(repository)/issues">Issues</a></li>
<li@if(active == "milestones"){ class="active"}><a href="@url(repository)/issues/milestones">Milestones</a></li> <li class="@if(active == "pulls" ){active}"><a href="@url(repository)/pulls">Pull requests</a></li>
<li class="@if(active == "labels" ){active}"><a href="@url(repository)/issues/labels">Labels</a></li>
<li class="@if(active == "milestones"){active} last"><a href="@url(repository)/issues/milestones">Milestones</a></li>
@if(loginAccount.isDefined){ @if(loginAccount.isDefined){
<li class="pull-right"> <li class="pull-right">
<div class="btn-group"> <div class="btn-group">
@if(create){ @if(newButton){
<a class="btn btn-small btn-success" href="#" disabled="disabled">New Issue</a> @if(active == "issues"){
} else { <a class="btn btn-success" href="@url(repository)/issues/new">New issue</a>
<a class="btn btn-small btn-success" href="@url(repository)/issues/new">New Issue</a> }
@if(active == "pulls"){
<a class="btn btn-success" href="@url(repository)/compare">New pull request</a>
}
@if(active == "labels"){
<a class="btn btn-success" href="javascript:void(0);" id="new-label-button">New label</a>
}
@if(active == "milestones"){
<a class="btn btn-success" href="@url(repository)/issues/milestones/new">New milestone</a>
}
} }
</div> </div>
</li> </li>

View File

@@ -1,7 +1,9 @@
@(active: String, @(active: String,
repository: service.RepositoryService.RepositoryInfo, repository: service.RepositoryService.RepositoryInfo,
id: Option[String] = None, id: Option[String] = None,
expand: Boolean = false)(body: Html)(implicit context: app.Context) expand: Boolean = false,
info: Option[Any] = None,
error: Option[Any] = None)(body: Html)(implicit context: app.Context)
@import context._ @import context._
@import view.helpers._ @import view.helpers._
@@ -31,6 +33,8 @@
} }
<div class="container"> <div class="container">
@helper.html.information(info)
@helper.html.error(error)
@if(repository.commitCount > 0){ @if(repository.commitCount > 0){
<div class="pull-right"> <div class="pull-right">
<div class="input-prepend"> <div class="input-prepend">

View File

@@ -58,7 +58,7 @@
<div style="width: 600px; border-right: 1px solid #d4d4d4;"> <div style="width: 600px; border-right: 1px solid #d4d4d4;">
<span class="error" id="error-title"></span> <span class="error" id="error-title"></span>
<input type="text" name="title" style="width: 580px" placeholder="Title"/> <input type="text" name="title" style="width: 580px" placeholder="Title"/>
@helper.html.preview(repository, "", false, true, "width: 580px; height: 200px;") @helper.html.preview(repository, "", false, true, true, hasWritePermission, "width: 580px; height: 200px;")
<input type="hidden" name="targetUserName" value="@originRepository.owner"/> <input type="hidden" name="targetUserName" value="@originRepository.owner"/>
<input type="hidden" name="targetBranch" value="@originId"/> <input type="hidden" name="targetBranch" value="@originId"/>
<input type="hidden" name="requestUserName" value="@forkedRepository.owner"/> <input type="hidden" name="requestUserName" value="@forkedRepository.owner"/>

View File

@@ -11,7 +11,6 @@
@import view.helpers._ @import view.helpers._
<div class="row-fluid"> <div class="row-fluid">
<div class="span10"> <div class="span10">
@issues.html.issuedetail(issue, comments, collaborators, milestones, hasWritePermission, repository)
@issues.html.commentlist(issue, comments, hasWritePermission, repository, Some(pullreq)) @issues.html.commentlist(issue, comments, hasWritePermission, repository, Some(pullreq))
@defining(comments.exists(_.action == "merge")){ merged => @defining(comments.exists(_.action == "merge")){ merged =>
@if(hasWritePermission && !issue.closed){ @if(hasWritePermission && !issue.closed){
@@ -67,7 +66,7 @@
</div> </div>
} }
<hr/> <hr/>
@issues.html.labels(issue, issueLabels, labels, hasWritePermission, repository) @issues.html.issueinfo(issue, comments, issueLabels, collaborators, milestones, labels, hasWritePermission, repository)
</div> </div>
</div> </div>
<script> <script>

View File

@@ -1,51 +0,0 @@
@(issues: List[(model.Issue, List[model.Label], Int)],
counts: List[service.PullRequestService.PullRequestCount],
filter: Option[String],
page: Int,
openCount: Int,
closedCount: Int,
allCount: Int,
condition: service.IssuesService.IssueSearchCondition,
repository: service.RepositoryService.RepositoryInfo,
hasWritePermission: Boolean)(implicit context: app.Context)
@import context._
@import view.helpers._
@html.main(s"Pull Requests - ${repository.owner}/${repository.name}", Some(repository)){
@html.menu("pulls", repository){
<div class="row-fluid">
<div class="span3">
<ul class="nav nav-pills nav-stacked">
<li@if(filter.isEmpty){ class="active"}>
<a href="@url(repository)/pulls">
<span class="count-right">@allCount</span>
All Requests
</a>
</li>
@if(loginAccount.isDefined){
<li@if(filter.map(_ == loginAccount.get.userName).getOrElse(false)){ class="active"}>
<a href="@url(repository)/pulls/@loginAccount.map(_.userName)">
<span class="count-right">@counts.find(_.userName == loginAccount.get.userName).map(_.count).getOrElse(0)</span>
Yours
</a>
</li>
}
</ul>
<hr>
<ul class="nav nav-pills nav-stacked small">
@counts.map { user =>
@if(loginAccount.isEmpty || loginAccount.get.userName != user.userName){
<li@if(filter.map(_ == user.userName).getOrElse(false)){ class="active"}>
<a href="@url(repository)/pulls/@user.userName">
<span class="count-right">@user.count</span>
@user.userName
</a>
</li>
}
}
</ul>
</div>
@listparts(issues, page, openCount, closedCount, condition, Some(repository), hasWritePermission)
</div>
}
}

View File

@@ -1,100 +0,0 @@
@(issues: List[(model.Issue, List[model.Label], Int)],
page: Int,
openCount: Int,
closedCount: Int,
condition: service.IssuesService.IssueSearchCondition,
repository: Option[service.RepositoryService.RepositoryInfo],
hasWritePermission: Boolean)(implicit context: app.Context)
@import context._
@import view.helpers._
<div class="span9">
@repository.map { repository =>
@if(hasWritePermission){
<div class="pull-right">
@helper.html.paginator(page, (if(condition.state == "open") openCount else closedCount), service.PullRequestService.PullRequestLimit, 7, condition.toURL)
<a href="@url(repository)/compare" class="btn btn-small btn-success">New pull request</a>
</div>
}
}
<div class="btn-group">
<a class="btn btn-small@if(condition.state == "open"){ active}" href="@condition.copy(state = "open").toURL">@openCount Open</a>
<a class="btn btn-small@if(condition.state == "closed"){ active}" href="@condition.copy(state = "closed").toURL">@closedCount Closed</a>
</div>
@helper.html.dropdown(
value = (condition.sort, condition.direction) match {
case ("created" , "desc") => "Newest"
case ("created" , "asc" ) => "Oldest"
case ("comments", "desc") => "Most commented"
case ("comments", "asc" ) => "Least commented"
case ("updated" , "desc") => "Recently updated"
case ("updated" , "asc" ) => "Least recently updated"
},
prefix = "Sort",
mini = false
){
<li>
<a href="@condition.copy(sort="created", direction="desc").toURL">
@helper.html.checkicon(condition.sort == "created" && condition.direction == "desc") Newest
</a>
</li>
<li>
<a href="@condition.copy(sort="created", direction="asc" ).toURL">
@helper.html.checkicon(condition.sort == "created" && condition.direction == "asc") Oldest
</a>
</li>
<li>
<a href="@condition.copy(sort="comments", direction="desc").toURL">
@helper.html.checkicon(condition.sort == "comments" && condition.direction == "desc") Most commented
</a>
</li>
<li>
<a href="@condition.copy(sort="comments", direction="asc" ).toURL">
@helper.html.checkicon(condition.sort == "comments" && condition.direction == "asc") Least commented
</a>
</li>
<li>
<a href="@condition.copy(sort="updated", direction="desc").toURL">
@helper.html.checkicon(condition.sort == "updated" && condition.direction == "desc") Recently updated
</a>
</li>
<li>
<a href="@condition.copy(sort="updated", direction="asc" ).toURL">
@helper.html.checkicon(condition.sort == "updated" && condition.direction == "asc") Least recently updated
</a>
</li>
}
<table class="table table-bordered table-hover table-issues">
@if(issues.isEmpty){
<tr>
<td style="padding: 20px; background-color: #eee; text-align: center;">
No pull requests to show.
</td>
</tr>
}
@issues.map { case (issue, labels, commentCount) =>
<tr>
<td>
<img src="@assets/common/images/pullreq-@(if(issue.closed) "closed" else "open").png"/>
<a href="@path/@issue.userName/@issue.repositoryName/pull/@issue.issueId" class="issue-title">@issue.title</a>
<span class="pull-right muted">#@issue.issueId</span>
<div style="margin-left: 20px;">
@issue.content.map { content =>
@cut(content, 90)
}.getOrElse {
<span class="muted">No description available</span>
}
</div>
<div class="small muted" style="margin-left: 20px;">
@avatarLink(issue.openedUserName, 20) by @user(issue.openedUserName, styleClass="username") @datetime(issue.registeredDate)&nbsp;
@if(commentCount > 0){
<i class="icon-comment"></i><a href="@path/@issue.userName/@issue.repositoryName/issues/@issue.issueId" class="issue-comment-count">@commentCount @plural(commentCount, "comment")</a>
}
</div>
</td>
</tr>
}
</table>
<div class="pull-right">
@helper.html.paginator(page, (if(condition.state == "open") openCount else closedCount), service.PullRequestService.PullRequestLimit, 10, condition.toURL)
</div>
</div>

View File

@@ -20,7 +20,7 @@
<span class="label label-info">Merged</span> <span class="label label-info">Merged</span>
@user(comment.commentedUserName, styleClass="username strong") merged @commits.size @plural(commits.size, "commit") @user(comment.commentedUserName, styleClass="username strong") merged @commits.size @plural(commits.size, "commit")
into <code>@pullreq.userName:@pullreq.branch</code> from <code>@pullreq.requestUserName:@pullreq.requestBranch</code> into <code>@pullreq.userName:@pullreq.branch</code> from <code>@pullreq.requestUserName:@pullreq.requestBranch</code>
at @datetime(comment.registeredDate) @helper.html.datetimeago(comment.registeredDate)
}.getOrElse { }.getOrElse {
<span class="label label-important">Closed</span> <span class="label label-important">Closed</span>
@user(issue.openedUserName, styleClass="username strong") wants to merge @commits.size @plural(commits.size, "commit") @user(issue.openedUserName, styleClass="username strong") wants to merge @commits.size @plural(commits.size, "commit")

View File

@@ -9,10 +9,10 @@
@html.main(s"${repository.owner}/${repository.name}", Some(repository)) { @html.main(s"${repository.owner}/${repository.name}", Some(repository)) {
@html.menu("code", repository){ @html.menu("code", repository){
<div class="head"> <div class="head">
@helper.html.dropdown( @helper.html.branchcontrol(
value = if(branch.length == 40) branch.substring(0, 10) else branch, branch,
prefix = if(branch.length == 40) "tree" else if(repository.branchList.contains(branch)) "branch" else "tree", repository,
mini = true hasWritePermission
){ ){
@repository.branchList.map { x => @repository.branchList.map { x =>
<li><a href="@url(repository)/blob/@encodeRefName(x)/@pathList.mkString("/")">@helper.html.checkicon(x == branch) @x</a></li> <li><a href="@url(repository)/blob/@encodeRefName(x)/@pathList.mkString("/")">@helper.html.checkicon(x == branch) @x</a></li>
@@ -34,7 +34,7 @@
<div class="pull-left"> <div class="pull-left">
@avatar(latestCommit, 20) @avatar(latestCommit, 20)
@user(latestCommit.authorName, latestCommit.authorEmailAddress, "username strong") @user(latestCommit.authorName, latestCommit.authorEmailAddress, "username strong")
<span class="muted">@datetime(latestCommit.commitTime)</span> <span class="muted">@helper.html.datetimeago(latestCommit.commitTime)</span>
<a href="@url(repository)/commit/@latestCommit.id" class="commit-message">@link(latestCommit.summary, repository)</a> <a href="@url(repository)/commit/@latestCommit.id" class="commit-message">@link(latestCommit.summary, repository)</a>
</div> </div>
<div class="btn-group pull-right"> <div class="btn-group pull-right">

View File

@@ -22,7 +22,7 @@
} }
</td> </td>
<td> <td>
@datetime(latestUpdateDate) @helper.html.datetimeago(latestUpdateDate, false)
</td> </td>
<td> <td>
@if(repository.repository.defaultBranch == branchName){ @if(repository.repository.defaultBranch == branchName){

View File

@@ -68,13 +68,13 @@
<div class="author"> <div class="author">
@avatar(commit, 20) @avatar(commit, 20)
<span>@user(commit.authorName, commit.authorEmailAddress, "username strong")</span> <span>@user(commit.authorName, commit.authorEmailAddress, "username strong")</span>
<span class="muted">authored on @datetime(commit.authorTime)</span> <span class="muted">authored @helper.html.datetimeago(commit.authorTime)</span>
</div> </div>
@if(commit.isDifferentFromAuthor) { @if(commit.isDifferentFromAuthor) {
<div class="committer"> <div class="committer">
<span class="icon-arrow-right"></span> <span class="icon-arrow-right"></span>
<span>@user(commit.committerName, commit.committerEmailAddress, "username strong")</span> <span>@user(commit.committerName, commit.committerEmailAddress, "username strong")</span>
<span class="muted"> committed on @datetime(commit.commitTime)</span> <span class="muted"> committed @helper.html.datetimeago(commit.commitTime)</span>
</div> </div>
} }
</div> </div>

View File

@@ -3,16 +3,17 @@
repository: service.RepositoryService.RepositoryInfo, repository: service.RepositoryService.RepositoryInfo,
commits: Seq[Seq[util.JGitUtil.CommitInfo]], commits: Seq[Seq[util.JGitUtil.CommitInfo]],
page: Int, page: Int,
hasNext: Boolean)(implicit context: app.Context) hasNext: Boolean,
hasWritePermission: Boolean)(implicit context: app.Context)
@import context._ @import context._
@import view.helpers._ @import view.helpers._
@html.main(s"${repository.owner}/${repository.name}", Some(repository)) { @html.main(s"${repository.owner}/${repository.name}", Some(repository)) {
@html.menu("code", repository){ @html.menu("code", repository){
<div class="head"> <div class="head">
@helper.html.dropdown( @helper.html.branchcontrol(
value = if(branch.length == 40) branch.substring(0, 10) else branch, branch,
prefix = if(branch.length == 40) "tree" else if(repository.branchList.contains(branch)) "branch" else "tree", repository,
mini = true hasWritePermission
){ ){
@repository.branchList.map { x => @repository.branchList.map { x =>
<li><a href="@url(repository)/commits/@encodeRefName(x)">@helper.html.checkicon(x == branch) @x</a></li> <li><a href="@url(repository)/commits/@encodeRefName(x)">@helper.html.checkicon(x == branch) @x</a></li>
@@ -58,11 +59,11 @@
} }
<div class="small"> <div class="small">
@user(commit.authorName, commit.authorEmailAddress, "username") @user(commit.authorName, commit.authorEmailAddress, "username")
<span class="muted">authored @datetime(commit.authorTime)</span> <span class="muted">authored @helper.html.datetimeago(commit.authorTime)</span>
@if(commit.isDifferentFromAuthor) { @if(commit.isDifferentFromAuthor) {
<span class="icon-arrow-right" style="margin-top : -2px ;"></span> <span class="icon-arrow-right" style="margin-top : -2px ;"></span>
@user(commit.committerName, commit.committerEmailAddress, "username") @user(commit.committerName, commit.committerEmailAddress, "username")
<span class="muted">committed @datetime(commit.authorTime)</span> <span class="muted">committed @helper.html.datetimeago(commit.authorTime)</span>
} }
</div> </div>
</div> </div>

View File

@@ -56,6 +56,6 @@
<link href="@assets/vendors/jsdifflib/diffview.css" type="text/css" rel="stylesheet" /> <link href="@assets/vendors/jsdifflib/diffview.css" type="text/css" rel="stylesheet" />
<script> <script>
$(function(){ $(function(){
diffUsingJS('oldText', 'newText', 'diffText'); diffUsingJS('oldText', 'newText', 'diffText', 1);
}); });
</script> </script>

View File

@@ -139,7 +139,7 @@ $(function(){
.append($('<div id="diffText">')) .append($('<div id="diffText">'))
.append($('<textarea id="newText" style="display: none;">').html(editor.getValue())) .append($('<textarea id="newText" style="display: none;">').html(editor.getValue()))
.append($('<textarea id="oldText" style="display: none;">').html($('#initial').val())); .append($('<textarea id="oldText" style="display: none;">').html($('#initial').val()));
diffUsingJS('oldText', 'newText', 'diffText'); diffUsingJS('oldText', 'newText', 'diffText', 1);
} }
}); });
}); });

View File

@@ -4,16 +4,18 @@
latestCommit: util.JGitUtil.CommitInfo, latestCommit: util.JGitUtil.CommitInfo,
files: List[util.JGitUtil.FileInfo], files: List[util.JGitUtil.FileInfo],
readme: Option[(List[String], String)], readme: Option[(List[String], String)],
hasWritePermission: Boolean)(implicit context: app.Context) hasWritePermission: Boolean,
info: Option[Any] = None,
error: Option[Any] = None)(implicit context: app.Context)
@import context._ @import context._
@import view.helpers._ @import view.helpers._
@html.main(s"${repository.owner}/${repository.name}", Some(repository)) { @html.main(s"${repository.owner}/${repository.name}", Some(repository)) {
@html.menu("code", repository, Some(branch), pathList.isEmpty){ @html.menu("code", repository, Some(branch), pathList.isEmpty, info, error){
<div class="head"> <div class="head">
@helper.html.dropdown( @helper.html.branchcontrol(
value = if(branch.length == 40) branch.substring(0, 10) else branch, branch,
prefix = if(branch.length == 40) "tree" else if(repository.branchList.contains(branch)) "branch" else "tree", repository,
mini = true hasWritePermission
){ ){
@repository.branchList.map { x => @repository.branchList.map { x =>
<li><a href="@url(repository)/tree/@encodeRefName(x)">@helper.html.checkicon(x == branch) @x</a></li> <li><a href="@url(repository)/tree/@encodeRefName(x)">@helper.html.checkicon(x == branch) @x</a></li>
@@ -47,13 +49,13 @@
<div class="author"> <div class="author">
@avatar(latestCommit, 20) @avatar(latestCommit, 20)
<span>@user(latestCommit.authorName, latestCommit.authorEmailAddress, "username strong")</span> <span>@user(latestCommit.authorName, latestCommit.authorEmailAddress, "username strong")</span>
<span class="muted"> authored on @datetime(latestCommit.authorTime)</span> <span class="muted"> authored @helper.html.datetimeago(latestCommit.authorTime)</span>
</div> </div>
@if(latestCommit.isDifferentFromAuthor) { @if(latestCommit.isDifferentFromAuthor) {
<div class="committer"> <div class="committer">
<span class="icon-arrow-right"></span> <span class="icon-arrow-right"></span>
<span>@user(latestCommit.committerName, latestCommit.committerEmailAddress, "username strong")</span> <span>@user(latestCommit.committerName, latestCommit.committerEmailAddress, "username strong")</span>
<span class="muted"> committed on @datetime(latestCommit.commitTime)</span> <span class="muted"> committed @helper.html.datetimeago(latestCommit.commitTime)</span>
</div> </div>
} }
</div> </div>
@@ -84,9 +86,19 @@
<td> <td>
@if(file.isDirectory){ @if(file.isDirectory){
@if(file.linkUrl.isDefined){ @if(file.linkUrl.isDefined){
<a href="@file.linkUrl">@file.name</a> <a href="@file.linkUrl">
<span class="simplified-path">@file.name.split("/").toList.init match {
case Nil => {}
case list => {@list.mkString("", "/", "/")}
}</span>@file.name.split("/").toList.last
</a>
} else { } else {
<a href="@url(repository)/tree@{(encodeRefName(branch) :: pathList).mkString("/", "/", "/")}@file.name">@file.name</a> <a href="@url(repository)/tree@{(encodeRefName(branch) :: pathList).mkString("/", "/", "/")}@file.name">
<span class="simplified-path">@file.name.split("/").toList.init match {
case Nil => {}
case list => {@list.mkString("", "/", "/")}
}</span>@file.name.split("/").toList.last
</a>
} }
} else { } else {
<a href="@url(repository)/blob@{(encodeRefName(branch) :: pathList).mkString("/", "/", "/")}@file.name">@file.name</a> <a href="@url(repository)/blob@{(encodeRefName(branch) :: pathList).mkString("/", "/", "/")}@file.name">@file.name</a>
@@ -96,7 +108,7 @@
<a href="@url(repository)/commit/@file.commitId" class="commit-message">@link(file.message, repository)</a> <a href="@url(repository)/commit/@file.commitId" class="commit-message">@link(file.message, repository)</a>
[@user(file.author, file.mailAddress)] [@user(file.author, file.mailAddress)]
</td> </td>
<td style="text-align: right;">@datetime(file.time)</td> <td style="text-align: right;">@helper.html.datetimeago(file.time, false)</td>
</tr> </tr>
} }
</table> </table>

View File

@@ -14,7 +14,7 @@
@repository.tags.reverse.map { tag => @repository.tags.reverse.map { tag =>
<tr> <tr>
<td><a href="@url(repository)/tree/@encodeRefName(tag.name)">@tag.name</a></td> <td><a href="@url(repository)/tree/@encodeRefName(tag.name)">@tag.name</a></td>
<td>@datetime(tag.time)</td> <td>@helper.html.datetimeago(tag.time, false)</td>
<td class="monospace"><a href="@url(repository)/commit/@tag.id">@tag.id.substring(0, 10)</a></td> <td class="monospace"><a href="@url(repository)/commit/@tag.id">@tag.id.substring(0, 10)</a></td>
<td> <td>
<a href="@url(repository)/archive/@{encodeRefName(tag.name)}.zip">ZIP</a> <a href="@url(repository)/archive/@{encodeRefName(tag.name)}.zip">ZIP</a>

View File

@@ -16,7 +16,7 @@
@files.drop((page - 1) * CodeLimit).take(CodeLimit).map { file => @files.drop((page - 1) * CodeLimit).take(CodeLimit).map { file =>
<div> <div>
<h5><a href="@url(repository)/blob/@repository.repository.defaultBranch/@file.path">@file.path</a></h5> <h5><a href="@url(repository)/blob/@repository.repository.defaultBranch/@file.path">@file.path</a></h5>
<div class="small muted">Latest commit at @datetime(file.lastModified)</div> <div class="small muted">Last commited @helper.html.datetimeago(file.lastModified)</div>
<pre class="prettyprint linenums:@file.highlightLineNumber" style="padding-left: 25px;">@Html(file.highlightText)</pre> <pre class="prettyprint linenums:@file.highlightLineNumber" style="padding-left: 25px;">@Html(file.highlightText)</pre>
</div> </div>
} }

View File

@@ -22,7 +22,7 @@
} }
<div class="small muted"> <div class="small muted">
Opened by <a href="@url(issue.openedUserName)" class="username">@issue.openedUserName</a> Opened by <a href="@url(issue.openedUserName)" class="username">@issue.openedUserName</a>
at @datetime(issue.registeredDate) @helper.html.datetimeago(issue.registeredDate)
@if(issue.commentCount > 0){ @if(issue.commentCount > 0){
&nbsp;&nbsp;<i class="icon-comment"></i><span class="strong">@issue.commentCount</span> @plural(issue.commentCount, "comment") &nbsp;&nbsp;<i class="icon-comment"></i><span class="strong">@issue.commentCount</span> @plural(issue.commentCount, "comment")
} }

View File

@@ -1,4 +1,7 @@
@(webHooks: List[model.WebHook], repository: service.RepositoryService.RepositoryInfo, info: Option[Any])(implicit context: app.Context) @(webHooks: List[model.WebHook],
enteredUrl: Option[Any],
repository: service.RepositoryService.RepositoryInfo,
info: Option[Any])(implicit context: app.Context)
@import context._ @import context._
@import view.helpers._ @import view.helpers._
@html.main("Settings", Some(repository)){ @html.main("Settings", Some(repository)){
@@ -11,13 +14,13 @@
<li>@webHook.url <a href="@url(repository)/settings/hooks/delete?url=@urlEncode(webHook.url)" class="remove">(remove)</a></li> <li>@webHook.url <a href="@url(repository)/settings/hooks/delete?url=@urlEncode(webHook.url)" class="remove">(remove)</a></li>
} }
</ul> </ul>
<form method="POST" action="@url(repository)/settings/hooks/add" validate="true"> <form method="POST" validate="true">
<div> <div>
<span class="error" id="error-url"></span> <span class="error" id="error-url"></span>
</div> </div>
<input type="text" name="url" id="url" style="width: 300px; margin-bottom: 0px;"/> <input type="text" name="url" id="url" value="@enteredUrl" style="width: 300px; margin-bottom: 0px;"/>
<input type="submit" class="btn" value="Add"/> <input type="submit" class="btn" formaction="@url(repository)/settings/hooks/add" value="Add"/>
<a href="@url(repository)/settings/hooks/test" class="btn">Test Hook</a> <input type="submit" class="btn" formaction="@url(repository)/settings/hooks/test" value="Test Hook"/>
</form> </form>
} }
} }

View File

@@ -22,7 +22,7 @@
<form action="@url(repository)/wiki/@if(page.isEmpty){_new} else {_edit}" method="POST" validate="true"> <form action="@url(repository)/wiki/@if(page.isEmpty){_new} else {_edit}" method="POST" validate="true">
<span id="error-pageName" class="error"></span> <span id="error-pageName" class="error"></span>
<input type="text" name="pageName" value="@pageName" style="width: 850px; font-weight: bold;" placeholder="Input a page name."/> <input type="text" name="pageName" value="@pageName" style="width: 850px; font-weight: bold;" placeholder="Input a page name."/>
@helper.html.preview(repository, page.map(_.content).getOrElse(""), true, false, "width: 850px; height: 400px;", "") @helper.html.preview(repository, page.map(_.content).getOrElse(""), true, false, false, false, "width: 850px; height: 400px;", "")
<input type="text" name="message" value="" style="width: 850px;" placeholder="Write a small message here explaining this change. (Optional)"/> <input type="text" name="message" value="" style="width: 850px;" placeholder="Write a small message here explaining this change. (Optional)"/>
<input type="hidden" name="currentPageName" value="@pageName"/> <input type="hidden" name="currentPageName" value="@pageName"/>
<input type="hidden" name="id" value="@page.map(_.id)"/> <input type="hidden" name="id" value="@page.map(_.id)"/>
@@ -36,4 +36,4 @@ $(function(){
return confirm('Are you sure you want to delete this page?'); return confirm('Are you sure you want to delete this page?');
}); });
}); });
</script> </script>

View File

@@ -36,7 +36,7 @@
<td width="0%"><input type="checkbox" name="commitId" value="@commit.id"></td> <td width="0%"><input type="checkbox" name="commitId" value="@commit.id"></td>
<td>@avatar(commit, 20)&nbsp;@user(commit.authorName, commit.authorEmailAddress)</td> <td>@avatar(commit, 20)&nbsp;@user(commit.authorName, commit.authorEmailAddress)</td>
<td width="80%"> <td width="80%">
<span class="muted">@datetime(commit.authorTime):</span>&nbsp;@commit.shortMessage <span class="muted">@helper.html.datetimeago(commit.authorTime):</span>&nbsp;@commit.shortMessage
</td> </td>
</tr> </tr>
} }

View File

@@ -12,7 +12,7 @@
<li> <li>
<h1 class="wiki-title">@pageName</h1> <h1 class="wiki-title">@pageName</h1>
<div class="small"> <div class="small">
<span class="muted"><strong>@page.committer</strong> edited this page at @datetime(page.time)</span> <span class="muted"><strong>@page.committer</strong> edited this page @helper.html.datetimeago(page.time)</span>
</div> </div>
</li> </li>
<li class="pull-right"> <li class="pull-right">
@@ -32,7 +32,7 @@
</tr> </tr>
<tr> <tr>
<td> <td>
<ul style="margin-left: 0px; margin-bottom: 0px;"> <ul style="margin-left: 0px; margin-bottom: 0px; word-break: break-all; width: 182px;">
@pages.map { page => @pages.map { page =>
<li style="margin-left:0px; list-style-type: none;"><a href="@url(repository)/wiki/@urlEncode(page)">@page</a></li> <li style="margin-left:0px; list-style-type: none;"><a href="@url(repository)/wiki/@urlEncode(page)">@page</a></li>
} }

View File

@@ -119,6 +119,11 @@ div.container {
width: 920px; width: 920px;
} }
div.container-wide {
padding-left: 10px;
padding-right: 10px;
}
div.pagination { div.pagination {
margin-top: 0px; margin-top: 0px;
margin-bottom: 0px; margin-bottom: 0px;
@@ -608,11 +613,109 @@ li.highlight {
background-color: #ffb; background-color: #ffb;
} }
span.simplified-path {
color: #888;
}
#branch-control-title {
margin: 5px 10px;
font-weight: bold;
}
#branch-control-close {
background: none;
border: none;
color: #aaa;
font-weight: bold;
line-height: 15px;
}
#branch-control-input {
border: solid 1px #ccc;
margin: 10px;
}
.new-branch-name {
font-weight: bold;
font-size: 1.2em;
padding-left: 16px;
}
/****************************************************************************/
/* nav pulls group */
/****************************************************************************/
.nav-pills-group:after {
display: table;
line-height: 0;
content: "";
}
.nav-pills-group:after {
clear: both;
}
.nav-pills-group > li {
float: left;
}
.nav-pills-group > li > a {
padding-right: 12px;
padding-left: 12px;
line-height: 14px;
color: #666;
font-weight: bold;
}
.nav-pills-group > li > a {
padding-top: 10px;
padding-bottom: 10px;
border-left : 1px solid #e5e5e5;
border-top : 1px solid #e5e5e5;
border-bottom : 1px solid #e5e5e5;
}
.nav-pills-group > .first > a {
-webkit-border-radius: 4px 0 0 4px;
-moz-border-radius: 4px 0 0 4px;
border-radius: 4px 0 0 4px;
}
.nav-pills-group > .last > a {
-webkit-border-radius: 0 4px 4px 0;
-moz-border-radius: 0 4px 4px 0;
border-radius: 0 4px 4px 0;
border-right : 1px solid #e5e5e5;
}
.nav-pills-group > .active > a,
.nav-pills-group > .active > a:hover,
.nav-pills-group > .active > a:focus {
color: #ffffff;
background-color: #0088cc;
border-color: #0088cc;
}
/****************************************************************************/ /****************************************************************************/
/* Issues */ /* Issues */
/****************************************************************************/ /****************************************************************************/
.btn-group.open .dropdown-toggle.flat {
background-image: none;
-webkit-box-shadow: none;
-moz-box-shadow: none;
box-shadow: none;
}
a.button-link {
font-weight: normal;
color: gray;
}
a.selected {
font-weight: bold;
color: black;
}
span.issue-status { span.issue-status {
display: block;
font-size: large; font-size: large;
text-align: center; text-align: center;
padding: 8px; padding: 8px;
@@ -625,7 +728,7 @@ table.table-issues {
a.issue-title { a.issue-title {
color: #333; color: #333;
font-weight: bold; font-weight: bold;
size: 110%; font-size: 120%;
} }
ul.label-list { ul.label-list {
@@ -662,8 +765,7 @@ span.milestone-alert {
} }
a.milestone-title { a.milestone-title {
font-size: 120%; font-size: 180%;
font-weight: bold;
} }
div.milestone-description { div.milestone-description {
@@ -671,13 +773,12 @@ div.milestone-description {
color: #666; color: #666;
} }
div.milestone-menu { a.milestone-title {
font-size: 80%; color: #333;
} }
div.milestone-menu a { div.milestone-menu {
margin-left: 8px; margin-top: 8px;
font-weight: bold;
} }
div.milestone-menu a.delete { div.milestone-menu a.delete {
@@ -689,13 +790,13 @@ div#milestone-progress-area {
} }
div#milestone-progress-area div.milestone-progress { div#milestone-progress-area div.milestone-progress {
width: 150px; width: 130px;
margin-bottom: -6px; margin-bottom: -6px;
} }
div.milestone-progress { div.milestone-progress {
position: relative; position: relative;
height: 20px; height: 10px;
color: white; color: white;
margin-bottom: 4px; margin-bottom: 4px;
font-weight: bold; font-weight: bold;
@@ -716,11 +817,6 @@ span.milestone-progress {
-moz-border-radius: 4px; -moz-border-radius: 4px;
} }
span.milestone-percentage {
position: absolute;
padding-left: 8px;
}
div.issue-header { div.issue-header {
padding-left: 8px; padding-left: 8px;
padding-right: 8px; padding-right: 8px;
@@ -805,6 +901,20 @@ div.attachable div.clickable {
background-color: white; background-color: white;
} }
ul.task-list {
padding-left: 2em;
margin-left: 0;
}
li.task-list-item {
list-style-type: none;
}
li.task-list-item input.task-list-item-checkbox {
margin: 0 4px 0.25em -20px;
vertical-align: middle;
}
/****************************************************************************/ /****************************************************************************/
/* Pull Request */ /* Pull Request */
/****************************************************************************/ /****************************************************************************/
@@ -838,20 +948,24 @@ div.author-info div.committer {
/****************************************************************************/ /****************************************************************************/
/* Diff */ /* Diff */
/****************************************************************************/ /****************************************************************************/
table.inlinediff { table.diff {
font-size: 12px; font-size: 12px;
font-family: Monaco, Menlo, Consolas, "Courier New", monospace; font-family: Monaco, Menlo, Consolas, "Courier New", monospace;
width: 100%; width: 100%;
} }
table.inlinediff thead { table.diff thead {
display: none; display: none;
} }
td.insert, td.equal, td.delete { table.inlinediff td.insert, table.inlinediff td.equal, table.inlinediff td.delete {
width: 100%; width: 100%;
} }
td.insert, td.equal, td.delete, td.empty {
width: 50%;
}
/****************************************************************************/ /****************************************************************************/
/* Repository Settings */ /* Repository Settings */
/****************************************************************************/ /****************************************************************************/

Binary file not shown.

Before

Width:  |  Height:  |  Size: 166 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 295 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 349 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 226 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 B

View File

@@ -4,7 +4,7 @@ $(function(){
// repository url text field // repository url text field
$('#repository-url').click(function(){ $('#repository-url').click(function(){
this.select(0, this.value.length); this.select(0, this.value.length);
}); });
// activate tooltip // activate tooltip
@@ -17,7 +17,7 @@ $(function(){
}); });
$('.markdown-head').mouseleave(function(e){ $('.markdown-head').mouseleave(function(e){
var anchorLink = $(e.target).children('a.markdown-anchor-link'); var anchorLink = $(e.target).children('a.markdown-anchor-link');
if(anchorLink.data('active') != true){ if(anchorLink.data('active') !== true){
anchorLink.hide(); anchorLink.hide();
} }
}); });
@@ -39,7 +39,7 @@ function displayErrors(data){
var i = 0; var i = 0;
$.each(data, function(key, value){ $.each(data, function(key, value){
$('#error-' + key.split(".").join("_")).text(value); $('#error-' + key.split(".").join("_")).text(value);
if(i == 0){ if(i === 0){
$('#' + key).focus(); $('#' + key).focus();
} }
i++; i++;
@@ -68,20 +68,26 @@ function displayErrors(data){
}; };
})(jQuery); })(jQuery);
function diffUsingJS(oldTextId, newTextId, outputId) { /**
* Render diff using jsdifflib.
*
* @param oldTextId {String} element id of old text
* @param newTextId {String} element id of new text
* @param outputId {String} element id of output element
* @param viewType {Number} 0: split, 1: unified
*/
function diffUsingJS(oldTextId, newTextId, outputId, viewType) {
// get the baseText and newText values from the two textboxes, and split them into lines // get the baseText and newText values from the two textboxes, and split them into lines
var oldText = document.getElementById(oldTextId).value; var oldText = document.getElementById(oldTextId).value;
if(oldText == ''){ var oldLines = [];
var oldLines = []; if(oldText !== ''){
} else { oldLines = difflib.stringAsLines(oldText);
var oldLines = difflib.stringAsLines(oldText);
} }
var newText = document.getElementById(newTextId).value var newText = document.getElementById(newTextId).value;
if(newText == ''){ var newLines = [];
var newLines = []; if(newText !== ''){
} else { newLines = difflib.stringAsLines(newText);
var newLines = difflib.stringAsLines(newText);
} }
// create a SequenceMatcher instance that diffs the two sets of lines // create a SequenceMatcher instance that diffs the two sets of lines
@@ -100,10 +106,10 @@ function diffUsingJS(oldTextId, newTextId, outputId) {
newTextLines: newLines, newTextLines: newLines,
opcodes: opcodes, opcodes: opcodes,
contextSize: 4, contextSize: 4,
viewType: 1 viewType: viewType
})); }));
} }
function jqSelectorEscape(val) { function jqSelectorEscape(val) {
return val.replace(/[!"#$%&'()*+,.\/:;<=>?@\[\\\]^`{|}~]/g, '\\$&'); return val.replace(/[!"#$%&'()*+,.\/:;<=>?@\[\\\]^`{|}~]/g, '\\$&');
} }

View File

@@ -4,25 +4,25 @@ $(function(){
}); });
$.each($('input[formaction]'), function(i, input){ $.each($('input[formaction]'), function(i, input){
$(input).click(function(){ $(input).click(function(){
var form = $(input).parents('form') var form = $(input).parents('form');
$(form).attr('action', $(input).attr('formaction')) $(form).attr('action', $(input).attr('formaction'));
}); });
}); });
}); });
function validate(e){ function validate(e){
var form = $(e.target); var form = $(e.target);
$(form).find('[type=submit]').attr('disabled', 'disabled') $(form).find('[type=submit]').attr('disabled', 'disabled');
if(form.data('validated') == true){ if(form.data('validated') === true){
return true; return true;
} }
$.post(form.attr('action') + '/validate', $(e.target).serialize(), function(data){ $.post(form.attr('action') + '/validate', $(e.target).serialize(), function(data){
$(form).find('[type=submit]').removeAttr('disabled') $(form).find('[type=submit]').removeAttr('disabled');
// clear all error messages // clear all error messages
$('.error').text(''); $('.error').text('');
if($.isEmptyObject(data)){ if($.isEmptyObject(data)){
form.data('validated', true); form.data('validated', true);
form.submit(); form.submit();
@@ -39,4 +39,4 @@ function displayErrors(data){
$.each(data, function(key, value){ $.each(data, function(key, value){
$('#error-' + key.split(".").join("_")).text(value); $('#error-' + key.split(".").join("_")).text(value);
}); });
} }

Some files were not shown because too many files have changed in this diff Show More