doortts doortts 2018-10-25
usermenu: Recategorize user project menu
@7a5bfd580519eeb30cde587a3fba37e9e353b656
app/assets/stylesheets/less/_usermenu.less
--- app/assets/stylesheets/less/_usermenu.less
+++ app/assets/stylesheets/less/_usermenu.less
@@ -60,7 +60,7 @@
   }
 
   li {
-    border-bottom: 1px solid #eee;
+    border-bottom: 1px solid transparent;
     margin-left: 0;
   }
 
@@ -450,23 +450,42 @@
   .project-item {
     font-size: 14px;
     overflow: hidden;
+
     .slash {
       padding: 5px;
     }
+
     .project-name{
       min-width: 50px;
       max-width: 220px;
       text-overflow: ellipsis;
       overflow: hidden;
       white-space: nowrap;
+
       &.org-name {
         max-width: 320px;
+        font-size: 1.1em;
       }
+
       a {
         &:hover {
           text-decoration: none;
           color: #3592B5;
         }
+      }
+    }
+
+    .all-project-names {
+      padding-left: 30px;
+    }
+
+    .all-org-names {
+      font-weight: 600;
+      color: black;
+      border-bottom: 1px solid transparent;
+
+      &:hover {
+        border-bottom: 1px solid #3592B5;
       }
     }
 
@@ -564,6 +583,7 @@
 
   .project-list, .org-list {
     &:hover {
+      cursor: pointer;
     }
   }
 
app/models/Organization.java
--- app/models/Organization.java
+++ app/models/Organization.java
@@ -92,6 +92,16 @@
         return (findRowCount != 0);
     }
 
+    public List<Project> getSortedByProjectName() {
+        this.projects.sort(new Comparator<Project>() {
+            @Override
+            public int compare(Project o1, Project o2) {
+                return o1.name.compareToIgnoreCase(o2.name);
+            }
+        });
+        return this.projects;
+    }
+
     public boolean isLastAdmin(User currentUser) {
         return OrganizationUser.isAdmin(this, currentUser) && getAdmins().size() == 1;
     }
@@ -155,6 +165,40 @@
                 .findList();
     }
 
+    public static List<Organization> findAllOrganizations() {
+        List<Organization> projects = Organization.find.fetch("projects").where().orderBy("name asc, projects.name asc").findList();
+        projects.sort(new Comparator<Organization>() {
+            @Override
+            public int compare(Organization o1, Organization o2) {
+                return o1.name.compareToIgnoreCase(o2.name);
+            }
+        });
+        return projects;
+    }
+
+    public static List<Organization> findAllOrganizations(String loginId) {
+        User user = User.findByLoginId(loginId);
+
+        Set<String> owners = new TreeSet<>(new Comparator<String>() {
+            @Override
+            public int compare(String o1, String o2) {
+                return o1.compareToIgnoreCase(o2);
+            }
+        });
+        for (FavoriteProject fp : user.favoriteProjects) {
+            owners.add(fp.owner);
+        }
+
+        List<Organization> orgs = new ArrayList<>();
+        for (String owner: owners) {
+            Organization org = Organization.findByName(owner);
+            if (org != null) {
+                orgs.add(org);
+            }
+        }
+        return orgs;
+    }
+
     /**
      * As resource.
      *
app/models/Project.java
--- app/models/Project.java
+++ app/models/Project.java
@@ -176,6 +176,10 @@
         }
     }
 
+    public static List<Project> findByOwner(String loginId) {
+        return find.where().ieq("owner", decodeUrlString(loginId)).orderBy("name asc").findList();
+    }
+
     public Set<User> findAuthors() {
         Set<User> allAuthors = new LinkedHashSet<>();
         allAuthors.addAll(getIssueUsers());
app/models/RecentProject.java
--- app/models/RecentProject.java
+++ app/models/RecentProject.java
@@ -15,7 +15,7 @@
 @Table(uniqueConstraints = @UniqueConstraint(columnNames = {"user_id", "project_id"}))
 public class RecentProject extends Model {
     private static final long serialVersionUID = 7306890271871188281L;
-    public static int MAX_RECENT_LIST_PER_USER = 20;
+    public static int MAX_RECENT_LIST_PER_USER = 30;
 
     public static Finder<Long, RecentProject> find = new Finder<>(Long.class, RecentProject.class);
 
app/models/User.java
--- app/models/User.java
+++ app/models/User.java
@@ -249,6 +249,10 @@
         return Project.findProjectsByMemberWithFilter(id, orderString);
     }
 
+    public List<Project> ownProjects() {
+        return Project.findByOwner(loginId);
+    }
+
     /**
      * Create a user and set creation date
      *
@@ -899,10 +903,24 @@
     }
 
     public List<Project> getFavoriteProjects() {
+        final User user = this;
+        favoriteProjects.sort(new Comparator<FavoriteProject>() {
+            @Override
+            public int compare(FavoriteProject o1, FavoriteProject o2) {
+                if (o1.owner.equals(user.loginId) || o2.owner.equals(user.loginId)) {
+                    return Integer.MIN_VALUE;
+                }
+                if (o1.owner.equals(o2.owner)) {
+                    return o1.projectName.compareToIgnoreCase(o2.projectName);
+                }
+                return o1.owner.compareToIgnoreCase(o2.owner);
+            }
+        });
+
         List<Project> projects = new ArrayList<>();
         for (FavoriteProject favoriteProject : this.favoriteProjects) {
             favoriteProject.project.refresh();
-            projects.add(0, favoriteProject.project);
+            projects.add(favoriteProject.project);
         }
 
         return projects;
app/views/common/usermenu.scala.html
--- app/views/common/usermenu.scala.html
+++ app/views/common/usermenu.scala.html
@@ -51,14 +51,19 @@
             }
         </div>
         <ul class="nav nav-tabs nm">
-            <li class="myProjectList active">
+            <li class="myOrganizationList active">
+                <a href="#myOrganizationList" data-toggle="tab">
+                @Messages("title.favorite")
+                </a>
+            </li>
+            <li class="myProjectList">
                 <a href="#myProjectList" data-toggle="tab">
                 @Messages("title.project")
                 </a>
             </li>
-            <li class="myOrganizationList">
-                <a href="#myOrganizationList" data-toggle="tab">
-                @Messages("title.organization")
+            <li class="allProjectList">
+                <a href="#allProjectList" data-toggle="tab">
+                @Messages("common.order.all")
                 </a>
             </li>
         </ul>
app/views/common/usermenu_tab_content_list.scala.html
--- app/views/common/usermenu_tab_content_list.scala.html
+++ app/views/common/usermenu_tab_content_list.scala.html
@@ -4,9 +4,13 @@
 * Copyright Yona & Yobi Authors & NAVER Corp.
 * https://yona.io
 **@
-<div class="tab-pane user-project-list active" id="myProjectList">
-@views.html.index.myProjectList(UserApp.currentUser())
-</div>
-<div class="tab-pane user-project-list" id="myOrganizationList">
+<div class="tab-pane user-project-list active" id="myOrganizationList">
 @views.html.index.myOrganizationList(UserApp.currentUser())
 </div>
+<div class="tab-pane user-project-list" id="myProjectList">
+@views.html.index.myProjectList(UserApp.currentUser())
+</div>
+<div class="tab-pane user-project-list" id="allProjectList">
+@views.html.index.allProjectList(UserApp.currentUser())
+</div>
+
 
app/views/index/allOrganizationList.scala.html (added)
+++ app/views/index/allOrganizationList.scala.html
@@ -0,0 +1,32 @@
+@**
+* Yona, 21st Century Project Hosting SW
+*
+* Copyright Yona & Yobi Authors & NAVER Corp.
+* https://yona.io
+**@
+@(currentUser:User)
+@import utils.TemplateHelper._
+
+@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 <- organizations) {
+                @if(favoredOrganizations.last.equals(organization)){
+                    @myOrganizationList_partial(organization, true, true)
+                } else {
+                    @myOrganizationList_partial(organization, true)
+                }
+            }
+        </ul>
+    }
+}
+
+    <div class="search-result">
+        <div class="group">
+            <input class="search-input org-search" type="text" autocomplete="off" placeholder="@Messages("title.type.name")">
+            <span class="bar"></span>
+        </div>
+        @displayOrganizations("organizations", Organization.findOrganizationsByUserLoginId(UserApp.currentUser.loginId), currentUser.getFavoriteOrganizations)
+    </div>
 
app/views/index/allOrganizationList_partial.scala.html (added)
+++ app/views/index/allOrganizationList_partial.scala.html
@@ -0,0 +1,36 @@
+@**
+* Yona, 21st Century Project Hosting SW
+*
+* Copyright Yona & Yobi Authors & NAVER Corp.
+* https://yona.io
+**@
+@(organization: Organization, favored:Boolean, isLast:Boolean = false)
+@import utils.TemplateHelper._
+
+@isAllowShowCount() = @{
+    UserApp.currentUser().isSiteManager || UserApp.currentUser().isAdminOf(organization)
+}
+
+@defining(UserApp.currentUser().getFavoriteProjects){ favoriteProjects =>
+<li class="@if(isLast){favored} org-li">
+    <div class="org-list project-flex-container all-orgs">
+        <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 all-org-names flex-item">
+                <div class="project-name org-name flex-item"><a href="@routes.OrganizationApp.organization(organization.name)" target="_blank">@organization.name</a></div>
+                <div class="project-owner flex-item">@if(isAllowShowCount){@organization.projects.size()}</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>
+    <ul class="project-ul">
+        @for(project <- organization.projects){
+            @allProjectList_partial(project, favoriteProjects.contains(project))
+        }
+    </ul>
+</li>
+}
 
app/views/index/allProjectList.scala.html (added)
+++ app/views/index/allProjectList.scala.html
@@ -0,0 +1,28 @@
+@**
+* Yona, 21st Century Project Hosting SW
+*
+* Copyright Yona & Yobi Authors & NAVER Corp.
+* https://yona.io
+**@
+@(currentUser:User)
+@import utils.TemplateHelper._
+
+@displayOrganizations(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) {
+            @allOrganizationList_partial(organization, UserApp.currentUser().getFavoriteOrganizations.contains(organization))
+        }
+    </ul>
+}
+}
+
+<div class="search-result">
+    <div class="group">
+        <input class="search-input org-search" type="text" autocomplete="off" placeholder="@Messages("title.type.name")">
+        <span class="bar"></span>
+    </div>
+    @displayOrganizations("organizations", Organization.findAllOrganizations())
+</div>
 
app/views/index/allProjectList_partial.scala.html (added)
+++ app/views/index/allProjectList_partial.scala.html
@@ -0,0 +1,29 @@
+@**
+* Yona, 21st Century Project Hosting SW
+*
+* Copyright Yona & Yobi Authors & NAVER Corp.
+* https://yona.io
+**@
+@import utils.AccessControl
+@import models.enumeration.Operation
+@(project:Project, favored:Boolean, isLast:Boolean = false)
+@import utils.TemplateHelper._
+
+
+@if(AccessControl.isAllowed(UserApp.currentUser(), project.asResource(), Operation.READ)){
+<li class="user-li @if(isLast){favored} @if(favored){ show-always } else { hide }" data-location="@routes.ProjectApp.goConventionMenu(project.owner, project.name)">
+    <div class="project-list project-flex-container">
+        <div class="project-item project-item-container">
+            <div class="flex-item site-logo all-project-names">
+                <i class="project-avatar">@if(hasProjectLogo(project)){<img class="logo" src="@urlToProjectLogo(project)">}else{<span class="dummy-25px"> </span>}</i>
+            </div>
+            <div class="projectName-owner flex-item">
+                <div class="project-name flex-item">@project.name @if(project.isPrivate){<i class="yobicon-lock yobicon-small"></i>}</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/index/displayProjects.scala.html (added)
+++ app/views/index/displayProjects.scala.html
@@ -0,0 +1,17 @@
+@**
+* Yona, 21st Century Project Hosting SW
+*
+* Copyright Yona & Yobi Authors & NAVER Corp.
+* https://yona.io
+**@
+@(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){
+            @views.html.index.myProjectList_partial(project, false)
+        }
+    </ul>
+}
 
app/views/index/displayProjectsWithFavored.scala.html (deleted)
--- app/views/index/displayProjectsWithFavored.scala.html
@@ -1,26 +0,0 @@
-@**
-* Yona, 21st Century Project Hosting SW
-*
-* Copyright Yona & Yobi Authors & NAVER Corp.
-* https://yona.io
-**@
-@(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){
-            @if(favoredProjects.last.equals(project)){
-                @views.html.index.myProjectList_partial(project, true, true)
-            } else {
-                @views.html.index.myProjectList_partial(project, true)
-            }
-        }
-        @for(project <- projects){
-            @if(!favoredProjects.contains(project)){
-                @views.html.index.myProjectList_partial(project, false)
-            }
-        }
-    </ul>
-}
app/views/index/myOrganizationList.scala.html
--- app/views/index/myOrganizationList.scala.html
+++ app/views/index/myOrganizationList.scala.html
@@ -1,7 +1,7 @@
 @**
 * Yona, 21st Century Project Hosting SW
 *
-* Copyright Yona & Yobi Authors & NAVER Corp.
+* Copyright Yona & Yobi Authors & NAVER Corp. & NAVER LABS Corp.
 * https://yona.io
 **@
 @(currentUser:User)
@@ -12,6 +12,27 @@
         <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">
+            @defining(UserApp.currentUser().ownProjects){ ownProjects =>
+                <li class="org-li">
+                    <div class="org-list project-flex-container all-orgs">
+                        <div class="project-item project-item-container">
+                            <div class="flex-item site-logo">
+                                <i class="project-avatar"></i>
+                            </div>
+                            <div class="projectName-owner all-org-names flex-item">
+                                <div class="project-name org-name flex-item">@UserApp.currentUser().loginId</div>
+                                <div class="project-owner flex-item">@ownProjects.size()</div>
+                            </div>
+                        </div>
+                        <div class="star-org flex-item"></div>
+                    </div>
+                    <ul class="project-ul">
+                    @for(project <- ownProjects){
+                        @allProjectList_partial(project, FavoriteProject.findByProjectId(UserApp.currentUser().id, project.id) != null)
+                    }
+                    </ul>
+                </li>
+            }
             @for(organization <- favoredOrganizations) {
                 @if(favoredOrganizations.last.equals(organization)){
                     @myOrganizationList_partial(organization, true, true)
@@ -28,10 +49,11 @@
     }
 }
 
+
     <div class="search-result">
         <div class="group">
             <input class="search-input org-search" type="text" autocomplete="off" placeholder="@Messages("title.type.name")">
             <span class="bar"></span>
         </div>
-        @displayOrganizations("organizations", Organization.findOrganizationsByUserLoginId(UserApp.currentUser.loginId), currentUser.getFavoriteOrganizations)
+        @displayOrganizations("organizations", Organization.findAllOrganizations(UserApp.currentUser.loginId), currentUser.getFavoriteOrganizations)
     </div>
app/views/index/myOrganizationList_partial.scala.html
--- app/views/index/myOrganizationList_partial.scala.html
+++ app/views/index/myOrganizationList_partial.scala.html
@@ -7,19 +7,30 @@
 @(organization: Organization, favored:Boolean, isLast:Boolean = false)
 @import utils.TemplateHelper._
 
-<li class="user-li @if(isLast){favored}" data-location="@routes.OrganizationApp.organization(organization.name)">
-    <div class="org-list project-flex-container">
+@isAllowShowCount() = @{
+    UserApp.currentUser().isSiteManager || UserApp.currentUser().isAdminOf(organization)
+}
+
+@defining(UserApp.currentUser().getFavoriteProjects){ favoriteProjects =>
+<li class="org-li @if(isLast){favored}">
+    <div class="org-list project-flex-container all-orgs">
         <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">@organization.name</div>
-                <div class="project-owner flex-item"></div>
+            <div class="projectName-owner all-org-names flex-item">
+                <div class="project-name org-name flex-item"><a href="@routes.OrganizationApp.organization(organization.name)" target="_blank">@organization.name</a></div>
+                <div class="project-owner flex-item">@if(isAllowShowCount){@organization.projects.size()}</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>
+    <ul class="project-ul">
+    @for(project <- organization.getSortedByProjectName){
+        @allProjectList_partial(project, favoriteProjects.contains(project))
+    }
+    </ul>
 </li>
+}
 
app/views/index/myOwnProjectList_partial.scala.html (added)
+++ app/views/index/myOwnProjectList_partial.scala.html
@@ -0,0 +1,31 @@
+@**
+* Yona, 21st Century Project Hosting SW
+*
+* Copyright Yona & Yobi Authors & NAVER Corp.
+* https://yona.io
+**@
+@(organization: Organization, favored:Boolean, isLast:Boolean = false)
+@import utils.TemplateHelper._
+
+@defining(UserApp.currentUser().ownProjects){ ownProjects =>
+<li class="org-li @if(isLast){favored}">
+    <div class="org-list project-flex-container all-orgs">
+        <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 all-org-names flex-item">
+                <div class="project-name org-name flex-item">@UserApp.currentUser().loginId</div>
+                <div class="project-owner flex-item">@ownProjects.size()</div>
+            </div>
+        </div>
+        <div class="star-org flex-item">
+        </div>
+    </div>
+    <ul class="project-ul">
+    @for(project <- ownProjects){
+        @allProjectList_partial(project, UserApp.currentUser().favoriteProjects.contains(project))
+    }
+    </ul>
+</li>
+}
app/views/index/myProjectList.scala.html
--- app/views/index/myProjectList.scala.html
+++ app/views/index/myProjectList.scala.html
@@ -20,7 +20,7 @@
 }
         <div>
             <div class="search-result">
-                <div class="tab-pane myproject-list-wrap active" >
+                <div class="tab-pane myproject-list-wrap" >
                     <div class="group">
                         <input class="search-input project-search" type="text" id="query" autocomplete="off" placeholder="@Messages("title.type.name")">
                         <span class="bar"></span>
@@ -50,7 +50,7 @@
                         </ul>
                     </div>
                     <div class="tab-content">
-                        @views.html.index.displayProjectsWithFavored("recentlyVisited", currentUser.getVisitedProjects, currentUser.getFavoriteProjects, true)
+                        @views.html.index.displayProjects("recentlyVisited", currentUser.getVisitedProjects, true)
                         @displayProjects("watching", currentUser.getWatchingProjects("name ASC"))
                         @displayProjects("createdByMe", Project.findProjectsCreatedByUser(currentUser.loginId, "createdDate desc"))
                         @displayProjects("joinmember", Project.findProjectsJustMemberAndNotOwner(currentUser, "name ASC"))
conf/messages
--- conf/messages
+++ conf/messages
@@ -927,6 +927,7 @@
 title.editIssue = Edit issue
 title.editMilestone = Edit milestone
 title.editPullRequest = Edit pull request
+title.favorite = Favorite
 title.features = Key features
 title.forgotpassword = Password forgotten?
 title.gettingStarted = Getting your new project started with <strong>{0}</strong>
conf/messages.ko-KR
--- conf/messages.ko-KR
+++ conf/messages.ko-KR
@@ -928,6 +928,7 @@
 title.editIssue = 이슈 수정
 title.editMilestone = 마일스톤 수정
 title.editPullRequest = 코드 보내기 수정
+title.favorite = 즐겨찾기
 title.features = 주요 기능 소개
 title.forgotpassword = 비밀번호를 잊어버리셨나요?
 title.gettingStarted = <strong>{0}</strong> 와 함께 새로운 프로젝트를 시작해 보세요.
public/javascripts/common/yona.Usermenu.js
--- public/javascripts/common/yona.Usermenu.js
+++ public/javascripts/common/yona.Usermenu.js
@@ -91,11 +91,17 @@
         });
 
         // search by keyword
-        $(".search-input").on("keyup", function() {
-            var value = $(this).val().toLowerCase().trim();
-            $(".user-li").each(function() {
-                $(this).toggle($(this).text().toLowerCase().indexOf(value) !== -1);
-            });
+            $(".search-input").on("keyup", function(event) {
+                var value = $(this).val().toLowerCase().trim();
+
+                if ( value !== "" || event.which === 8) {  // 8: backspace
+                    $(".user-li").each(function() {
+                        $(this).toggle($(this).text().toLowerCase().indexOf(value) !== -1);
+                    });
+                    $(".org-li").each(function() {
+                        $(this).toggle($(this).text().toLowerCase().indexOf(value) !== -1);
+                    });
+                }
         }).on("keydown.moveCursorFromInputform", function(e) {
             switch (e.keyCode) {
                 case 27:   // ESC
@@ -162,12 +168,14 @@
             }
         }
 
-        $(".user-ul > .user-li").on("click", function (e) {
+        $(".user-ul > .user-li, .project-ul > .user-li").on("click", function (e) {
+            e.preventDefault();
+            e.stopPropagation();
             var location = $(this).data('location');
-            if(e.metaKey || e.ctrlKey) {
-                window.open(location, '_blank');
-            } else {
+            if(e.metaKey || e.ctrlKey || e.shiftKey) {
                 window.location = location;
+            } else {
+                window.open(location, '_blank');
             }
         });
 
@@ -201,5 +209,9 @@
                     });
                 });
         }
+
+        $(".all-orgs").on("click", function () {
+            var $li = $(this).closest("li").find(".hide").toggle("fast");
+        });
     }
 });
Add a comment
List