Compare commits

...

74 Commits
2.3 ... 2.4

Author SHA1 Message Date
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
Naoki Takezoe
3b1367dd8e (refs #488)Displays the milestone on the issue list. 2014-09-20 03:02:52 +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
70 changed files with 1674 additions and 1085 deletions

View File

@@ -80,6 +80,13 @@ Run the following commands in `Terminal` to
Release Notes
--------
### 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
- Scala based plugin system
- Embedded Jetty war extraction directory moved to `GITBUCKET_HOME/tmp`

View File

@@ -34,9 +34,9 @@
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="0.7"
inkscape:cx="482.58197"
inkscape:cy="-83.92636"
inkscape:zoom="0.98994949"
inkscape:cx="174.78739"
inkscape:cy="-195.96338"
inkscape:document-units="px"
inkscape:current-layer="layer1-9"
showgrid="false"
@@ -1583,7 +1583,7 @@
<path
id="path2991-7-1-4-1"
transform="translate(-154.10522,1432.0357)"
d="m 359.99999,290.93362 a 104.28571,104.28571 0 1 1 -208.57142,0 104.28571,104.28571 0 1 1 208.57142,0 z"
d="m 359.99999,290.93362 c 0,57.59541 -46.6903,104.28572 -104.28571,104.28572 -57.59541,0 -104.28571,-46.69031 -104.28571,-104.28572 0,-57.5954 46.6903,-104.28571 104.28571,-104.28571 57.59541,0 104.28571,46.69031 104.28571,104.28571 z"
sodipodi:ry="104.28571"
sodipodi:rx="104.28571"
sodipodi:cy="290.93362"
@@ -1592,7 +1592,7 @@
sodipodi:type="arc" />
<path
id="path2993-4-5-8-7"
d="m 359.99999,290.93362 a 104.28571,104.28571 0 1 1 -208.57142,0 104.28571,104.28571 0 1 1 208.57142,0 z"
d="m 359.99999,290.93362 c 0,57.59541 -46.6903,104.28572 -104.28571,104.28572 -57.59541,0 -104.28571,-46.69031 -104.28571,-104.28572 0,-57.5954 46.6903,-104.28571 104.28571,-104.28571 57.59541,0 104.28571,46.69031 104.28571,104.28571 z"
sodipodi:ry="104.28571"
sodipodi:rx="104.28571"
sodipodi:cy="290.93362"
@@ -1643,7 +1643,7 @@
sodipodi:cy="812.36218"
sodipodi:rx="10"
sodipodi:ry="10"
d="m 710,812.36218 a 10,10 0 1 1 -20,0 10,10 0 1 1 20,0 z"
d="m 710,812.36218 c 0,5.52285 -4.47715,10 -10,10 -5.52285,0 -10,-4.47715 -10,-10 0,-5.52284 4.47715,-10 10,-10 5.52285,0 10,4.47716 10,10 z"
transform="matrix(1.2362333,-1.2362333,1.2362333,1.2362333,-1490.7493,1534.7336)" />
<rect
style="fill:#ffffff;fill-opacity:1;stroke:#bebeff;stroke-width:10.80681515000000000;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
@@ -1670,7 +1670,7 @@
style="fill:#ffffff;stroke:#bebefa;stroke-width:22.72570610000000000;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
transform="matrix(1.0049237,0,0,0.61497516,302.39116,1664.7945)"
d="m 372.74629,230.89374 a 21.718279,35.140915 0 1 1 -43.43655,0 21.718279,35.140915 0 1 1 43.43655,0 z"
d="m 372.74629,230.89374 c 0,19.40779 -9.7236,35.14091 -21.71827,35.14091 -11.99468,0 -21.71828,-15.73312 -21.71828,-35.14091 0,-19.40779 9.7236,-35.14092 21.71828,-35.14092 11.99467,0 21.71827,15.73313 21.71827,35.14092 z"
sodipodi:ry="35.140915"
sodipodi:rx="21.718279"
sodipodi:cy="230.89374"
@@ -1680,7 +1680,7 @@
sodipodi:type="arc" />
<path
transform="matrix(1.0049237,0,0,0.61497516,300.85563,1514.4712)"
d="m 372.74629,230.89374 a 21.718279,35.140915 0 1 1 -43.43655,0 21.718279,35.140915 0 1 1 43.43655,0 z"
d="m 372.74629,230.89374 c 0,19.40779 -9.7236,35.14091 -21.71827,35.14091 -11.99468,0 -21.71828,-15.73312 -21.71828,-35.14091 0,-19.40779 9.7236,-35.14092 21.71828,-35.14092 11.99467,0 21.71827,15.73313 21.71827,35.14092 z"
sodipodi:ry="35.140915"
sodipodi:rx="21.718279"
sodipodi:cy="230.89374"
@@ -1690,7 +1690,7 @@
sodipodi:type="arc" />
<path
transform="matrix(1.0049237,0,0,0.61497516,401.70879,1561.5007)"
d="m 372.74629,230.89374 a 21.718279,35.140915 0 1 1 -43.43655,0 21.718279,35.140915 0 1 1 43.43655,0 z"
d="m 372.74629,230.89374 c 0,19.40779 -9.7236,35.14091 -21.71827,35.14091 -11.99468,0 -21.71828,-15.73312 -21.71828,-35.14091 0,-19.40779 9.7236,-35.14092 21.71828,-35.14092 11.99467,0 21.71827,15.73313 21.71827,35.14092 z"
sodipodi:ry="35.140915"
sodipodi:rx="21.718279"
sodipodi:cy="230.89374"
@@ -1698,6 +1698,147 @@
id="path3795-8-4-8-2-1"
style="fill:#ffffff;stroke:#bebeff;stroke-width:12.04511166000000000;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
sodipodi:type="arc" />
<g
id="g3992">
<rect
transform="matrix(0.70710678,0.70710678,-0.70710678,0.70710678,0,0)"
style="fill:#3c3c3c;fill-opacity:1;stroke:none"
width="34.635483"
height="158.96587"
x="1836.6243"
y="-1788.4895"
id="rect2995-0-8-4-1" />
<rect
transform="matrix(0.70710678,-0.70710678,0.70710678,0.70710678,0,0)"
style="fill:#3c3c3c;fill-opacity:1;stroke:none"
width="33.538391"
height="96.944809"
x="1628.6003"
y="1772.8655"
id="rect2995-0-8-4-1-4" />
</g>
<g
id="g4112"
transform="translate(88.611046,-13.773858)">
<rect
transform="matrix(0.70710678,0.70710678,-0.70710678,0.70710678,0,0)"
style="fill:#a0a0a0;fill-opacity:1;stroke:none"
width="34.635483"
height="158.96587"
x="1527.2657"
y="-1466.7803"
id="rect2995-0-8-4-1-5" />
<rect
transform="matrix(0.70710678,-0.70710678,0.70710678,0.70710678,0,0)"
style="fill:#a0a0a0;fill-opacity:1;stroke:none"
width="33.538391"
height="96.944809"
x="1306.8911"
y="1463.507"
id="rect2995-0-8-4-1-4-5" />
</g>
<path
style="fill:#b3b3b3;stroke:none"
d="m 2185.2705,373.3859 -109.47,85.45235 29.4727,-89.94984 z"
id="path3894-1-1"
inkscape:connector-curvature="0" />
<rect
style="fill:#b3b3b3;stroke:#b3b3b3;stroke-width:49.97417831;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0"
id="rect3088-5-5-7"
width="174.36192"
height="89.170021"
x="2060.0393"
y="293.00055" />
<rect
style="fill:#dcdcdc;fill-opacity:1;stroke:#dcdcdc;stroke-width:2.10925268999999990;stroke-linejoin:miter;stroke-miterlimit:4.30000019000000040;stroke-opacity:1;stroke-dasharray:none"
id="rect4170"
width="35.913948"
height="206.36755"
x="2110.2112"
y="507.8555" />
<rect
style="fill:#dcdcdc;fill-opacity:1;stroke:#ffffff;stroke-width:15.12008381000000100;stroke-linejoin:miter;stroke-miterlimit:4.30000019000000040;stroke-opacity:1;stroke-dasharray:none"
id="rect4166"
width="174.5864"
height="76.446434"
x="2035.1414"
y="548.66016" />
<rect
style="fill:#dcdcdc;fill-opacity:1;stroke:#dcdcdc;stroke-width:1.42725468000000010;stroke-linejoin:miter;stroke-miterlimit:4.30000019000000040;stroke-opacity:1;stroke-dasharray:none"
id="rect4174"
width="43.442127"
height="43.442127"
x="1928.0846"
y="-1122.7543"
transform="matrix(0.72181305,0.69208809,-0.72181305,0.69208809,0,0)" />
<path
sodipodi:type="arc"
style="fill:#ffffe6;fill-opacity:1;stroke:#ffffff;stroke-width:10.1960001;stroke-linejoin:miter;stroke-miterlimit:4.30000019;stroke-opacity:1;stroke-dasharray:none"
id="path4364"
sodipodi:cx="1418.2542"
sodipodi:cy="434.14883"
sodipodi:rx="11.111678"
sodipodi:ry="11.111678"
d="m 1429.3658,434.14883 c 0,6.13681 -4.9748,11.11168 -11.1116,11.11168 -6.1369,0 -11.1117,-4.97487 -11.1117,-11.11168 0,-6.13681 4.9748,-11.11167 11.1117,-11.11167 6.1368,0 11.1116,4.97486 11.1116,11.11167 z"
transform="matrix(1.2783369,0,0,1.2783369,315.0834,31.171302)" />
<path
style="fill:#dcdcdc;stroke:none;fill-opacity:1"
d="m 2533.6893,373.6989 -109.47,85.45235 29.4727,-89.94984 z"
id="path3894-1-1-1"
inkscape:connector-curvature="0" />
<rect
style="fill:#dcdcdc;stroke:#dcdcdc;stroke-width:49.97417449999999700;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;fill-opacity:1"
id="rect3088-5-5-7-7"
width="174.36192"
height="89.170021"
x="2408.458"
y="293.31354" />
<rect
style="fill:#3c3c3c;fill-opacity:1;stroke:#888888;stroke-width:73.08132935000000400;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
id="rect3220"
width="104.54597"
height="104.54597"
x="45.94949"
y="1925.303" />
<rect
style="fill:#ffffff;fill-opacity:1;stroke:none"
id="rect3998-1"
width="117.84303"
height="30.608574"
x="1271.0641"
y="-1484.6459"
transform="matrix(-0.70710678,0.70710678,-0.70710678,-0.70710678,0,0)" />
<rect
style="fill:#ffffff;fill-opacity:1;stroke:none"
id="rect3998-1-7"
width="117.84303"
height="30.608574"
x="1408.8896"
y="1314.712"
transform="matrix(0.70710678,0.70710678,-0.70710678,0.70710678,0,0)" />
<rect
style="fill:#0088cc;fill-opacity:1;stroke:#0088cc;stroke-width:73.08132935000000400;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
id="rect3220-4"
width="104.54597"
height="104.54597"
x="337.49615"
y="1924.5376" />
<rect
style="fill:#ffffff;fill-opacity:1;stroke:none"
id="rect3998-1-0"
width="117.84303"
height="30.608574"
x="1064.3683"
y="-1690.2594"
transform="matrix(-0.70710678,0.70710678,-0.70710678,-0.70710678,0,0)" />
<rect
style="fill:#ffffff;fill-opacity:1;stroke:none"
id="rect3998-1-7-9"
width="117.84303"
height="30.608574"
x="1614.5032"
y="1108.0162"
transform="matrix(0.70710678,0.70710678,-0.70710678,0.70710678,0,0)" />
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 87 KiB

After

Width:  |  Height:  |  Size: 93 KiB

View File

@@ -42,7 +42,7 @@ object MyBuild extends Build {
"org.apache.commons" % "commons-email" % "1.3.1",
"org.apache.httpcomponents" % "httpclient" % "4.3",
"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",
"org.quartz-scheduler" % "quartz" % "2.2.1",
"com.h2database" % "h2" % "1.4.180",

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 "$@"

View File

@@ -50,20 +50,20 @@ trait DashboardControllerBase extends ControllerBase {
val userName = context.loginAccount.get.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)
dashboard.html.issues(
issues.html.listparts(
searchIssue(condition, filterUser, false, (page - 1) * IssueLimit, IssueLimit, userRepos: _*),
dashboard.html.issueslist(
searchIssue(condition, false, (page - 1) * IssueLimit, IssueLimit, userRepos: _*),
page,
countIssue(condition.copy(state = "open" ), filterUser, false, userRepos: _*),
countIssue(condition.copy(state = "closed"), filterUser, false, userRepos: _*),
countIssue(condition.copy(state = "open" ), false, userRepos: _*),
countIssue(condition.copy(state = "closed"), false, userRepos: _*),
condition),
countIssue(condition, Map.empty, false, userRepos: _*),
countIssue(condition, Map("assigned" -> userName), false, userRepos: _*),
countIssue(condition, Map("created_by" -> userName), false, userRepos: _*),
countIssueGroupByRepository(condition, filterUser, false, userRepos: _*),
countIssue(condition.copy(assigned = None, author = None), false, userRepos: _*),
countIssue(condition.copy(assigned = Some(userName), author = None), false, userRepos: _*),
countIssue(condition.copy(assigned = None, author = Some(userName)), false, userRepos: _*),
countIssueGroupByRepository(condition, false, userRepos: _*),
condition,
filter)
@@ -80,24 +80,24 @@ trait DashboardControllerBase extends ControllerBase {
}.copy(repo = repository))
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 page = IssueSearchCondition.page(request)
val counts = countIssueGroupByRepository(
IssueSearchCondition().copy(state = condition.state), Map.empty, true, userRepos: _*)
IssueSearchCondition().copy(state = condition.state), true, userRepos: _*)
dashboard.html.pulls(
pulls.html.listparts(
searchIssue(condition, filterUser, true, (page - 1) * PullRequestLimit, PullRequestLimit, allRepos: _*),
dashboard.html.pullslist(
searchIssue(condition, true, (page - 1) * PullRequestLimit, PullRequestLimit, allRepos: _*),
page,
countIssue(condition.copy(state = "open" ), filterUser, true, allRepos: _*),
countIssue(condition.copy(state = "closed"), filterUser, true, allRepos: _*),
countIssue(condition.copy(state = "open" ), true, allRepos: _*),
countIssue(condition.copy(state = "closed"), true, allRepos: _*),
condition,
None,
false),
getPullRequestCountGroupByUser(condition.state == "closed", None, None),
getAllPullRequestCountGroupByUser(condition.state == "closed", userName),
userRepos.map { case (userName, repoName) =>
(userName, repoName, counts.find { x => x._1 == userName && x._2 == repoName }.map(_._3).getOrElse(0))
}.sortBy(_._3).reverse,

View File

@@ -21,7 +21,6 @@ trait IssuesControllerBase extends ControllerBase {
case class IssueCreateForm(title: String, content: 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 IssueStateForm(issueId: Int, content: Option[String])
@@ -33,10 +32,12 @@ trait IssuesControllerBase extends ControllerBase {
"labelNames" -> trim(optional(text()))
)(IssueCreateForm.apply)
val issueTitleEditForm = mapping(
"title" -> trim(label("Title", text(required)))
)(x => x)
val issueEditForm = mapping(
"title" -> trim(label("Title", text(required))),
"content" -> trim(optional(text()))
)(IssueEditForm.apply)
"content" -> trim(optional(text()))
)(x => x)
val commentForm = mapping(
"issueId" -> label("Issue Id", number()),
@@ -48,16 +49,8 @@ trait IssuesControllerBase extends ControllerBase {
"content" -> trim(optional(text()))
)(IssueStateForm.apply)
get("/:owner/:repository/issues")(referrersOnly {
searchIssues("all", _)
})
get("/:owner/:repository/issues/assigned/:userName")(referrersOnly {
searchIssues("assigned", _)
})
get("/:owner/:repository/issues/created_by/:userName")(referrersOnly {
searchIssues("created_by", _)
get("/:owner/:repository/issues")(referrersOnly { repository =>
searchIssues(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) =>
getIssue(owner, name, params("id")).map { issue =>
if(isEditable(owner, name, issue.openedUserName)){
// 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
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}")
} else Unauthorized
@@ -181,7 +189,7 @@ trait IssuesControllerBase extends ControllerBase {
if(isEditable(x.userName, x.repositoryName, x.openedUserName)){
params.get("dataType") collect {
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 {
contentType = formats("json")
org.json4s.jackson.Serialization.write(
@@ -235,15 +243,17 @@ trait IssuesControllerBase extends ControllerBase {
milestoneId("milestoneId").map { milestoneId =>
getMilestonesWithIssueCount(repository.owner, repository.name)
.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 Ok()
})
post("/:owner/:repository/issues/batchedit/state")(collaboratorsOnly { repository =>
defining(params.get("value")){ action =>
executeBatch(repository) {
handleComment(_, None, repository)( _ => action)
action match {
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) = {
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) = {
@@ -319,15 +332,15 @@ trait IssuesControllerBase extends ControllerBase {
val (action, recordActivity) =
getAction(issue)
.collect {
case "close" => true -> (Some("close") ->
Some(if(issue.isPullRequest) recordClosePullRequestActivity _ else recordCloseIssueActivity _))
case "reopen" => false -> (Some("reopen") ->
Some(recordReopenIssueActivity _))
}
case "close" if(!issue.closed) => true ->
(Some("close") -> Some(if(issue.isPullRequest) recordClosePullRequestActivity _ else recordCloseIssueActivity _))
case "reopen" if(issue.closed) => false ->
(Some("reopen") -> Some(recordReopenIssueActivity _))
}
.map { case (closed, t) =>
updateClosed(owner, name, issueId, closed)
t
}
updateClosed(owner, name, issueId, closed)
t
}
.getOrElse(None -> None)
val commentId = content
@@ -337,7 +350,7 @@ trait IssuesControllerBase extends ControllerBase {
case (content, action) => createComment(owner, name, userName, issueId, content, action)
}
// record activity
// record comment activity if comment is entered
content foreach {
(if(issue.isPullRequest) recordCommentPullRequestActivity _ else recordCommentIssueActivity _)
(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) =>
val filterUser = Map(filter -> params.getOrElse("userName", ""))
val page = IssueSearchCondition.page(request)
val sessionKey = Keys.Session.Issues(owner, repoName)
@@ -383,19 +395,15 @@ trait IssuesControllerBase extends ControllerBase {
)
issues.html.list(
searchIssue(condition, filterUser, false, (page - 1) * IssueLimit, IssueLimit, owner -> repoName),
"issues",
searchIssue(condition, false, (page - 1) * IssueLimit, IssueLimit, owner -> repoName),
page,
(getCollaborators(owner, repoName) :+ owner).sorted,
getMilestones(owner, repoName),
getLabels(owner, repoName),
countIssue(condition.copy(state = "open"), filterUser, false, owner -> repoName),
countIssue(condition.copy(state = "closed"), filterUser, 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),
countIssue(condition.copy(state = "open" ), false, owner -> repoName),
countIssue(condition.copy(state = "closed"), false, owner -> repoName),
condition,
filter,
repository,
hasWritePermission(owner, repoName, context.loginAccount))
}

View File

@@ -2,51 +2,67 @@ package app
import jp.sf.amateras.scalatra.forms._
import service._
import util.CollaboratorsAuthenticator
import util.{ReferrerAuthenticator, CollaboratorsAuthenticator}
import util.Implicits._
import org.scalatra.i18n.Messages
import org.scalatra.Ok
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 {
self: LabelsService with RepositoryService with CollaboratorsAuthenticator =>
self: LabelsService with IssuesService with RepositoryService
with ReferrerAuthenticator with CollaboratorsAuthenticator =>
case class LabelForm(labelName: String, color: String)
val newForm = mapping(
"newLabelName" -> trim(label("Label name", text(required, labelName, maxlength(100)))),
"newColor" -> trim(label("Color", text(required, color)))
val labelForm = mapping(
"labelName" -> trim(label("Label name", text(required, labelName, maxlength(100)))),
"labelColor" -> trim(label("Color", text(required, color)))
)(LabelForm.apply)
val editForm = mapping(
"editLabelName" -> trim(label("Label name", text(required, labelName, maxlength(100)))),
"editColor" -> trim(label("Color", text(required, color)))
)(LabelForm.apply)
post("/:owner/:repository/issues/label/new", newForm)(collaboratorsOnly { (form, repository) =>
createLabel(repository.owner, repository.name, form.labelName, form.color.substring(1))
redirect(s"/${repository.owner}/${repository.name}/issues")
get("/:owner/:repository/issues/labels")(referrersOnly { repository =>
issues.labels.html.list(
getLabels(repository.owner, repository.name),
countIssueGroupByLabels(repository.owner, repository.name, IssuesService.IssueSearchCondition()),
repository,
hasWritePermission(repository.owner, repository.name, context.loginAccount))
})
ajaxGet("/:owner/:repository/issues/label/edit")(collaboratorsOnly { repository =>
issues.labels.html.editlist(getLabels(repository.owner, repository.name), repository)
ajaxGet("/:owner/:repository/issues/labels/new")(collaboratorsOnly { 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()),
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 =>
issues.labels.html.edit(Some(label), repository)
} 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))
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()),
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)
issues.labels.html.editlist(getLabels(repository.owner, repository.name), repository)
Ok()
})
/**

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -43,14 +43,13 @@ trait IssuesService {
* Returns the count of the search result against issues.
*
* @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 repos Tuple of the repository owner and the repository name
* @return the count of the search result
*/
def countIssue(condition: IssueSearchCondition, filterUser: Map[String, String], onlyPullRequest: Boolean,
repos: (String, String)*)(implicit s: Session): Int =
Query(searchIssueQuery(repos, condition, filterUser, onlyPullRequest).length).first
def countIssue(condition: IssueSearchCondition, onlyPullRequest: Boolean, repos: (String, String)*)
(implicit s: Session): Int =
Query(searchIssueQuery(repos, condition, onlyPullRequest).length).first
/**
* Returns the Map which contains issue count for each labels.
@@ -58,13 +57,12 @@ trait IssuesService {
* @param owner the repository owner
* @param repository the repository name
* @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)
*/
def countIssueGroupByLabels(owner: String, repository: String, condition: IssueSearchCondition,
filterUser: Map[String, String])(implicit s: Session): Map[String, Int] = {
def countIssueGroupByLabels(owner: String, repository: String,
condition: IssueSearchCondition)(implicit s: Session): Map[String, Int] = {
searchIssueQuery(Seq(owner -> repository), condition.copy(labels = Set.empty), filterUser, false)
searchIssueQuery(Seq(owner -> repository), condition.copy(labels = Set.empty), false)
.innerJoin(IssueLabels).on { (t1, t2) =>
t1.byIssue(t2.userName, t2.repositoryName, t2.issueId)
}
@@ -84,15 +82,14 @@ trait IssuesService {
* 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 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
* @return list which contains issue count for each repository
*/
def countIssueGroupByRepository(
condition: IssueSearchCondition, filterUser: Map[String, String], onlyPullRequest: Boolean,
condition: IssueSearchCondition, onlyPullRequest: Boolean,
repos: (String, String)*)(implicit s: Session): List[(String, String, Int)] = {
searchIssueQuery(repos, condition.copy(repo = None), filterUser, onlyPullRequest)
searchIssueQuery(repos, condition.copy(repo = None), onlyPullRequest)
.groupBy { t =>
t.userName -> t.repositoryName
}
@@ -107,19 +104,18 @@ trait IssuesService {
* Returns the search result against issues.
*
* @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 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 limit the limit for pagination
* @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)
*/
def searchIssue(condition: IssueSearchCondition, filterUser: Map[String, String], onlyPullRequest: Boolean,
def searchIssue(condition: IssueSearchCondition, pullRequest: Boolean,
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
searchIssueQuery(repos, condition, filterUser, onlyPullRequest)
searchIssueQuery(repos, condition, pullRequest)
.innerJoin(IssueOutline).on { (t1, t2) => t1.byIssue(t2.userName, t2.repositoryName, t2.issueId) }
.sortBy { case (t1, t2) =>
(condition.sort match {
@@ -136,21 +132,23 @@ trait IssuesService {
.drop(offset).take(limit)
.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) }
.map { case (((t1, t2), t3), t4) =>
(t1, t2.commentCount, t4.labelId.?, t4.labelName.?, t4.color.?)
.leftJoin (Milestones) .on { case ((((t1, t2), t3), t4), t5) => t1.byMilestone(t5.userName, t5.repositoryName, t5.milestoneId) }
.map { case ((((t1, t2), t3), t4), t5) =>
(t1, t2.commentCount, t4.labelId.?, t4.labelName.?, t4.color.?, t5.title.?)
}
.list
.splitWith { (c1, c2) =>
c1._1.userName == c2._1.userName &&
c1._1.userName == c2._1.userName &&
c1._1.repositoryName == c2._1.repositoryName &&
c1._1.issueId == c2._1.issueId
c1._1.issueId == c2._1.issueId
}
.map { issues => issues.head match {
case (issue, commentCount, _,_,_) =>
(issue,
case (issue, commentCount, _, _, _, milestone) =>
IssueInfo(issue,
issues.flatMap { t => t._3.map (
Label(issue.userName, issue.repositoryName, _, t._4.get, t._5.get)
)} toList,
milestone,
commentCount)
}} toList
}
@@ -159,7 +157,7 @@ trait IssuesService {
* Assembles query for conditional issue searching.
*/
private def searchIssueQuery(repos: Seq[(String, String)], condition: IssueSearchCondition,
filterUser: Map[String, String], onlyPullRequest: Boolean)(implicit s: Session) =
pullRequest: Boolean)(implicit s: Session) =
Issues filter { t1 =>
condition.repo
.map { _.split('/') match { case array => Seq(array(0) -> array(1)) } }
@@ -169,10 +167,9 @@ trait IssuesService {
(t1.closed === (condition.state == "closed").bind) &&
(t1.milestoneId === condition.milestoneId.get.get.bind, condition.milestoneId.flatten.isDefined) &&
(t1.milestoneId.? isEmpty, condition.milestoneId == Some(None)) &&
(t1.assignedUserName === filterUser("assigned").bind, filterUser.get("assigned").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.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) &&
(IssueLabels filter { t2 =>
(t2.byIssue(t1.userName, t1.repositoryName, t1.issueId)) &&
(t2.labelId in
@@ -337,11 +334,20 @@ object IssuesService {
case class IssueSearchCondition(
labels: Set[String] = Set.empty,
milestoneId: Option[Option[Int]] = None,
author: Option[String] = None,
assigned: Option[String] = None,
repo: Option[String] = None,
state: String = "open",
sort: String = "created",
direction: String = "desc"){
def isEmpty: Boolean = {
labels.isEmpty && milestoneId.isEmpty && author.isEmpty && assigned.isEmpty &&
state == "open" && sort == "created" && direction == "desc"
}
def nonEmpty: Boolean = !isEmpty
def toURL: String =
"?" + List(
if(labels.isEmpty) None else Some("labels=" + urlEncode(labels.mkString(","))),
@@ -349,6 +355,8 @@ object IssuesService {
case Some(x) => x.toString
case None => "none"
})},
author .map(x => "author=" + urlEncode(x)),
assigned.map(x => "assigned=" + urlEncode(x)),
repo.map("for=" + urlEncode(_)),
Some("state=" + urlEncode(state)),
Some("sort=" + urlEncode(sort)),
@@ -370,6 +378,8 @@ object IssuesService {
case "none" => None
case x => x.toIntOpt
},
param(request, "author"),
param(request, "assigned"),
param(request, "for"),
param(request, "state", Seq("open", "closed")).getOrElse("open"),
param(request, "sort", Seq("created", "comments", "updated")).getOrElse("created"),
@@ -383,4 +393,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] =
Labels.filter(_.byPrimaryKey(owner, repository, labelId)).firstOption
def createLabel(owner: String, repository: String, labelName: String, color: String)(implicit s: Session): Unit =
Labels insert Label(
def createLabel(owner: String, repository: String, labelName: String, color: String)(implicit s: Session): Int =
Labels returning Labels.map(_.labelId) += Label(
userName = owner,
repositoryName = repository,
labelName = labelName,

View File

@@ -36,6 +36,24 @@ trait PullRequestService { self: IssuesService =>
.list
.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,
originBranch: String, requestUserName: String, requestRepositoryName: String, requestBranch: String,
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)
}.list
}

View File

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

View File

@@ -134,8 +134,8 @@ class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl:
// Retrieve all issue count in the repository
val issueCount =
countIssue(IssueSearchCondition(state = "open"), Map.empty, false, owner -> repository) +
countIssue(IssueSearchCondition(state = "closed"), Map.empty, false, owner -> repository)
countIssue(IssueSearchCondition(state = "open"), false, owner -> repository) +
countIssue(IssueSearchCondition(state = "closed"), false, owner -> repository)
// Extract new commit and apply issue comment
val defaultBranch = getRepository(owner, repository, baseUrl).get.repository.defaultBranch

View File

@@ -7,7 +7,7 @@ import play.twirl.api.Html
import service.{AccountService, RepositoryService, SystemSettingsService}
import model.{Account, Session}
import util.{JGitUtil, Keys}
import plugin.{Fragment, PluginConnectionHolder, Redirect}
import plugin.{RawData, Fragment, PluginConnectionHolder, Redirect}
import service.RepositoryService.RepositoryInfo
import plugin.Security._
@@ -21,7 +21,7 @@ class PluginActionInvokeFilter extends Filter with SystemSettingsService with Re
(req, res) match {
case (request: HttpServletRequest, response: HttpServletResponse) => {
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)){
chain.doFilter(req, res)
}
@@ -46,13 +46,7 @@ class PluginActionInvokeFilter extends Filter with SystemSettingsService with Re
} finally {
PluginConnectionHolder.threadLocal.remove()
}
result match {
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)
}
processActionResult(result, request, response, context)
} else {
// TODO NotFound or Error?
}
@@ -81,13 +75,7 @@ class PluginActionInvokeFilter extends Filter with SystemSettingsService with Re
} finally {
PluginConnectionHolder.threadLocal.remove()
}
result match {
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)
}
processActionResult(result, request, response, context)
} else {
// TODO NotFound or Error?
}
@@ -97,6 +85,24 @@ class PluginActionInvokeFilter extends Filter with SystemSettingsService with Re
} 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
*/
@@ -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 = {
response.setContentType("text/html; charset=UTF-8")
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)
}
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 = {
import org.json4s._
import org.json4s.jackson.Serialization

View File

@@ -4,6 +4,7 @@ import org.eclipse.jgit.api.Git
import util.Directory._
import util.StringUtil._
import util.ControlUtil._
import scala.annotation.tailrec
import scala.collection.JavaConverters._
import org.eclipse.jgit.lib._
import org.eclipse.jgit.revwalk._
@@ -190,38 +191,23 @@ object JGitUtil {
* @return HTML of the file list
*/
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 =>
val objectId = git.getRepository.resolve(revision)
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)
if(path != "."){
treeWalk.setRecursive(true)
treeWalk.setFilter(new TreeFilter(){
treeWalk
} else {
val treeWalk = TreeWalk.forPath(git.getRepository, path, revCommit.getTree)
treeWalk.enterSubtree()
treeWalk
}
var stopRecursive = false
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
})
}
using(treeWalk) { treeWalk =>
while (treeWalk.next()) {
// submodule
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 = 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))
}
}
}

View File

@@ -88,9 +88,10 @@ class GitBucketHtmlSerializer(
Map[String, VerbatimSerializer](VerbatimSerializer.DEFAULT -> new GitBucketVerbatimSerializer).asJava
) with LinkConverter with RequestCache {
override protected def printImageTag(imageNode: SuperNode, url: String): Unit =
printer.print("<a target=\"_blank\" href=\"").print(fixUrl(url)).print("\">")
.print("<img src=\"").print(fixUrl(url)).print("\" alt=\"").printEncoded(printChildrenToString(imageNode)).print("\"/></a>")
override protected def printImageTag(imageNode: SuperNode, url: String): Unit = {
printer.print("<a target=\"_blank\" href=\"").print(fixUrl(url, true)).print("\">")
.print("<img src=\"").print(fixUrl(url, true)).print("\" alt=\"").printEncoded(printChildrenToString(imageNode)).print("\"/></a>")
}
override protected def printLink(rendering: LinkRenderer.Rendering): Unit = {
printer.print('<').print('a')
@@ -101,9 +102,21 @@ class GitBucketHtmlSerializer(
printer.print('>').print(rendering.text).print("</a>")
}
private def fixUrl(url: String): String = {
if(!enableWikiLink || url.startsWith("http://") || url.startsWith("https://") || url.startsWith("#")){
url
private def fixUrl(url: String, isImage: Boolean = false): String = {
if(!enableWikiLink){
if(url.startsWith("http://") || url.startsWith("https://") || url.startsWith("#") || url.startsWith("/")){
url
} else 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 {
repository.httpUrl.replaceFirst("/git/", "/").stripSuffix(".git") + "/wiki/_blob/" + url
}

View File

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

View File

@@ -0,0 +1,184 @@
@(issues: List[service.IssuesService.IssueInfo],
page: Int,
openCount: Int,
closedCount: Int,
condition: service.IssuesService.IssueSearchCondition,
collaborators: List[String] = Nil,
milestones: List[model.Milestone] = Nil,
labels: List[model.Label] = Nil,
repository: Option[service.RepositoryService.RepositoryInfo] = None,
hasWritePermission: Boolean = false)(implicit context: app.Context)
@import context._
@import view.helpers._
@import service.IssuesService.IssueInfo
<div class="span9">
@if(condition.labels.nonEmpty || condition.milestoneId.isDefined){
<a href="@condition.copy(labels = Set.empty, milestoneId = None).toURL" id="clear-filter">
<i class="icon-remove-circle"></i> Clear milestone and label filters
</a>
}
@if(condition.repo.isDefined){
<a href="@condition.copy(repo = None).toURL" id="clear-filter">
<i class="icon-remove-circle"></i> Clear filter on @condition.repo
</a>
}
<div class="pull-right">
@helper.html.paginator(page, (if(condition.state == "open") openCount else closedCount), service.IssuesService.IssueLimit, 7, condition.toURL)
</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 issues 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){
<a href="@url(repository.get)/issues/new">Create a new issue.</a>
}
}
</td>
</tr>
} else {
@if(hasWritePermission){
<tr>
<td style="background-color: #eee;">
<div class="btn-group">
<button class="btn btn-mini strong" id="state">@{if(condition.state == "open") "Close" else "Reopen"}</button>
</div>
@helper.html.dropdown("Label") {
@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.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>
}
}
</td>
</tr>
}
}
@issues.map { case IssueInfo(issue, labels, milestone, 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>

View File

@@ -1,4 +1,4 @@
@(issues: List[(model.Issue, List[model.Label], Int)],
@(issues: List[service.IssuesService.IssueInfo],
page: Int,
openCount: Int,
closedCount: Int,
@@ -7,6 +7,7 @@
hasWritePermission: Boolean)(implicit context: app.Context)
@import context._
@import view.helpers._
@import service.IssuesService.IssueInfo
<div class="span9">
@repository.map { repository =>
@if(hasWritePermission){
@@ -71,7 +72,7 @@
</td>
</tr>
}
@issues.map { case (issue, labels, commentCount) =>
@issues.map { case IssueInfo(issue, labels, milestone, commentCount) =>
<tr>
<td>
<img src="@assets/common/images/pullreq-@(if(issue.closed) "closed" else "open").png"/>

View File

@@ -9,9 +9,12 @@
@if(showIndex){
<div>
<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>
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>
<ul id="commit-file-list" style="display: none;">
@diffs.zipWithIndex.map { case (diff, i) =>
@@ -38,7 +41,7 @@
<a name="diff-@i"></a>
<table class="table table-bordered">
<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){
@diff.oldPath -> @diff.newPath
@if(newCommitId.isDefined){
@@ -66,7 +69,7 @@
</th>
</tr>
<tr>
<td>
<td style="padding: 0;">
@if(diff.newContent != None || diff.oldContent != None){
<div id="diffText-@i"></div>
<textarea id="newText-@i" style="display: none;">@diff.newContent.getOrElse("")</textarea>
@@ -94,10 +97,25 @@ $(function(){
});
}
@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');
// Render diffs as unified mode initially
renderDiffs(1);
$('#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)
<div class="btn-group"@if(style.nonEmpty){ style="@style"}>
<button class="btn dropdown-toggle@if(mini){ btn-mini} else { btn-small}" data-toggle="dropdown">
@(value : String = "",
prefix: String = "",
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){
<i class="icon-cog"></i>
} else {

View File

@@ -5,6 +5,7 @@
@import context._
@import view.helpers._
@if(loginAccount.isDefined){
<hr/><br/>
<form method="POST" validate="true">
<div class="issue-avatar-image">@avatar(loginAccount.get.userName, 48)</div>
<div class="box issue-comment-box">

View File

@@ -5,20 +5,36 @@
pullreq: Option[model.PullRequest] = None)(implicit context: app.Context)
@import context._
@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 on @datetime(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)
</div>
</div>
@comments.map { comment =>
@if(comment.action != "close" && comment.action != "reopen" && comment.action != "delete_branch"){
<div class="issue-avatar-image">@avatar(comment.commentedUserName, 48)</div>
<div class="box issue-comment-box" id="comment-@comment.commentId">
<div class="box-header-small">
<i class="icon-comment"></i>
@user(comment.commentedUserName, styleClass="username strong")
@if(comment.action == "comment"){
commented
} else {
@if(pullreq.isEmpty){ referenced the issue } else { referenced the pull request }
}
<span class="muted">
@if(comment.action == "comment"){
commented
} else {
@if(pullreq.isEmpty){ referenced the issue } else { referenced the pull request }
}
on @datetime(comment.registeredDate)
</span>
<span class="pull-right">
@datetime(comment.registeredDate)
@if(comment.action != "commit" && comment.action != "merge" && comment.action != "refer" &&
(hasWritePermission || loginAccount.map(_.userName == comment.commentedUserName).getOrElse(false))){
<a href="#" data-comment-id="@comment.commentId"><i class="icon-pencil"></i></a>&nbsp;
@@ -86,13 +102,22 @@
<script>
$(function(){
$('i.icon-pencil').click(function(){
var id = $(this).closest('a').data('comment-id');
$.get('@url(repository)/issue_comments/_data/' + id,
var id = $(this).closest('a').data('comment-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'
},
function(data){
$('#commentContent-' + id).empty().html(data);
$content.empty().html(data);
});
return false;
});

View File

@@ -7,7 +7,8 @@
@import view.helpers._
@html.main(s"New Issue - ${repository.owner}/${repository.name}", Some(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">
<div class="row-fluid">
<div class="span9">
@@ -32,7 +33,7 @@
@if(hasWritePermission){
<input type="hidden" name="milestoneId" value=""/>
@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 =>
<li>
<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;">
@milestone.dueDate.map { 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 {
<span class="muted">Due in @date(dueDate)</span>
<span class="muted">Due by @date(dueDate)</span>
}
}.getOrElse {
<span class="muted">No due date</span>
@@ -65,7 +66,7 @@
</div>
<div class="span3">
@if(hasWritePermission){
<span class="strong">Add Labels</span>
<span class="strong">Labels</span>
<div>
<div id="label-list">
<ul class="label-list nav nav-pills nav-stacked">
@@ -112,7 +113,7 @@ $(function(){
if(milestoneId == ''){
$('#label-milestone').text('No milestone');
} 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');
}
$('input[name=milestoneId]').val(milestoneId);

View File

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

View File

@@ -10,31 +10,86 @@
@import view.helpers._
@html.main(s"${issue.title} - Issue #${issue.issueId} - ${repository.owner}/${repository.name}", Some(repository)){
@html.menu("issues", repository){
@tab("issues", false, repository)
<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-right">Issue #@issue.issueId</li>
</ul>
<div class="row-fluid">
<div class="span10">
@issuedetail(issue, comments, collaborators, milestones, hasWritePermission, repository)
@commentlist(issue, comments, hasWritePermission, repository)
@commentform(issue, true, hasWritePermission, repository)
</div>
<div class="span2">
<li class="pull-left">
<h1>
<span class="show-title">
<span id="show-title">@issue.title</span>
<span class="muted">#@issue.issueId</span>
</span>
<span class="edit-title" style="display: none;">
<span id="error-edit-title" class="error"></span>
<input type="text" class="span9" id="edit-title" value="@issue.title"/>
</span>
</h1>
@if(issue.closed) {
<span class="label label-important issue-status">Closed</span>
} else {
<span class="label label-success issue-status">Open</span>
}
<div class="small" style="text-align: center;">
@defining(comments.filter( _.action.contains("comment") ).size){ count =>
<span class="strong">@count</span> @plural(count, "comment")
<span class="muted">
@user(issue.openedUserName, styleClass="username strong") opened this issue on @datetime(issue.registeredDate) - @defining(
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>
<hr/>
@issues.html.labels(issue, issueLabels, labels, hasWritePermission, repository)
<div class="edit-title" style="display: none;">
<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>
}
}
<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,169 @@
@(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() {
<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() {
<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])
@if(issueLabels.isEmpty){
<li><span class="muted small">None yet</span></li>
}
@issueLabels.map { label =>
<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)
@import context._
@import view.helpers._
@defining((if(label.isEmpty) ("new", 190, 4) else ("edit", 180, 8))){ case (mode, width, margin) =>
<div id="@(mode)LabelArea">
<form method="POST" id="edit-label-form" validate="true" style="margin-bottom: 8px;"
action="@url(repository)/issues/label/@{if(mode == "new") "new" else label.get.labelId + "/edit"}">
<span id="error-@(mode)LabelName" class="error"></span>
<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"}/>
<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;">
@defining(label.map(_.labelId).getOrElse("new")){ labelId =>
<div id="edit-label-area-@labelId">
<form style="margin-bottom: 0px;">
<input type="text" id="labelName-@labelId" style="width: 300px; margin-bottom: 0px;" value="@label.map(_.labelName)"@if(labelId == "new"){ placeholder="New label name"}/>
<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" class="span3" id="labelColor-@labelId" value="#@label.map(_.color)" readonly style="width: 100px;">
<span class="add-on"><i style="background-color: #@label.map(_.color).getOrElse("888888");"></i></span>
</div>
<input type="submit" class="btn" style="margin-left: @(margin)px; margin-bottom: 0px;" value="@if(mode == "new"){Create} else {Save}"/>
@if(mode == "edit"){
<input type="hidden" name="editLabelId" value="@label.map(_.labelId)"/>
}
<script>
$('div#label-color-@labelId').colorpicker();
</script>
<span id="label-error-@labelId" class="error" style="padding-left: 40px;"></span>
<span class="pull-right">
<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>
<script>
$(function(){
@if(mode == "new"){
$('#newColor').colorpicker();
</div>
<script>
$(function(){
$('#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 {
$('#editColor').colorpicker();
$('#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;
});
$('#label-@labelId').show();
}
});
</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,66 @@
@(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)
<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($('#new-label-area').size() != 0){
$('#new-label-table').hide();
$('#new-label-area').remove();
} 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,
collaborators: List[String],
milestones: List[model.Milestone],
labels: List[model.Label],
openCount: Int,
closedCount: Int,
allCount: Int,
assignedCount: Option[Int],
createdByCount: Option[Int],
labelCounts: Map[String, Int],
condition: service.IssuesService.IssueSearchCondition,
filter: String,
repository: service.RepositoryService.RepositoryInfo,
hasWritePermission: Boolean)(implicit context: app.Context)
@import context._
@import view.helpers._
@html.main(s"Issues - ${repository.owner}/${repository.name}", Some(repository)){
@html.menu("issues", repository){
@tab("issues", false, repository)
<div class="row-fluid">
<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>
@html.main((if(target == "issues") "Issues" else "Pull requests") + s" - ${repository.owner}/${repository.name}", Some(repository)){
@html.menu(target, repository){
@tab(target, true, repository)
@listparts(target, issues, page, openCount, closedCount, condition, collaborators, milestones, labels, Some(repository), hasWritePermission)
@if(hasWritePermission){
<form id="batcheditForm" method="POST">
<input type="hidden" name="value"/>
<input type="hidden" name="checked"/>
<input type="hidden" name="from" value="@target"/>
</form>
}
}
@@ -145,20 +27,34 @@
@if(hasWritePermission){
<script>
$(function(){
$('#manageLabel').click(function(){
if($(this).data('toggle-state')){
location.href = '@url(repository)/issues';
} else {
$(this).data('toggle-state', 'on');
$.get('@url(repository)/issues/label/edit', function(data){
$('#label-list').parent().empty().html(data);
});
$('a.header-link').mouseover(function(e){
var target = e.target;
if(e.target.tagName != 'A'){
target = e.target.parentElement;
}
$(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 button').prop('disabled',
!$('.table-issues input[type=checkbox]').filter(':checked').length);
if($('.table-issues input[type=checkbox]').filter(':checked').length == 0){
$('#table-issues-control').show();
$('#table-issues-batchedit').hide();
} else {
$('#table-issues-control').hide();
$('#table-issues-batchedit').show();
}
}).filter(':first').change();
var submitBatchEdit = function(action, value) {
@@ -170,8 +66,8 @@ $(function(){
form.submit();
};
$('#state').click(function(){
submitBatchEdit('@url(repository)/issues/batchedit/state', $(this).text().toLowerCase());
$('a.toggle-state').click(function(){
submitBatchEdit('@url(repository)/issues/batchedit/state', $(this).data('id'));
});
$('a.toggle-label').click(function(){
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,
openCount: Int,
closedCount: Int,
@@ -10,175 +11,207 @@
hasWritePermission: Boolean = false)(implicit context: app.Context)
@import context._
@import view.helpers._
<div class="span9">
@if(condition.labels.nonEmpty || condition.milestoneId.isDefined){
<a href="@condition.copy(labels = Set.empty, milestoneId = None).toURL" id="clear-filter">
<i class="icon-remove-circle"></i> Clear milestone and label filters
@import service.IssuesService.IssueInfo
@if(condition.nonEmpty){
<div>
<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>
}
@if(condition.repo.isDefined){
<a href="@condition.copy(repo = None).toURL" id="clear-filter">
<i class="icon-remove-circle"></i> Clear filter on @condition.repo
</a>
}
<div class="pull-right">
@helper.html.paginator(page, (if(condition.state == "open") openCount else closedCount), service.IssuesService.IssueLimit, 7, condition.toURL)
</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 issues 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){
<a href="@url(repository.get)/issues/new">Create a new issue.</a>
}
}
</td>
</tr>
} else {
@if(hasWritePermission){
<tr>
<td style="background-color: #eee;">
<div class="btn-group">
<button class="btn btn-mini strong" id="state">@{if(condition.state == "open") "Close" else "Reopen"}</button>
</div>
@helper.html.dropdown("Label") {
@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>
</span>
<div class="pull-right" id="table-issues-control">
@helper.html.dropdown("Author", flat = true) {
@collaborators.map { collaborator =>
<li>
<a href="@condition.copy(author = (if(condition.author == Some(collaborator)) None else Some(collaborator))).toURL">
@helper.html.checkicon(condition.author == Some(collaborator))
@avatar(collaborator, 20) @collaborator
</a>
</li>
}
}
@helper.html.dropdown("Label", flat = true) {
@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">
@helper.html.checkicon(condition.labels.contains(label.labelName))
<span style="background-color: #@label.color;" class="label-color">&nbsp;&nbsp;</span>
@label.labelName
</a>
</li>
}
}
@helper.html.dropdown("Milestone", flat = true) {
<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
</a>
</li>
}
}
@helper.html.dropdown("Assignee", flat = true) {
@collaborators.map { collaborator =>
<li>
<a href="@condition.copy(assigned = Some(collaborator)).toURL">
@helper.html.checkicon(condition.assigned == Some(collaborator))
@avatar(collaborator, 20) @collaborator
</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>
}
}
@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>
@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 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

@@ -2,12 +2,18 @@
@import context._
@import view.helpers._
@html.main(s"Milestones - ${repository.owner}/${repository.name}"){
@html.menu("milestones", repository){
@issues.html.tab("milestones", false, repository)
@html.menu("issues", 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">
<fieldset>
<label for="title"><string>Title</string></label>
<input type="text" id="title" name="title" style="width: 400px;" value="@milestone.map(_.title)"/>
<input type="text" id="title" name="title" style="width: 500px;" value="@milestone.map(_.title)" placeholder="Title"/>
<span id="error-title" class="error"></span>
</fieldset>
<fieldset>

View File

@@ -6,94 +6,93 @@
@import view.helpers._
@html.main(s"Milestones - ${repository.owner}/${repository.name}"){
@html.menu("issues", repository){
@issues.html.tab("milestones", false, repository)
<div class="row-fluid">
<div class="span3">
<ul class="nav nav-pills nav-stacked">
<li@if(state == "open"){ class="active"}>
<a href="?state=open">
<span class="count-right">@milestones.filter(_._1.closedDate.isEmpty).size</span>
Open Milestones
@issues.html.tab("milestones", hasWritePermission, repository)
<table class="table table-bordered table-hover table-issues">
<tr>
<th style="background-color: #eee;">
<span class="small">
<a class="button-link@if(state == "open"){ selected}" href="?state=open">
<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>
</li>
<li@if(state == "closed"){ class="active"}>
<a href="?state=closed">
<span class="count-right">@milestones.filter(_._1.closedDate.isDefined).size</span>
Closed Milestones
</a>
</li>
</ul>
@if(hasWritePermission){
<hr>
<a href="@url(repository)/issues/milestones/new" class="btn btn-block">Create a new milestone</a>
}
</div>
<div class="span9">
<table class="table table-bordered table-hover">
@defining(milestones.filter { case (milestone, _, _) =>
milestone.closedDate.map(_ => state == "closed").getOrElse(state == "open")
}){ milestones =>
@milestones.map { case (milestone, openCount, closedCount) =>
<tr>
<td>
<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>
</span>
</th>
</tr>
@defining(milestones.filter { case (milestone, _, _) =>
milestone.closedDate.map(_ => state == "closed").getOrElse(state == "open")
}){ milestones =>
@milestones.map { case (milestone, openCount, closedCount) =>
<tr>
<td style="padding-top: 15px; padding-bottom: 15px;">
<div class="milestone row-fluid">
<div class="span4">
<a href="@url(repository)/issues?milestone=@milestone.milestoneId&state=open" class="milestone-title">@milestone.title</a>
<div style="margin-top: 6px">
@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 by @date(dueDate)</span>
} else {
<span class="muted">Due by @date(dueDate)</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 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>
@if(milestone.description.isDefined){
<div class="milestone-description">
@markdown(milestone.description.get, repository, false, false)
</div>
}
</td>
</tr>
</div>
</div>
@if(milestone.description.isDefined){
<div class="milestone-description">
@markdown(milestone.description.get, repository, false, false)
</div>
}
@if(milestones.isEmpty){
<tr>
<td style="padding: 20px; background-color: #eee; text-align: center;">
No milestones to show.
@if(hasWritePermission){
<a href="@url(repository)/issues/milestones/new">Create a new milestone.</a>
}
</td>
</tr>
</td>
</tr>
}
@if(milestones.isEmpty){
<tr>
<td style="padding: 20px; background-color: #eee; text-align: center;">
No milestones to show.
@if(hasWritePermission){
<a href="@url(repository)/issues/milestones/new">Create a new milestone.</a>
}
}
</table>
</div>
</div>
</td>
</tr>
}
}
</table>
}
}
<script>

View File

@@ -1,15 +1,6 @@
@(total: Int, progress: Int, showPercentage: Boolean)
@(total: Int, progress: Int)
<div class="milestone-progress">
@if(progress > 0){
<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>

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 view.helpers._
<ul class="nav nav-tabs pull-left fill-width">
<li@if(active == "issues"){ class="active"}><a href="@url(repository)/issues">Browse Issues</a></li>
<li@if(active == "milestones"){ class="active"}><a href="@url(repository)/issues/milestones">Milestones</a></li>
<ul class="nav nav-pills-group pull-left fill-width">
<li class="@if(active == "issues" ){active} first"><a href="@url(repository)/issues">Issues</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){
<li class="pull-right">
<div class="btn-group">
@if(create){
<a class="btn btn-small btn-success" href="#" disabled="disabled">New Issue</a>
} else {
<a class="btn btn-small btn-success" href="@url(repository)/issues/new">New Issue</a>
@if(newButton){
@if(active == "issues"){
<a class="btn 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>
</li>

View File

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

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

View File

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

View File

@@ -84,9 +84,19 @@
<td>
@if(file.isDirectory){
@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 {
<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 {
<a href="@url(repository)/blob@{(encodeRefName(branch) :: pathList).mkString("/", "/", "/")}@file.name">@file.name</a>

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 view.helpers._
@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>
}
</ul>
<form method="POST" action="@url(repository)/settings/hooks/add" validate="true">
<form method="POST" validate="true">
<div>
<span class="error" id="error-url"></span>
</div>
<input type="text" name="url" id="url" style="width: 300px; margin-bottom: 0px;"/>
<input type="submit" class="btn" value="Add"/>
<a href="@url(repository)/settings/hooks/test" class="btn">Test Hook</a>
<input type="text" name="url" id="url" value="@enteredUrl" style="width: 300px; margin-bottom: 0px;"/>
<input type="submit" class="btn" formaction="@url(repository)/settings/hooks/add" value="Add"/>
<input type="submit" class="btn" formaction="@url(repository)/settings/hooks/test" value="Test Hook"/>
</form>
}
}

View File

@@ -119,6 +119,11 @@ div.container {
width: 920px;
}
div.container-wide {
padding-left: 10px;
padding-right: 10px;
}
div.pagination {
margin-top: 0px;
margin-bottom: 0px;
@@ -608,11 +613,85 @@ li.highlight {
background-color: #ffb;
}
span.simplified-path {
color: #888;
}
/****************************************************************************/
/* 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 */
/****************************************************************************/
.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 {
display: block;
font-size: large;
text-align: center;
padding: 8px;
@@ -625,7 +704,7 @@ table.table-issues {
a.issue-title {
color: #333;
font-weight: bold;
size: 110%;
font-size: 120%;
}
ul.label-list {
@@ -662,8 +741,7 @@ span.milestone-alert {
}
a.milestone-title {
font-size: 120%;
font-weight: bold;
font-size: 180%;
}
div.milestone-description {
@@ -671,13 +749,12 @@ div.milestone-description {
color: #666;
}
div.milestone-menu {
font-size: 80%;
a.milestone-title {
color: #333;
}
div.milestone-menu a {
margin-left: 8px;
font-weight: bold;
div.milestone-menu {
margin-top: 8px;
}
div.milestone-menu a.delete {
@@ -689,13 +766,13 @@ div#milestone-progress-area {
}
div#milestone-progress-area div.milestone-progress {
width: 150px;
width: 130px;
margin-bottom: -6px;
}
div.milestone-progress {
position: relative;
height: 20px;
height: 10px;
color: white;
margin-bottom: 4px;
font-weight: bold;
@@ -716,11 +793,6 @@ span.milestone-progress {
-moz-border-radius: 4px;
}
span.milestone-percentage {
position: absolute;
padding-left: 8px;
}
div.issue-header {
padding-left: 8px;
padding-right: 8px;
@@ -838,20 +910,24 @@ div.author-info div.committer {
/****************************************************************************/
/* Diff */
/****************************************************************************/
table.inlinediff {
table.diff {
font-size: 12px;
font-family: Monaco, Menlo, Consolas, "Courier New", monospace;
width: 100%;
}
table.inlinediff thead {
table.diff thead {
display: none;
}
td.insert, td.equal, td.delete {
table.inlinediff td.insert, table.inlinediff td.equal, table.inlinediff td.delete {
width: 100%;
}
td.insert, td.equal, td.delete, td.empty {
width: 50%;
}
/****************************************************************************/
/* 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: 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').click(function(){
this.select(0, this.value.length);
this.select(0, this.value.length);
});
// activate tooltip
@@ -17,7 +17,7 @@ $(function(){
});
$('.markdown-head').mouseleave(function(e){
var anchorLink = $(e.target).children('a.markdown-anchor-link');
if(anchorLink.data('active') != true){
if(anchorLink.data('active') !== true){
anchorLink.hide();
}
});
@@ -39,7 +39,7 @@ function displayErrors(data){
var i = 0;
$.each(data, function(key, value){
$('#error-' + key.split(".").join("_")).text(value);
if(i == 0){
if(i === 0){
$('#' + key).focus();
}
i++;
@@ -68,20 +68,26 @@ function displayErrors(data){
};
})(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
var oldText = document.getElementById(oldTextId).value;
if(oldText == ''){
var oldLines = [];
} else {
var oldLines = difflib.stringAsLines(oldText);
var oldLines = [];
if(oldText !== ''){
oldLines = difflib.stringAsLines(oldText);
}
var newText = document.getElementById(newTextId).value
if(newText == ''){
var newLines = [];
} else {
var newLines = difflib.stringAsLines(newText);
var newText = document.getElementById(newTextId).value;
var newLines = [];
if(newText !== ''){
newLines = difflib.stringAsLines(newText);
}
// create a SequenceMatcher instance that diffs the two sets of lines
@@ -100,10 +106,10 @@ function diffUsingJS(oldTextId, newTextId, outputId) {
newTextLines: newLines,
opcodes: opcodes,
contextSize: 4,
viewType: 1
viewType: viewType
}));
}
function jqSelectorEscape(val) {
return val.replace(/[!"#$%&'()*+,.\/:;<=>?@\[\\\]^`{|}~]/g, '\\$&');
}
}

View File

@@ -4,25 +4,25 @@ $(function(){
});
$.each($('input[formaction]'), function(i, input){
$(input).click(function(){
var form = $(input).parents('form')
$(form).attr('action', $(input).attr('formaction'))
var form = $(input).parents('form');
$(form).attr('action', $(input).attr('formaction'));
});
});
});
function validate(e){
var form = $(e.target);
$(form).find('[type=submit]').attr('disabled', 'disabled')
if(form.data('validated') == true){
$(form).find('[type=submit]').attr('disabled', 'disabled');
if(form.data('validated') === true){
return true;
}
$.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
$('.error').text('');
if($.isEmptyObject(data)){
form.data('validated', true);
form.submit();
@@ -39,4 +39,4 @@ function displayErrors(data){
$.each(data, function(key, value){
$('#error-' + key.split(".").join("_")).text(value);
});
}
}

View File

@@ -173,8 +173,13 @@ diffview = {
addCellsInline(node, b++, n++, baseTextLines, change);
}
} else {
b = addCells(node, b, be, baseTextLines, change);
n = addCells(node, n, ne, newTextLines, change);
var changeBase = change, changeNew = change;
if (change === "replace") {
if (b < be) changeBase = "delete";
if (n < ne) changeNew = "insert";
}
b = addCells(node, b, be, baseTextLines, changeBase);
n = addCells(node, n, ne, newTextLines, changeNew);
}
}