Jihwan Chun 2015-05-23
Added UI for webhook form.
@ab24aeb723050d454125c8678d485f8753a7ed42
app/assets/stylesheets/less/_page.less
--- app/assets/stylesheets/less/_page.less
+++ app/assets/stylesheets/less/_page.less
@@ -604,7 +604,7 @@
                 color: #2cb400;
             }
         }
-        
+
         .d2-program:hover {
             background-color:#5f5f5f;
             text-decoration:none;
@@ -2140,7 +2140,7 @@
                     &.over { color:#f36c22; }
                 }
             }
-            
+
             .progress-wrap {
                 overflow: hidden;
                 color: #999;
@@ -3069,7 +3069,7 @@
     min-height: 140px;
     padding: 15px 20px;
     text-align: left;
-    clear: both;    
+    clear: both;
 }
 
 div.markdown-preview {
@@ -3482,9 +3482,9 @@
                 text-decoration: none;
             }
 
-            &.name { 
+            &.name {
                 color : #333;
-                
+
                 &:hover {
                     color:#333;
                 }
@@ -3534,7 +3534,7 @@
                     &.item-icon {
                         background-color:#F7F7F7;
                         color:#3592b5;
-                        font-size: 9px; 
+                        font-size: 9px;
                         padding-top:2px;
                         line-height: 12px;
                         border-left:1px solid #EEE;
@@ -3542,7 +3542,7 @@
                         &:first-child {
                             border-left:none;
                         }
-                        
+
                         &.strong {
                             color:@primary;
                         }
@@ -3686,7 +3686,7 @@
             min-height:30px;
         }
 
-       
+
         .write-comment-box { padding:0; margin-top:20px; }
     }
 
@@ -5665,7 +5665,7 @@
         text-decoration:none;
         background-color:#fafafa;
     }
-    
+
     &.open {
         .box-shadow(inset 5px 0px 0px @state-open);
     }
@@ -5849,7 +5849,7 @@
         margin-top: 15px;
         font-weight: normal;
         padding:0 15px;
-        
+
         strong {
             color:#f36c22;
         }
@@ -5956,7 +5956,7 @@
         margin-top:10px;
         font-size: 13px;
         color:#999;
-        padding-left:20px;  
+        padding-left:20px;
 
         &.np {
             padding-left:0 !important;
@@ -5974,7 +5974,7 @@
         }
     }
 
-    .user-link { 
+    .user-link {
         color: #3592b5 !important;
         font-weight: bold;
     }
@@ -6130,6 +6130,81 @@
     }
 }
 
+.webhook-editor-wrap {
+    .new-webhook-wrap {
+        margin:30px auto;
+
+        .form-legend {
+            display:block;
+            margin-bottom:10px;
+        }
+        .form-wrap {
+            position:relative;
+            display:inline-block;
+            vertical-align:top;
+        }
+        .input-webhook-payload {
+            width:355px;
+        }
+        .input-webhook-secret {
+            width:214px;
+        }
+        .btn-submit {
+            vertical-align: top;
+            min-width:100px;
+        }
+    }
+
+    .webhook-list-wrap {
+        margin:0 auto;
+
+        .list-head {
+            border:1px solid #ddd;
+            border-width:2px 0px 0px;
+            background: #fafafa;
+
+            .payload-url {
+                padding-right:18px;
+                line-height:30px;
+                text-align:right;
+            }
+            .secret {
+                padding-left:8px;
+                line-height:30px;
+            }
+        }
+        .list-item {
+            border-top:1px solid #ddd;
+
+            .table {
+                .webhook-secret {
+                    display: block;
+                    max-width: 50%;
+                    font-size: 12px;
+                }
+                tr {
+                    border-bottom: 1px solid #ddd;
+                    &:last-of-type {
+                        border-bottom: none;
+                    }
+                }
+                td {
+                    border-top: none;
+                    &.info {
+                        width: 150px;
+                        text-align: right;
+                    }
+                    &.actions {
+                        width: 150px;
+                        text-align: right;
+                        padding-right: 1px;
+                    }
+                }
+            }
+        }
+    }
+}
+
 .category-exclusive {
     margin-right:5px;
     vertical-align: middle;
@@ -6234,4 +6309,3 @@
 .watch-btn > button {
   padding: 4px 10px;
 }
-
app/controllers/ProjectApp.java
--- app/controllers/ProjectApp.java
+++ app/controllers/ProjectApp.java
@@ -1156,9 +1156,13 @@
     @Transactional
     @IsAllowed(Operation.UPDATE)
     public static Result newWebhook(String ownerId, String projectName) {
-        if(HttpUtil.isJSONPreferred(request())){
-            return ok("{}");
-        }
+        Form<Webhook> addWebhookForm = form(Webhook.class).bindFromRequest();
+        Project project = Project.findByOwnerAndProjectName(ownerId, projectName);
+
+        Webhook.create(project.id,
+                        addWebhookForm.field("payload_url").value(),
+                        addWebhookForm.field("secret").value());
+
         return redirect(routes.ProjectApp.webhooks(ownerId, projectName));
     }
 
app/models/Webhook.java
--- app/models/Webhook.java
+++ app/models/Webhook.java
@@ -107,9 +107,20 @@
         return find.where().eq("project.id", projectId).findList();
     }
 
-    public static List<Webhook> findWebhookListByProject(Long projectId) {
-        return find.fetch("webhook").fetch("payload_url", "secret").where()
-                .eq("project.id", projectId).findList();
+    public static void create(Long projectId, String payloadUrl, String secret) {
+        Webhook webhook = new Webhook(projectId, payloadUrl, secret);
+        webhook.save();
+    }
+
+    public static void delete(Long webhookId, Long projectId) {
+        Webhook.findByIds(webhookId, projectId).delete();
+    }
+
+    public static Webhook findByIds(Long webhookId, Long projectId) {
+        return find.where()
+                .eq("webhook.id", webhookId)
+                .eq("project.id", projectId)
+                .findUnique();
     }
 
     /**
 
app/views/project/partial_webhooks_list.scala.html (added)
+++ app/views/project/partial_webhooks_list.scala.html
@@ -0,0 +1,69 @@
+@**
+* Yobi, Project Hosting SW
+*
+* Copyright 2015 NAVER Corp.
+* http://yobi.io
+*
+* @author Jihwan Chun
+*
+* Licensed under the Apache License, Version 2.0 (the "License");
+* you may not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+*   http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License.
+**@
+@(project:Project, webhooks: List[models.Webhook])
+
+@import utils.TemplateHelper._
+@import utils.AccessControl._
+
+@if(webhooks.isEmpty){
+  <div class="error-wrap">
+    <i class="ico ico-err1"></i>
+    <p>@Messages("project.webhook.list.empty")</p>
+  </div>
+} else {
+  <div class="row-fluid list-head">
+    <div class="span8 payload-url">
+      <strong>@Messages("project.webhook.payloadUrl")</strong>
+    </div>
+      <div class="span4 secret">
+      <strong>@Messages("project.webhook.secret")</strong>
+    </div>
+  </div>
+
+  @webhooks.map { webhook =>
+    <div class="row-fluid list-item" data-webhook-id="@webhook.id">
+      <div class="span8">
+        <h6 class="right-txt mr20">
+          @webhook.payloadUrl
+        </h6>
+      </div>
+      <div class="span4">
+        <table class="table nm">
+          <tr>
+            <td>
+              <span class="webhook-secret">
+                  @webhook.secret
+              </span>
+            </td>
+            <td class="actions">
+              <button type="button"
+                class="ybtn ybtn-danger ybtn-small"
+                data-webhook-id="@webhook.id"
+                data-delete-uri="@routes.ProjectApp.deleteWebhook(project.owner, project.name, webhook.id)">
+                  @Messages("button.delete")
+              </button>
+            </td>
+          </tr>
+        </table>
+      </div>
+    </div>
+  }
+}
app/views/project/webhooks.scala.html
--- app/views/project/webhooks.scala.html
+++ app/views/project/webhooks.scala.html
@@ -4,7 +4,7 @@
 * Copyright 2015 NAVER Corp.
 * http://yobi.io
 *
-* @author Jihwan Chun @ KAIST School of Computing
+* @author Jihwan Chun
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
@@ -18,18 +18,42 @@
 * See the License for the specific language governing permissions and
 * limitations under the License.
 **@
-@(message: String, webhooks: List[Webhook], project:Project)
+@(message: String, webhooks: List[models.Webhook], project:Project)
 
-@import helper._
+@import utils.TemplateHelper._
+@import utils.AccessControl._
 
 @projectLayout(message, project, utils.MenuType.PROJECT_SETTING) {
-@projectMenu(project, utils.MenuType.PROJECT_SETTING, "")
-<div class="page-wrap-outer">
-    <div class="project-page-wrap">
-        @partial_settingmenu(project)
+  @projectMenu(project, utils.MenuType.PROJECT_SETTING, "")
+  <div class="page-wrap-outer">
+    <div class="project-page-wrap webhook-editor-wrap">
+      @partial_settingmenu(project)
 
-        Hello world!
-</div>
-<link rel="stylesheet" type="text/css" media="screen" href="/assets/javascripts/lib/mentionjs/mention.css">
+      @if(isProjectResourceCreatable(UserApp.currentUser, project, ResourceType.WEBHOOK)) {
+      <form id="formNewWebhook" action="@routes.ProjectApp.newWebhook(project.owner, project.name)" method="post" class="new-webhook-wrap">
+        <strong class="form-legend">@Messages("project.webhook.new")</strong>
+        <div class="form-wrap">
+          <div>
+            <input type="text" name="payload_url" class="input-webhook-payload" maxlength="2048" autocomplete="off" placeholder="@Messages("project.webhook.payloadUrl")">
+            <input type="text" name="secret" class="input-webhook-secret" maxlength="250" autocomplete="off" placeholder="@Messages("project.webhook.secret")">
+          </div>
+        </div>
+        <button type="submit" class="ybtn ybtn-primary btn-submit">@Messages("project.webhook.add")</button>
+      </form>
+      }
 
+      <div id="webhooksList" class="webhook-list-wrap">
+        @partial_webhooks_list(project, webhooks)
+      </div>
+    </div>
+  </div>
+
+  <script type="text/javascript">
+    $(function(){
+      $yobi.loadModule("project.Webhook", {
+        "form": "#formNewWebhook",
+        "list": "#webhooksList"
+      });
+    });
+  </script>
 }
conf/messages
--- conf/messages
+++ conf/messages
@@ -617,6 +617,11 @@
 project.watcher.number = number of watcher
 project.watching = Watching
 project.webhook = Webhooks
+project.webhook.new = Create new webhook (Invoked by git push)
+project.webhook.payloadUrl = Payload URL
+project.webhook.secret = Secret token
+project.webhook.add = Add webhook
+project.webhook.list.empty = No webhook exists.
 project.you.are.not.watching = You are not watching {0} project.
 project.you.are.watching = You are watching {0} project.
 project.you.may.want.to.be.a.member = You can send a sign-up request for {0} project.
conf/messages.ja-JP
--- conf/messages.ja-JP
+++ conf/messages.ja-JP
@@ -404,6 +404,7 @@
 project.watch = 見守る
 project.watching = 見守っている
 project.webhook = Webフック
+project.webhook.add = Webフック追加
 project.you.are.not.watching = {0} プロジェクトを見守っていません
 project.you.are.watching = {0} プロジェクトを見守っています
 project.you.may.want.to.be.a.member = {0} プロジェクトのメンバーに追加の申請が出来ません
conf/messages.ko-KR
--- conf/messages.ko-KR
+++ conf/messages.ko-KR
@@ -617,6 +617,11 @@
 project.watcher.number = 지켜보는 사람 수
 project.watching = 지켜보기 중
 project.webhook = 웹후크
+project.webhook = 새 웹후크 생성 (Git push 시에 실행됩니다)
+project.webhook.payloadUrl = 전송할 주소
+project.webhook.secret = 보안 토큰
+project.webhook.add = 웹후크 추가
+project.webhook.list.empty = 등록된 웹후크가 없습니다.
 project.you.are.not.watching = {0} 프로젝트를 지켜보고 있지 않습니다.
 project.you.are.watching = {0} 프로젝트를 지켜보는 중입니다.
 project.you.may.want.to.be.a.member = {0} 프로젝트 멤버로 등록 요청을 할 수 있습니다.
 
public/javascripts/service/yobi.project.Webhook.js (added)
+++ public/javascripts/service/yobi.project.Webhook.js
@@ -0,0 +1,99 @@
+/**
+ * Yobi, Project Hosting SW
+ *
+ * Copyright 2015 NAVER Corp.
+ * http://yobi.io
+ *
+ * @author Jihwan Chun
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+(function(ns){
+
+    var oNS = $yobi.createNamespace(ns);
+    oNS.container[oNS.name] = function(options){
+
+        var elements = {};
+
+        /**
+         * Initialize
+         * @param options
+         * @private
+         */
+        function _init(options){
+            _initElement();
+            _attachEvent();
+        }
+
+        /**
+         * Initialize element variables
+         * @param options
+         * @private
+         */
+        function _initElement(){
+            elements.webhookListWrap = $(".webhook-list-wrap");
+        }
+
+        /**
+         * Attach event handlers
+         * @private
+         */
+        function _attachEvent(){
+            elements.webhookListWrap.on("click", "[data-delete-uri]", _onClickBtnDeleteWebhook);
+        }
+
+        /**
+         * "Click" event handler of webhook row delete button
+         * Send request to remove webhook.
+         *
+         * @param event
+         * @private
+         */
+        function _onClickBtnDeleteWebhook(evt){
+            // TODO: Decide whether to show confirm modal or not
+            _requestRemoveWebhook(evt.target);
+        }
+
+        /**
+         * Send AJAX request to remove webhook with specified delete button
+         *
+         * @param target
+         * @private
+         */
+        function _requestRemoveWebhook(target){
+            var targetButton = $(target);
+
+            $.ajax(targetButton.data("deleteUri"), {
+                "method": "post",
+                "data"  : {"_method": "delete"}
+            })
+            .done(function(){
+                _removeWebhookFromView(targetButton.data("webhookId"));
+            });
+        }
+
+        /**
+         * Remove specified webhook from webhook view
+         *
+         * @param webhookId
+         * @private
+         */
+        function _removeWebhookFromView(webhookId){
+            $('div[data-webhook-id="' + webhookId + '"]').remove();
+        }
+
+        _init(options || {});
+    };
+
+})("yobi.project.Webhook");
Add a comment
List