doortts doortts 2017-01-28
usermenu: Support favorite project/organization
@2ea871629ec491e36bfdd27833a408f9b9d49703
app/assets/stylesheets/less/_usermenu.less
--- app/assets/stylesheets/less/_usermenu.less
+++ app/assets/stylesheets/less/_usermenu.less
@@ -74,6 +74,9 @@
     margin: 10px 0;
     padding: 0;
     list-style-type: none;
+    .favored {
+      border-bottom: 1px dashed #7b1fa2;
+    }
   }
 
   .user-li {
@@ -292,10 +295,10 @@
     height: 15px;
   }
   .starred {
-    color: red;
+    color: #e91e63;
   }
 
-  .star-project {
+  .star-project, .star-org {
     width: 29px;
     min-height: 35px;
     flex-shrink: 0;
@@ -544,7 +547,7 @@
     border-bottom: 1px dashed #7b1fa2;
   }
 
-  .project-list{
+  .project-list, .org-list {
     &:hover {
       background-color: #eee;
     }
 
app/controllers/api/UserApi.java (added)
+++ app/controllers/api/UserApi.java
@@ -0,0 +1,84 @@
+/**
+ * Yona, 21st Century Project Hosting SW
+ * <p>
+ * Copyright Yona & Yobi Authors & NAVER Corp.
+ * https://yona.io
+ **/
+package controllers.api;
+
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import controllers.UserApp;
+import models.FavoriteOrganization;
+import models.FavoriteProject;
+import play.db.ebean.Transactional;
+import play.libs.Json;
+import play.mvc.Controller;
+import play.mvc.Result;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import static play.libs.Json.toJson;
+
+public class UserApi extends Controller {
+
+    @Transactional
+    public static Result toggleFoveriteProject(String projectId) {
+        if(projectId == null) {
+            return badRequest("Wrong project id");
+        }
+        boolean isFavored = UserApp.currentUser().toggleFavoriteProject(Long.valueOf(projectId));
+        ObjectNode json = Json.newObject();
+        json.put("projectId", projectId);
+        json.put("favored", isFavored);
+        return ok(json);
+    }
+
+    @Transactional
+    public static Result getFoveriteProjects() {
+        ObjectNode json = Json.newObject();
+        List<ObjectNode> projects = new ArrayList<>();
+        List<Long> projectIds = new ArrayList<>();
+        for (FavoriteProject favoriteProject : UserApp.currentUser().favoriteProjects) {
+            ObjectNode project = Json.newObject();
+            project.put("projectId", favoriteProject.project.id);
+            project.put("projectName", favoriteProject.projectName);
+            project.put("owner", favoriteProject.owner);
+            projects.add(project);
+            projectIds.add(favoriteProject.project.id);
+        }
+        json.put("projectIds", toJson(projectIds));
+        json.put("projects", toJson(projects));
+        return ok(json);
+    }
+
+
+    @Transactional
+    public static Result toggleFoveriteOrganization(String organizationId) {
+        if(organizationId == null) {
+            return badRequest("Wrong organization id");
+        }
+        boolean isFavored = UserApp.currentUser().toggleFavoriteOrganization(Long.valueOf(organizationId));
+        ObjectNode json = Json.newObject();
+        json.put("organizationId", organizationId);
+        json.put("favored", isFavored);
+        return ok(json);
+    }
+
+    @Transactional
+    public static Result getFoveriteOrganizations() {
+        ObjectNode json = Json.newObject();
+        List<ObjectNode> organizations = new ArrayList<>();
+        List<Long> organizationIds = new ArrayList<>();
+        for (FavoriteOrganization favoriteOrganization : UserApp.currentUser().favoriteOrganizations) {
+            ObjectNode organization = Json.newObject();
+            organization.put("organizationId", favoriteOrganization.organization.id);
+            organization.put("organizationName", favoriteOrganization.organizationName);
+            organizations.add(organization);
+            organizationIds.add(favoriteOrganization.organization.id);
+        }
+        json.put("organizationIds", toJson(organizationIds));
+        json.put("organizations", toJson(organizations));
+        return ok(json);
+    }
+}
 
app/models/FavoriteOrganization.java (added)
+++ app/models/FavoriteOrganization.java
@@ -0,0 +1,37 @@
+/**
+ * Yona, 21st Century Project Hosting SW
+ * <p>
+ * Copyright Yona & Yobi Authors & NAVER Corp.
+ * https://yona.io
+ **/
+package models;
+
+import play.db.ebean.Model;
+
+import javax.persistence.Entity;
+import javax.persistence.Id;
+import javax.persistence.ManyToOne;
+import javax.persistence.OneToOne;
+
+@Entity
+public class FavoriteOrganization extends Model {
+    public static Finder<Long, FavoriteOrganization> finder = new Finder<>(Long.class, FavoriteOrganization.class);
+
+    @Id
+    public Long id;
+
+    @ManyToOne
+    public User user;
+
+    @OneToOne
+    public Organization organization;
+
+    public String organizationName;
+
+    public FavoriteOrganization(User user, Organization organization) {
+        this.user = user;
+        this.organization = organization;
+
+        this.organizationName = organization.name;
+    }
+}
 
app/models/FavoriteProject.java (added)
+++ app/models/FavoriteProject.java
@@ -0,0 +1,39 @@
+/**
+ * Yona, 21st Century Project Hosting SW
+ * <p>
+ * Copyright Yona & Yobi Authors & NAVER Corp.
+ * https://yona.io
+ **/
+package models;
+
+import play.db.ebean.Model;
+
+import javax.persistence.Entity;
+import javax.persistence.Id;
+import javax.persistence.ManyToOne;
+import javax.persistence.OneToOne;
+
+@Entity
+public class FavoriteProject extends Model {
+    public static Finder<Long, FavoriteProject> finder = new Finder<>(Long.class, FavoriteProject.class);
+
+    @Id
+    public Long id;
+
+    @ManyToOne
+    public User user;
+
+    @OneToOne
+    public Project project;
+
+    public String owner;
+    public String projectName;
+
+    public FavoriteProject(User user, Project project) {
+        this.user = user;
+        this.project = project;
+
+        this.owner = project.owner;
+        this.projectName = project.name;
+    }
+}
app/models/User.java
--- app/models/User.java
+++ app/models/User.java
@@ -174,6 +174,12 @@
     @OneToMany(mappedBy = "user")
     public List<Mention> mentions;
 
+    @OneToMany(mappedBy = "user")
+    public List<FavoriteProject> favoriteProjects;
+
+    @OneToMany(mappedBy = "user")
+    public List<FavoriteOrganization> favoriteOrganizations;
+
     /**
      * The user's preferred language code which can be recognized by {@link play.api.i18n.Lang#get},
      * such as "ko", "en-US" or "ja". This field is used as a language for notification mail.
@@ -857,4 +863,72 @@
     public int hashCode() {
         return id != null ? id.hashCode() : 0;
     }
+
+    public List<Project> getFavoriteProjects() {
+        List<Project> projects = new ArrayList<>();
+        for (FavoriteProject favoriteProject : this.favoriteProjects) {
+            projects.add(favoriteProject.project);
+        }
+
+        return projects;
+    }
+
+    public boolean toggleFavoriteProject(Long projectId) {
+        for (FavoriteProject favoriteProject : this.favoriteProjects) {
+            if( favoriteProject.project.id.equals(projectId) ){
+                removeFavoriteProject(projectId);
+                this.favoriteProjects.remove(favoriteProject);
+                return false;
+            }
+        }
+
+        FavoriteProject favoriteProject = new FavoriteProject(this, Project.find.byId(projectId));
+        this.favoriteProjects.add(favoriteProject);
+        favoriteProject.save();
+        return true;
+    }
+
+    private void removeFavoriteProject(Long projectId) {
+        List<FavoriteProject> list = FavoriteProject.finder.where()
+                .eq("user.id", this.id)
+                .eq("project.id", projectId).findList();
+
+        if(list != null && list.size() > 0){
+            list.get(0).delete();
+        }
+    }
+
+    public List<Organization> getFavoriteOrganizations() {
+        List<Organization> organizations = new ArrayList<>();
+        for (FavoriteOrganization favoriteOrganization : this.favoriteOrganizations) {
+            organizations.add(favoriteOrganization.organization);
+        }
+
+        return organizations;
+    }
+
+    public boolean toggleFavoriteOrganization(Long organizationId) {
+        for (FavoriteOrganization favoriteOrganization : this.favoriteOrganizations) {
+            if( favoriteOrganization.organization.id.equals(organizationId) ){
+                removeFavoriteOrganization(organizationId);
+                this.favoriteOrganizations.remove(favoriteOrganization);
+                return false;
+            }
+        }
+
+        FavoriteOrganization favoriteOrganization = new FavoriteOrganization(this, Organization.find.byId(organizationId));
+        this.favoriteOrganizations.add(favoriteOrganization);
+        favoriteOrganization.save();
+        return true;
+    }
+
+    private void removeFavoriteOrganization(Long organizationId) {
+        List<FavoriteOrganization> list = FavoriteOrganization.finder.where()
+                .eq("user.id", this.id)
+                .eq("organization.id", organizationId).findList();
+
+        if(list != null && list.size() > 0){
+            list.get(0).delete();
+        }
+    }
 }
app/views/index/myOrganizationList.scala.html
--- app/views/index/myOrganizationList.scala.html
+++ app/views/index/myOrganizationList.scala.html
@@ -7,31 +7,22 @@
 @(currentUser:User)
 @import utils.TemplateHelper._
 
-@displayProjects(title:String, organizations:List[Organization], isActive:Boolean = false) = {
-@if(organizations.isEmpty) {
-    <div id="@title" class="no-result tab-pane user-ul @if(isActive){active}">@Messages("title.no.results")</div>
-} else {
-    <ul class="tab-pane user-ul @if(isActive){active}" id="@title">
-    @for(organization <- organizations){
-        <li class="user-li">
-            <div class="project-list project-flex-container">
-                <div class="project-item project-item-container">
-                    <div class="flex-item site-logo">
-                        <i class="project-avatar">@if(hasOrganizationLogo(organization)){<img class="logo" src="@urlToOrganizationLogo(organization)">}else{<span class="dummy-25px"> </span>}</i>
-                    </div>
-                    <div class="projectName-owner flex-item" onclick="window.location='@routes.OrganizationApp.organization(organization.name)';">
-                        <div class="project-name org-name flex-item"><a href="@routes.OrganizationApp.organization(organization.name)">@organization.name</a></div>
-                        <div class="project-owner flex-item"></div>
-                    </div>
-                </div>
-                <div class="star-project flex-item">
-                    <i class="star material-icons">star</i>
-                </div>
-            </div>
-        </li>
+@displayOrganizations(title:String, organizations:List[Organization], favoredOrganizations:List[Organization],  isActive:Boolean = false) = {
+    @if(organizations.isEmpty && favoredOrganizations.isEmpty) {
+        <div id="@title" class="no-result tab-pane user-ul @if(isActive) {active}">@Messages("title.no.results")</div>
+    } else {
+        <ul class="tab-pane user-ul @if(isActive) {active}" id="@title">
+            @for(organization <- favoredOrganizations) {
+                @myOrganizationList_partial(organization, true)
+            }
+            @if(favoredOrganizations.nonEmpty){<li class="favored"></li>}
+            @for(organization <- organizations) {
+                @if(!favoredOrganizations.contains(organization)) {
+                    @myOrganizationList_partial(organization, false)
+                }
+            }
+        </ul>
     }
-    </ul>
-}
 }
 
     <div class="search-result">
@@ -39,5 +30,5 @@
             <input class="search-input org-search" type="text" id="query" autocomplete="off" placeholder="@Messages("title.type.name")">
             <span class="bar"></span>
         </div>
-        @displayProjects("recentlyVisited", Organization.findOrganizationsByUserLoginId(UserApp.currentUser.loginId))
+        @displayOrganizations("organizations", Organization.findOrganizationsByUserLoginId(UserApp.currentUser.loginId), currentUser.getFavoriteOrganizations)
     </div>
 
app/views/index/myOrganizationList_partial.scala.html (added)
+++ app/views/index/myOrganizationList_partial.scala.html
@@ -0,0 +1,25 @@
+@**
+* Yona, 21st Century Project Hosting SW
+*
+* Copyright Yona & Yobi Authors & NAVER Corp.
+* https://yona.io
+**@
+@(organization: Organization, favored:Boolean)
+@import utils.TemplateHelper._
+
+<li class="user-li" onclick="window.location='@routes.OrganizationApp.organization(organization.name)';">
+    <div class="org-list project-flex-container">
+        <div class="project-item project-item-container">
+            <div class="flex-item site-logo">
+                <i class="project-avatar">@if(hasOrganizationLogo(organization)){<img class="logo" src="@urlToOrganizationLogo(organization)">}else{<span class="dummy-25px"> </span>}</i>
+            </div>
+            <div class="projectName-owner flex-item">
+                <div class="project-name org-name flex-item"><a href="@routes.OrganizationApp.organization(organization.name)">@organization.name</a></div>
+                <div class="project-owner flex-item"></div>
+            </div>
+        </div>
+        <div class="star-org flex-item" data-organization-id="@organization.id">
+            <i class="star @if(favored){starred} material-icons">star</i>
+        </div>
+    </div>
+</li>
app/views/index/myProjectList.scala.html
--- app/views/index/myProjectList.scala.html
+++ app/views/index/myProjectList.scala.html
@@ -7,28 +7,31 @@
 @(currentUser:User)
 @import utils.TemplateHelper._
 
+@displayProjectsWithFavored(title:String, projects:List[Project], favoredProjects:List[Project], isActive:Boolean = false) = {
+    @if(projects.isEmpty && favoredProjects.isEmpty) {
+        <div id="@title" class="no-result tab-pane user-ul @if(isActive){active}">@Messages("title.no.results")</div>
+    } else {
+        <ul class="tab-pane user-ul @if(isActive){active}" id="@title">
+        @for(project <- favoredProjects){
+            @myProjectList_partial(project, true)
+        }
+        @if(favoredProjects.nonEmpty){<li class="favored"></li>}
+        @for(project <- projects){
+            @if(!favoredProjects.contains(project)){
+                @myProjectList_partial(project, false)
+            }
+        }
+        </ul>
+    }
+}
+
 @displayProjects(title:String, projects:List[Project], isActive:Boolean = false) = {
     @if(projects.isEmpty) {
         <div id="@title" class="no-result tab-pane user-ul @if(isActive){active}">@Messages("title.no.results")</div>
     } else {
         <ul class="tab-pane user-ul @if(isActive){active}" id="@title">
         @for(project <- projects){
-            <li class="user-li" onclick="window.location='@routes.ProjectApp.project(project.owner, project.name)';">
-                <div class="project-list project-flex-container">
-                    <div class="project-item project-item-container">
-                        <div class="flex-item site-logo">
-                            <i class="project-avatar">@if(hasProjectLogo(project)){<a href="@routes.ProjectApp.project(project.owner, project.name)" ><img class="logo" src="@urlToProjectLogo(project)"></a>}else{<span class="dummy-25px"> </span>}</i>
-                        </div>
-                        <div class="projectName-owner flex-item">
-                            <div class="project-name flex-item"><a href="@routes.ProjectApp.project(project.owner, project.name)">@project.name @if(project.isPrivate){<i class="yobicon-lock yobicon-small"></i>}</a></div>
-                            <div class="project-owner flex-item"><a href="@routes.UserApp.userInfo(project.owner)" >@project.owner</a></div>
-                        </div>
-                    </div>
-                    <div class="star-project flex-item">
-                        <i class="star material-icons">star</i>
-                    </div>
-                </div>
-            </li>
+            @myProjectList_partial(project, false)
         }
         </ul>
     }
@@ -65,7 +68,7 @@
                         </ul>
                     </div>
                     <div class="tab-content">
-                        @displayProjects("recentlyVisited", currentUser.getVisitedProjects, true)
+                        @displayProjectsWithFavored("recentlyVisited", currentUser.getVisitedProjects, currentUser.getFavoriteProjects, true)
                         @displayProjects("watching", currentUser.getWatchingProjects("name ASC"))
                         @displayProjects("createdByMe", Project.findProjectsCreatedByUser(currentUser.loginId, "createdDate desc"))
                         @displayProjects("joinmember", Project.findProjectsJustMemberAndNotOwner(currentUser, "name ASC"))
 
app/views/index/myProjectList_partial.scala.html (added)
+++ app/views/index/myProjectList_partial.scala.html
@@ -0,0 +1,26 @@
+@**
+* Yona, 21st Century Project Hosting SW
+*
+* Copyright Yona & Yobi Authors & NAVER Corp.
+* https://yona.io
+**@
+@(project:Project, favored:Boolean)
+@import utils.TemplateHelper._
+
+<li class="user-li" onclick="window.location='@routes.ProjectApp.project(project.owner, project.name)';">
+    <div class="project-list project-flex-container">
+        <div class="project-item project-item-container">
+            <div class="flex-item site-logo">
+                <i class="project-avatar">@if(hasProjectLogo(project)){<a href="@routes.ProjectApp.project(project.owner, project.name)" ><img class="logo" src="@urlToProjectLogo(project)"></a>}else{<span class="dummy-25px"> </span>}</i>
+            </div>
+            <div class="projectName-owner flex-item">
+                <div class="project-name flex-item"><a href="@routes.ProjectApp.project(project.owner, project.name)">@project.name @if(project.isPrivate){<i class="yobicon-lock yobicon-small"></i>}</a></div>
+                <div class="project-owner flex-item"><a href="@routes.UserApp.userInfo(project.owner)" >@project.owner</a></div>
+            </div>
+        </div>
+        <div class="star-project flex-item" data-project-id="@project.id">
+            <i class="star @if(favored){starred} material-icons">star</i>
+        </div>
+    </div>
+</li>
+
app/views/layout.scala.html
--- app/views/layout.scala.html
+++ app/views/layout.scala.html
@@ -64,6 +64,7 @@
                     closeSidebar($sidebar);
                 } else {
                     openSidebar($sidebar);
+                    updateStar();
                 }
             });
 
@@ -80,6 +81,7 @@
 
             // used for new project list ui
             $(".right-menu").on('click', ".myProjectList, a[href='#recentlyVisited'], a[href='#createdByMe'], a[href='#watching'], a[href='#joinmember']", function() {
+                updateStar();
                 setTimeout(function focusToProjectSearchInput() {
                     var $projectSearch = $('.project-search');
                     var $orgSearch = $('.org-search');
@@ -117,6 +119,53 @@
                         break;
                 }
             });
+
+            $(".project-list > .star-project").on("click", function toggleProjectFavorite(e) {
+                e.stopPropagation();
+                var that = $(this);
+                $.post("@api.routes.UserApi.toggleFoveriteProject("")" + that.data("projectId"))
+                        .done(function (data) {
+                            if(data.favored){
+                                that.find('i').addClass("starred");
+                            } else {
+                                that.find('i').removeClass("starred");
+                            }
+                        })
+                        .fail(function (data) {
+                            $yobi.alert("Update failed: " + JSON.parse(data.responseText).reason);
+                        });
+            });
+
+            $(".org-list > .star-org").on("click", function toggleOrgFavorite(e) {
+                e.stopPropagation();
+                var that = $(this);
+                $.post("@api.routes.UserApi.toggleFoveriteOrganization("")" + that.data("organizationId"))
+                        .done(function (data) {
+                            if(data.favored){
+                                that.find('i').addClass("starred");
+                            } else {
+                                that.find('i').removeClass("starred");
+                            }
+                        })
+                        .fail(function (data) {
+                            $yobi.alert("Update failed: " + JSON.parse(data.responseText).reason);
+                        });
+            });
+
+            // This method intended to sync sub tab list of projects
+            function updateStar(){
+                $.get("@api.routes.UserApi.getFoveriteProjects()")
+                        .done(function(data){
+                            $(".star-project").each(function () {
+                                var $this = $(this);
+                                if (data.projectIds.includes($this.data("projectId"))) {
+                                    $this.find("i").addClass("starred");
+                                } else {
+                                    $this.find("i").removeClass("starred");
+                                }
+                            });
+                        });
+            }
         });
     </script>
 </body>
 
conf/evolutions/default/11.sql (added)
+++ conf/evolutions/default/11.sql
@@ -0,0 +1,37 @@
+# --- !Ups
+CREATE TABLE favorite_project (
+  id                        BIGINT AUTO_INCREMENT NOT NULL,
+  user_id                   BIGINT,
+  project_id                BIGINT,
+  owner                     VARCHAR(255),
+  project_name              VARCHAR(255),
+  CONSTRAINT pk_favorite_project PRIMARY KEY (id),
+  CONSTRAINT uq_favorite_project_user_id_project_id_1 UNIQUE (user_id, project_id),
+  CONSTRAINT fk_favorite_project_user FOREIGN KEY (user_id) REFERENCES n4user (id) on DELETE CASCADE,
+  CONSTRAINT fk_favorite_project_project FOREIGN KEY (project_id) REFERENCES project (id) on DELETE CASCADE
+  )
+  row_format=compressed, key_block_size=8
+;
+
+CREATE index ix_favorite_project_user_1 ON favorite_project (user_id);
+CREATE index ix_favorite_project_project_2 ON favorite_project (project_id);
+
+CREATE TABLE favorite_organization (
+  id                        BIGINT AUTO_INCREMENT NOT NULL,
+  user_id                   BIGINT,
+  organization_id           BIGINT,
+  organization_name        VARCHAR(255),
+  CONSTRAINT pk_favorite_organization PRIMARY KEY (id),
+  CONSTRAINT uq_favorite_organization_user_id_organization_id_1 UNIQUE (user_id, organization_id),
+  CONSTRAINT fk_favorite_organization_user FOREIGN KEY (user_id) REFERENCES n4user (id) on DELETE CASCADE,
+  CONSTRAINT fk_favorite_organization_organization FOREIGN KEY (organization_id) REFERENCES organization (id) on DELETE CASCADE
+  )
+  row_format=compressed, key_block_size=8
+;
+
+CREATE index ix_favorite_organization_user_1 ON favorite_organization (user_id);
+CREATE index ix_favorite_organization_organization_2 ON favorite_organization (organization_id);
+
+# --- !Downs
+DROP TABLE favorite_project;
+DROP TABLE favorite_organization;
conf/routes
--- conf/routes
+++ conf/routes
@@ -39,6 +39,10 @@
 POST           /-_-api/v1/owners/:owner/projects/:projectName/posts                    controllers.api.BoardApi.newPostByJson(owner:String, projectName:String)
 POST           /-_-api/v1/owners/:owner/projects/:projectName/postlabel/:number        controllers.api.BoardApi.updatePostLabel(owner:String, projectName:String, number:Long)
 GET            /-_-api/v1/hello                                                        controllers.api.GlobalApi.hello()
+GET            /-_-api/v1/favoriteProjects                                             controllers.api.UserApi.getFoveriteProjects
+POST           /-_-api/v1/favoriteProjects/:projectId                                  controllers.api.UserApi.toggleFoveriteProject(projectId:String)
+GET            /-_-api/v1/favoriteOrganizations                                        controllers.api.UserApi.getFoveriteOrganizations
+POST           /-_-api/v1/favoriteOrganizations/:organizationId                        controllers.api.UserApi.toggleFoveriteOrganization(organizationId:String)
 
 
 
Add a comment
List