THIS IS A TEST INSTANCE ONLY! REPOSITORIES CAN BE DELETED AT ANY TIME!

Browse Source

usermenu: Recategorize user project menu

tags/v1.11.0
Suwon Chae 1 year ago
parent
commit
7a5bfd5805
20 changed files with 343 additions and 54 deletions
  1. +21
    -1
      app/assets/stylesheets/less/_usermenu.less
  2. +44
    -0
      app/models/Organization.java
  3. +4
    -0
      app/models/Project.java
  4. +1
    -1
      app/models/RecentProject.java
  5. +19
    -1
      app/models/User.java
  6. +9
    -4
      app/views/common/usermenu.scala.html
  7. +7
    -3
      app/views/common/usermenu_tab_content_list.scala.html
  8. +32
    -0
      app/views/index/allOrganizationList.scala.html
  9. +36
    -0
      app/views/index/allOrganizationList_partial.scala.html
  10. +28
    -0
      app/views/index/allProjectList.scala.html
  11. +29
    -0
      app/views/index/allProjectList_partial.scala.html
  12. +17
    -0
      app/views/index/displayProjects.scala.html
  13. +0
    -26
      app/views/index/displayProjectsWithFavored.scala.html
  14. +24
    -2
      app/views/index/myOrganizationList.scala.html
  15. +16
    -5
      app/views/index/myOrganizationList_partial.scala.html
  16. +31
    -0
      app/views/index/myOwnProjectList_partial.scala.html
  17. +2
    -2
      app/views/index/myProjectList.scala.html
  18. +1
    -0
      conf/messages
  19. +1
    -0
      conf/messages.ko-KR
  20. +21
    -9
      public/javascripts/common/yona.Usermenu.js

+ 21
- 1
app/assets/stylesheets/less/_usermenu.less View File

@@ -60,7 +60,7 @@
}

li {
border-bottom: 1px solid #eee;
border-bottom: 1px solid transparent;
margin-left: 0;
}

@@ -450,18 +450,23 @@
.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;
@@ -470,6 +475,20 @@
}
}

.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;
}
}

.project-owner {
color: grey;
padding-right: 10px;
@@ -564,6 +583,7 @@

.project-list, .org-list {
&:hover {
cursor: pointer;
}
}



+ 44
- 0
app/models/Organization.java View File

@@ -92,6 +92,16 @@ public class Organization extends Model implements ResourceConvertible {
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 @@ public class Organization extends Model implements ResourceConvertible {
.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.
*


+ 4
- 0
app/models/Project.java View File

@@ -176,6 +176,10 @@ public class Project extends Model implements LabelOwner {
}
}

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());


+ 1
- 1
app/models/RecentProject.java View File

@@ -15,7 +15,7 @@ import java.util.List;
@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);



+ 19
- 1
app/models/User.java View File

@@ -249,6 +249,10 @@ public class User extends Model implements ResourceConvertible {
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 class User extends Model implements ResourceConvertible {
}

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;


+ 9
- 4
app/views/common/usermenu.scala.html View File

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


+ 7
- 3
app/views/common/usermenu_tab_content_list.scala.html View File

@@ -4,9 +4,13 @@
* Copyright Yona & Yobi Authors & NAVER Corp.
* https://yona.io
**@
<div class="tab-pane user-project-list active" id="myProjectList">
<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="myOrganizationList">
@views.html.index.myOrganizationList(UserApp.currentUser())
<div class="tab-pane user-project-list" id="allProjectList">
@views.html.index.allProjectList(UserApp.currentUser())
</div>


+ 32
- 0
app/views/index/allOrganizationList.scala.html View File

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

+ 36
- 0
app/views/index/allOrganizationList_partial.scala.html View File

@@ -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>
}

+ 28
- 0
app/views/index/allProjectList.scala.html View File

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

+ 29
- 0
app/views/index/allProjectList_partial.scala.html View File

@@ -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>
}

+ 17
- 0
app/views/index/displayProjects.scala.html View File

@@ -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>
}

+ 0
- 26
app/views/index/displayProjectsWithFavored.scala.html View File

@@ -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>
}

+ 24
- 2
app/views/index/myOrganizationList.scala.html View File

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

+ 16
- 5
app/views/index/myOrganizationList_partial.scala.html View File

@@ -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>
}

+ 31
- 0
app/views/index/myOwnProjectList_partial.scala.html View File

@@ -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>
}

+ 2
- 2
app/views/index/myProjectList.scala.html View File

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


+ 1
- 0
conf/messages View File

@@ -927,6 +927,7 @@ title.createdByMe = Create
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>


+ 1
- 0
conf/messages.ko-KR View File

@@ -928,6 +928,7 @@ title.createdByMe = 내가 만든
title.editIssue = 이슈 수정
title.editMilestone = 마일스톤 수정
title.editPullRequest = 코드 보내기 수정
title.favorite = 즐겨찾기
title.features = 주요 기능 소개
title.forgotpassword = 비밀번호를 잊어버리셨나요?
title.gettingStarted = <strong>{0}</strong> 와 함께 새로운 프로젝트를 시작해 보세요.


+ 21
- 9
public/javascripts/common/yona.Usermenu.js View File

@@ -91,11 +91,17 @@ $(function() {
});

// 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 @@ $(function() {
}
}

$(".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 @@ $(function() {
});
});
}

$(".all-orgs").on("click", function () {
var $li = $(this).closest("li").find(".hide").toggle("fast");
});
}
});

Loading…
Cancel
Save