From 640db8fe93b7ae2bdbe952505a5102b84c92ed22 Mon Sep 17 00:00:00 2001 From: Benjamin Date: Sun, 28 Jun 2026 02:52:42 +0200 Subject: [PATCH 1/7] feat: add favorites for projects and tasks via 3-dots menu Reuse the platform favorites framework: enrich Task/Project REST responses with a per-viewer favorite flag (FavoriteService) and add a site-wide favorite-extensions bundle that registers project/task in the top-bar Favorites drawer and adds a favorite-button toggle to the project card and task drawer 3-dots menus. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../org/exoplatform/task/dto/TaskDto.java | 5 +- .../task/rest/ProjectRestService.java | 28 ++++- .../task/rest/TaskRestService.java | 31 ++++- .../task/rest/TestProjectRestService.java | 41 ++++-- .../task/rest/TestTaskRestService.java | 84 ++++++++++--- .../main/webapp/WEB-INF/gatein-resources.xml | 20 +++ .../components/ProjectFavoriteItem.vue | 117 ++++++++++++++++++ .../components/ProjectFavoriteMenuAction.vue | 81 ++++++++++++ .../components/TaskFavoriteItem.vue | 117 ++++++++++++++++++ .../components/TaskFavoriteMenuItem.vue | 90 ++++++++++++++ .../vue-app/favorite-extensions/extensions.js | 60 +++++++++ .../favorite-extensions/initComponents.js | 33 +++++ .../vue-app/favorite-extensions/main.js | 36 ++++++ webapps/webpack.common.js | 1 + 14 files changed, 710 insertions(+), 34 deletions(-) create mode 100644 webapps/src/main/webapp/vue-app/favorite-extensions/components/ProjectFavoriteItem.vue create mode 100644 webapps/src/main/webapp/vue-app/favorite-extensions/components/ProjectFavoriteMenuAction.vue create mode 100644 webapps/src/main/webapp/vue-app/favorite-extensions/components/TaskFavoriteItem.vue create mode 100644 webapps/src/main/webapp/vue-app/favorite-extensions/components/TaskFavoriteMenuItem.vue create mode 100644 webapps/src/main/webapp/vue-app/favorite-extensions/extensions.js create mode 100644 webapps/src/main/webapp/vue-app/favorite-extensions/initComponents.js create mode 100644 webapps/src/main/webapp/vue-app/favorite-extensions/main.js diff --git a/services/src/main/java/org/exoplatform/task/dto/TaskDto.java b/services/src/main/java/org/exoplatform/task/dto/TaskDto.java index 6331b68eb..3dc8f9b50 100644 --- a/services/src/main/java/org/exoplatform/task/dto/TaskDto.java +++ b/services/src/main/java/org/exoplatform/task/dto/TaskDto.java @@ -70,6 +70,8 @@ public class TaskDto implements Serializable { private String activityId; + private boolean favorite; + public TaskDto clone() { // NOSONAR return new TaskDto(id, title, @@ -87,7 +89,8 @@ public TaskDto clone() { // NOSONAR startDate, endDate, dueDate, - activityId); + activityId, + favorite); } } diff --git a/services/src/main/java/org/exoplatform/task/rest/ProjectRestService.java b/services/src/main/java/org/exoplatform/task/rest/ProjectRestService.java index b752a23e3..b7f9224f4 100644 --- a/services/src/main/java/org/exoplatform/task/rest/ProjectRestService.java +++ b/services/src/main/java/org/exoplatform/task/rest/ProjectRestService.java @@ -39,6 +39,8 @@ import org.exoplatform.social.core.space.model.Space; import org.exoplatform.social.core.space.spi.SpaceService; import org.exoplatform.social.core.identity.SpaceMemberFilterListAccess.Type; +import org.exoplatform.social.metadata.favorite.FavoriteService; +import org.exoplatform.social.metadata.favorite.model.Favorite; import org.exoplatform.task.dao.OrderBy; import org.exoplatform.task.dao.ProjectQuery; import org.exoplatform.task.dto.ProjectDto; @@ -49,6 +51,9 @@ import org.exoplatform.task.service.*; import org.exoplatform.task.util.*; import org.gatein.common.text.EntityEncoder; + +import io.meeds.task.plugin.ProjectPermanentLinkPlugin; + import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; @@ -82,6 +87,8 @@ public class ProjectRestService implements ResourceContainer { private IdentityManager identityManager; + private FavoriteService favoriteService; + public ProjectRestService(TaskService taskService, CommentService commentService, ProjectService projectService, @@ -89,7 +96,8 @@ public ProjectRestService(TaskService taskService, UserService userService, SpaceService spaceService, LabelService labelService, - IdentityManager identityManager) { + IdentityManager identityManager, + FavoriteService favoriteService) { this.taskService = taskService; this.commentService = commentService; this.projectService = projectService; @@ -98,6 +106,23 @@ public ProjectRestService(TaskService taskService, this.spaceService = spaceService; this.labelService = labelService; this.identityManager = identityManager; + this.favoriteService = favoriteService; + } + + private boolean isFavorite(String objectType, long objectId) { + Identity identity = ConversationState.getCurrent().getIdentity(); + if (identity == null) { + return false; + } + org.exoplatform.social.core.identity.model.Identity userIdentity = + identityManager.getOrCreateUserIdentity(identity.getUserId()); + if (userIdentity == null) { + return false; + } + return favoriteService.isFavorite(new Favorite(objectType, + String.valueOf(objectId), + null, + Long.parseLong(userIdentity.getId()))); } private enum TaskType { @@ -486,6 +511,7 @@ private JSONObject buildJsonProject(ProjectDto project, boolean participatorPara projectJson.put("description", project.getDescription()); // projectJson.put("status", statusService.getStatus(projectId)); projectJson.put("canManage", project.canEdit(ConversationState.getCurrent().getIdentity())); + projectJson.put("favorite", isFavorite(ProjectPermanentLinkPlugin.OBJECT_TYPE, projectId)); return projectJson; } diff --git a/services/src/main/java/org/exoplatform/task/rest/TaskRestService.java b/services/src/main/java/org/exoplatform/task/rest/TaskRestService.java index 3015790c3..d35b969b6 100644 --- a/services/src/main/java/org/exoplatform/task/rest/TaskRestService.java +++ b/services/src/main/java/org/exoplatform/task/rest/TaskRestService.java @@ -53,8 +53,11 @@ import org.exoplatform.services.rest.resource.ResourceContainer; import org.exoplatform.services.security.ConversationState; import org.exoplatform.services.security.Identity; +import org.exoplatform.social.core.manager.IdentityManager; import org.exoplatform.social.core.space.model.Space; import org.exoplatform.social.core.space.spi.SpaceService; +import org.exoplatform.social.metadata.favorite.FavoriteService; +import org.exoplatform.social.metadata.favorite.model.Favorite; import org.exoplatform.task.dao.TaskQuery; import org.exoplatform.task.dto.ChangeLogEntry; import org.exoplatform.task.dto.CommentDto; @@ -83,6 +86,7 @@ import io.meeds.social.html.model.HtmlTransformerContext; import io.meeds.social.html.utils.HtmlUtils; +import io.meeds.task.plugin.TaskPermanentLinkPlugin; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; @@ -115,6 +119,10 @@ public class TaskRestService implements ResourceContainer { private LabelService labelService; + private FavoriteService favoriteService; + + private IdentityManager identityManager; + private static final String PERCENT_ENCODED_REGEX = "%(?![0-9a-fA-F]{2})"; public TaskRestService(TaskService taskService, @@ -123,7 +131,9 @@ public TaskRestService(TaskService taskService, StatusService statusService, UserService userService, SpaceService spaceService, - LabelService labelService) { + LabelService labelService, + FavoriteService favoriteService, + IdentityManager identityManager) { this.taskService = taskService; this.commentService = commentService; this.projectService = projectService; @@ -131,6 +141,8 @@ public TaskRestService(TaskService taskService, this.userService = userService; this.spaceService = spaceService; this.labelService = labelService; + this.favoriteService = favoriteService; + this.identityManager = identityManager; } @@ -138,6 +150,22 @@ private enum TaskType { ALL, INCOMING, OVERDUE, WATCHED, COLLABORATED, ASSIGNED } + private boolean isFavorite(String objectType, long objectId) { + Identity identity = ConversationState.getCurrent().getIdentity(); + if (identity == null) { + return false; + } + org.exoplatform.social.core.identity.model.Identity userIdentity = + identityManager.getOrCreateUserIdentity(identity.getUserId()); + if (userIdentity == null) { + return false; + } + return favoriteService.isFavorite(new Favorite(objectType, + String.valueOf(objectId), + null, + Long.parseLong(userIdentity.getId()))); + } + @GET @@ -160,6 +188,7 @@ public Response getTaskById(@Parameter(description = "Task id", required = true) return Response.status(Response.Status.FORBIDDEN).build(); } transformHtml(task, ConversationState.getCurrent().getIdentity()); + task.setFavorite(isFavorite(TaskPermanentLinkPlugin.OBJECT_TYPE, task.getId())); return Response.ok(task).build(); } catch (Exception e) { LOG.error("Can't get Task By Id {}", id, e); diff --git a/services/src/test/java/org/exoplatform/task/rest/TestProjectRestService.java b/services/src/test/java/org/exoplatform/task/rest/TestProjectRestService.java index 47de8815b..ffb542d9d 100644 --- a/services/src/test/java/org/exoplatform/task/rest/TestProjectRestService.java +++ b/services/src/test/java/org/exoplatform/task/rest/TestProjectRestService.java @@ -48,6 +48,7 @@ import org.exoplatform.services.security.ConversationState; import org.exoplatform.services.security.Identity; import org.exoplatform.social.core.manager.IdentityManager; +import org.exoplatform.social.metadata.favorite.FavoriteService; import org.exoplatform.social.core.space.SpaceUtils; import org.exoplatform.social.core.space.model.Space; import org.exoplatform.social.core.space.spi.SpaceService; @@ -99,6 +100,9 @@ public class TestProjectRestService { @Mock IdentityManager identityManager; + @Mock + FavoriteService favoriteService; + @Before public void setup() { RuntimeDelegate.setInstance(new RuntimeDelegateImpl()); @@ -113,7 +117,9 @@ public void testGetTasks() throws Exception { statusService, userService, spaceService, - labelService); + labelService, + favoriteService, + identityManager); Identity root = new Identity("root"); ConversationState.setCurrent(new ConversationState(root)); TaskDto task1 = new TaskDto(); @@ -184,7 +190,8 @@ public void testGetProjects() throws Exception { userService, spaceService, labelService, - identityManager); + identityManager, + favoriteService); Identity root = new Identity("root"); ConversationState.setCurrent(new ConversationState(root)); @@ -239,7 +246,8 @@ public void testGetDefaultStatusByProjectId() throws Exception { userService, spaceService, labelService, - identityManager); + identityManager, + favoriteService); Identity john = new Identity("john"); ConversationState.setCurrent(new ConversationState(john)); ProjectDto project = new ProjectDto(); @@ -267,7 +275,8 @@ public void testGetProjectById() throws Exception { userService, spaceService, labelService, - identityManager); + identityManager, + favoriteService); Identity john = new Identity("john"); ConversationState.setCurrent(new ConversationState(john)); ProjectDto project = new ProjectDto(); @@ -305,7 +314,8 @@ public void testCreateProject() throws Exception { userService, spaceService, labelService, - identityManager); + identityManager, + favoriteService); Identity john = new Identity("john"); ConversationState.setCurrent(new ConversationState(john)); Set manager = new HashSet(); @@ -369,7 +379,8 @@ public void testUpdateProject() throws Exception { userService, spaceService, labelService, - identityManager); + identityManager, + favoriteService); Identity john = new Identity("john"); Identity exo = new Identity("exo"); ConversationState.setCurrent(new ConversationState(john)); @@ -415,7 +426,8 @@ public void testGetSatusesByProjectId() throws Exception { userService, spaceService, labelService, - identityManager); + identityManager, + favoriteService); Identity root = new Identity("root"); ConversationState.setCurrent(new ConversationState(root)); @@ -488,7 +500,8 @@ public void testFindUsersToMention() throws Exception { userService, spaceService, labelService, - identityManager); + identityManager, + favoriteService); Identity root = new Identity("root"); ConversationState.setCurrent(new ConversationState(root)); @@ -526,7 +539,8 @@ public void testFindUsersToMentionInTasksAffectedToProject() throws Exception { userService, spaceService, labelService, - identityManager); + identityManager, + favoriteService); Identity root = new Identity("root"); final User userA = TestUtils.getUserA(); @@ -562,7 +576,8 @@ public void testDeleteProject() throws Exception { userService, spaceService, labelService, - identityManager); + identityManager, + favoriteService); Identity john = new Identity("john"); ConversationState.setCurrent(new ConversationState(john)); Set manager = new HashSet(); @@ -589,7 +604,8 @@ public void testCloneProject() throws Exception { userService, spaceService, labelService, - identityManager); + identityManager, + favoriteService); Identity john = new Identity("john"); ConversationState.setCurrent(new ConversationState(john)); Set manager = new HashSet(); @@ -623,7 +639,8 @@ public void testChangeProjectColor() throws Exception { userService, spaceService, labelService, - identityManager); + identityManager, + favoriteService); Identity john = new Identity("john"); ConversationState.setCurrent(new ConversationState(john)); Set manager = new HashSet(); diff --git a/services/src/test/java/org/exoplatform/task/rest/TestTaskRestService.java b/services/src/test/java/org/exoplatform/task/rest/TestTaskRestService.java index 4da00ec76..556540b91 100644 --- a/services/src/test/java/org/exoplatform/task/rest/TestTaskRestService.java +++ b/services/src/test/java/org/exoplatform/task/rest/TestTaskRestService.java @@ -43,7 +43,9 @@ import org.exoplatform.services.rest.impl.RuntimeDelegateImpl; import org.exoplatform.services.security.ConversationState; import org.exoplatform.services.security.Identity; +import org.exoplatform.social.core.manager.IdentityManager; import org.exoplatform.social.core.space.spi.SpaceService; +import org.exoplatform.social.metadata.favorite.FavoriteService; import org.exoplatform.task.dto.*; import org.exoplatform.task.service.UserService; import org.exoplatform.task.rest.model.CommentEntity; @@ -73,6 +75,12 @@ public class TestTaskRestService { @Mock LabelService labelService; + @Mock + FavoriteService favoriteService; + + @Mock + IdentityManager identityManager; + @Before @@ -89,7 +97,9 @@ public void testGetTasks() throws Exception { statusService, userService, spaceService, - labelService); + labelService, + favoriteService, + identityManager); Identity root = new Identity("root"); ConversationState.setCurrent(new ConversationState(root)); TaskDto task1 = new TaskDto(); @@ -155,7 +165,9 @@ public void testGetTasksByProjectId() throws Exception { statusService, userService, spaceService, - labelService); + labelService, + favoriteService, + identityManager); Identity root = new Identity("root"); ConversationState.setCurrent(new ConversationState(root)); TaskDto task1 = new TaskDto(); @@ -196,7 +208,9 @@ public void testGetTaskById() throws Exception { statusService, userService, spaceService, - labelService); + labelService, + favoriteService, + identityManager); Identity root = new Identity("root"); ConversationState.setCurrent(new ConversationState(root)); TaskDto task1 = new TaskDto(); @@ -218,7 +232,9 @@ public void testAddTask() throws Exception { statusService, userService, spaceService, - labelService); + labelService, + favoriteService, + identityManager); Identity john = new Identity("john"); ConversationState.setCurrent(new ConversationState(john)); TaskDto task1 = new TaskDto(); @@ -265,7 +281,9 @@ public void testUpdateTaskById() throws Exception { statusService, userService, spaceService, - labelService); + labelService, + favoriteService, + identityManager); Identity john = new Identity("john"); ConversationState.setCurrent(new ConversationState(john)); TaskDto task1 = new TaskDto(); @@ -314,7 +332,9 @@ public void deleteTaskById() throws Exception { statusService, userService, spaceService, - labelService); + labelService, + favoriteService, + identityManager); Identity john = new Identity("john"); ConversationState.setCurrent(new ConversationState(john)); TaskDto task = new TaskDto(); @@ -337,7 +357,9 @@ public void testGetLabels() throws Exception { statusService, userService, spaceService, - labelService); + labelService, + favoriteService, + identityManager); Identity root = new Identity("root"); ConversationState.setCurrent(new ConversationState(root)); @@ -390,7 +412,9 @@ public void getLabelsByTaskId() throws Exception { statusService, userService, spaceService, - labelService); + labelService, + favoriteService, + identityManager); Identity root = new Identity("root"); ConversationState.setCurrent(new ConversationState(root)); @@ -457,7 +481,9 @@ public void testAddTaskToLabel() throws Exception { statusService, userService, spaceService, - labelService); + labelService, + favoriteService, + identityManager); Identity root = new Identity("root"); ConversationState.setCurrent(new ConversationState(root)); @@ -535,7 +561,9 @@ public void testGetTaskLogs() throws Exception { statusService, userService, spaceService, - labelService); + labelService, + favoriteService, + identityManager); Identity root = new Identity("root"); ConversationState.setCurrent(new ConversationState(root)); @@ -575,7 +603,9 @@ public void testRemoveTaskFromLabel() throws Exception { statusService, userService, spaceService, - labelService); + labelService, + favoriteService, + identityManager); Identity root = new Identity("root"); ConversationState.setCurrent(new ConversationState(root)); ProjectDto project = new ProjectDto(); @@ -638,7 +668,9 @@ public void testAddLabel() throws Exception { statusService, userService, spaceService, - labelService); + labelService, + favoriteService, + identityManager); Identity root = new Identity("root"); ConversationState.setCurrent(new ConversationState(root)); ProjectDto project = new ProjectDto(); @@ -672,7 +704,9 @@ public void testRemoveLabel() throws Exception { statusService, userService, spaceService, - labelService); + labelService, + favoriteService, + identityManager); Identity root = new Identity("root"); ConversationState.setCurrent(new ConversationState(root)); ProjectDto project = new ProjectDto(); @@ -710,7 +744,9 @@ public void testAddTaskComment() throws Exception { statusService, userService, spaceService, - labelService); + labelService, + favoriteService, + identityManager); Identity john = new Identity("john"); ConversationState.setCurrent(new ConversationState(john)); @@ -760,7 +796,9 @@ public void testfindUsersToMention() throws Exception { statusService, userService, spaceService, - labelService); + labelService, + favoriteService, + identityManager); Identity root = new Identity("root"); ConversationState.setCurrent(new ConversationState(root)); @@ -797,7 +835,9 @@ public void testDeleteComment() throws Exception { statusService, userService, spaceService, - labelService); + labelService, + favoriteService, + identityManager); Identity john = new Identity("john"); ConversationState.setCurrent(new ConversationState(john)); @@ -852,7 +892,9 @@ public void testAddTaskSubComment() throws Exception { statusService, userService, spaceService, - labelService); + labelService, + favoriteService, + identityManager); Identity john = new Identity("john"); ConversationState.setCurrent(new ConversationState(john)); @@ -898,7 +940,9 @@ public void testFilterTasks() throws Exception { statusService, userService, spaceService, - labelService); + labelService, + favoriteService, + identityManager); Identity root = new Identity("root"); ConversationState.setCurrent(new ConversationState(root)); TaskDto task1 = new TaskDto(); @@ -959,7 +1003,9 @@ public void testUpdateCompleted() throws Exception { statusService, userService, spaceService, - labelService); + labelService, + favoriteService, + identityManager); Identity root = new Identity("root"); ConversationState.setCurrent(new ConversationState(root)); TaskDto task = new TaskDto(); diff --git a/webapps/src/main/webapp/WEB-INF/gatein-resources.xml b/webapps/src/main/webapp/WEB-INF/gatein-resources.xml index 915a5fe6c..d741b23f5 100644 --- a/webapps/src/main/webapp/WEB-INF/gatein-resources.xml +++ b/webapps/src/main/webapp/WEB-INF/gatein-resources.xml @@ -387,4 +387,24 @@ + + taskFavoriteExtensions + FavoriteDrawerGRP + + + vue + + + eXoVueI18n + + + commonVueComponents + + + extensionRegistry + + + diff --git a/webapps/src/main/webapp/vue-app/favorite-extensions/components/ProjectFavoriteItem.vue b/webapps/src/main/webapp/vue-app/favorite-extensions/components/ProjectFavoriteItem.vue new file mode 100644 index 000000000..40defcace --- /dev/null +++ b/webapps/src/main/webapp/vue-app/favorite-extensions/components/ProjectFavoriteItem.vue @@ -0,0 +1,117 @@ + + + diff --git a/webapps/src/main/webapp/vue-app/favorite-extensions/components/ProjectFavoriteMenuAction.vue b/webapps/src/main/webapp/vue-app/favorite-extensions/components/ProjectFavoriteMenuAction.vue new file mode 100644 index 000000000..a05e939b1 --- /dev/null +++ b/webapps/src/main/webapp/vue-app/favorite-extensions/components/ProjectFavoriteMenuAction.vue @@ -0,0 +1,81 @@ + + + diff --git a/webapps/src/main/webapp/vue-app/favorite-extensions/components/TaskFavoriteItem.vue b/webapps/src/main/webapp/vue-app/favorite-extensions/components/TaskFavoriteItem.vue new file mode 100644 index 000000000..2988031b2 --- /dev/null +++ b/webapps/src/main/webapp/vue-app/favorite-extensions/components/TaskFavoriteItem.vue @@ -0,0 +1,117 @@ + + + diff --git a/webapps/src/main/webapp/vue-app/favorite-extensions/components/TaskFavoriteMenuItem.vue b/webapps/src/main/webapp/vue-app/favorite-extensions/components/TaskFavoriteMenuItem.vue new file mode 100644 index 000000000..1f051b174 --- /dev/null +++ b/webapps/src/main/webapp/vue-app/favorite-extensions/components/TaskFavoriteMenuItem.vue @@ -0,0 +1,90 @@ + + + diff --git a/webapps/src/main/webapp/vue-app/favorite-extensions/extensions.js b/webapps/src/main/webapp/vue-app/favorite-extensions/extensions.js new file mode 100644 index 000000000..f616cfbf7 --- /dev/null +++ b/webapps/src/main/webapp/vue-app/favorite-extensions/extensions.js @@ -0,0 +1,60 @@ +/* + * This file is part of the Meeds project (https://meeds.io/). + * + * Copyright (C) 2020 - 2025 Meeds Association contact@meeds.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +// Register the Project and Task types in the global top-bar Favorites drawer. +// The drawer resolves the section label from i18n key +// `UITopBarFavoritesPortlet.types.` and falls back to `name` when absent. +extensionRegistry.registerExtension('favorite', 'favorite-type', { + rank: 60, + id: 'project', + name: 'Projects', + icon: 'fa-tasks', +}); +extensionRegistry.registerComponent('favorite-project', 'favorite-drawer-item', { + id: 'project', + vueComponent: Vue.options.components['project-favorite-item'], +}); + +extensionRegistry.registerExtension('favorite', 'favorite-type', { + rank: 61, + id: 'task', + name: 'Tasks', + icon: 'fa-check-square', +}); +extensionRegistry.registerComponent('favorite-task', 'favorite-drawer-item', { + id: 'task', + vueComponent: Vue.options.components['task-favorite-item'], +}); + +// Add the favorite toggle inside the project card 3-dots menu +// (consumed by ProjectCardFront.vue via extension-registry-components). +extensionRegistry.registerComponent('TaskProjectMenu', 'task-project-menu', { + id: 'project-favorite', + rank: 5, + vueComponent: Vue.options.components['project-favorite-menu-action'], +}); + +// Add the favorite toggle inside the task drawer 3-dots menu +// (consumed by TaskDrawer.vue via loadExtensions('Task', 'task-menu')). +extensionRegistry.registerExtension('Task', 'task-menu', { + id: 'task-favorite', + rank: 5, + enabled: true, + vueComponent: 'task-favorite-menu-item', +}); diff --git a/webapps/src/main/webapp/vue-app/favorite-extensions/initComponents.js b/webapps/src/main/webapp/vue-app/favorite-extensions/initComponents.js new file mode 100644 index 000000000..3102cbb23 --- /dev/null +++ b/webapps/src/main/webapp/vue-app/favorite-extensions/initComponents.js @@ -0,0 +1,33 @@ +/* + * This file is part of the Meeds project (https://meeds.io/). + * + * Copyright (C) 2020 - 2025 Meeds Association contact@meeds.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import ProjectFavoriteItem from './components/ProjectFavoriteItem.vue'; +import TaskFavoriteItem from './components/TaskFavoriteItem.vue'; +import ProjectFavoriteMenuAction from './components/ProjectFavoriteMenuAction.vue'; +import TaskFavoriteMenuItem from './components/TaskFavoriteMenuItem.vue'; + +const components = { + 'project-favorite-item': ProjectFavoriteItem, + 'task-favorite-item': TaskFavoriteItem, + 'project-favorite-menu-action': ProjectFavoriteMenuAction, + 'task-favorite-menu-item': TaskFavoriteMenuItem, +}; + +for (const key in components) { + Vue.component(key, components[key]); +} diff --git a/webapps/src/main/webapp/vue-app/favorite-extensions/main.js b/webapps/src/main/webapp/vue-app/favorite-extensions/main.js new file mode 100644 index 000000000..b3b7d57aa --- /dev/null +++ b/webapps/src/main/webapp/vue-app/favorite-extensions/main.js @@ -0,0 +1,36 @@ +/* + * This file is part of the Meeds project (https://meeds.io/). + * + * Copyright (C) 2020 - 2025 Meeds Association contact@meeds.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import './initComponents.js'; +import './extensions.js'; + +import * as projectService from '../../js/projectService.js'; +import * as tasksService from '../../js/tasksService.js'; + +// The Favorites drawer items need to resolve project/task titles by id, and they +// are rendered outside the task apps, so expose the services globally if needed. +if (!Vue.prototype.$projectService) { + window.Object.defineProperty(Vue.prototype, '$projectService', { + value: projectService, + }); +} +if (!Vue.prototype.$tasksService) { + window.Object.defineProperty(Vue.prototype, '$tasksService', { + value: tasksService, + }); +} diff --git a/webapps/webpack.common.js b/webapps/webpack.common.js index 1392cff9a..842fcbaee 100644 --- a/webapps/webpack.common.js +++ b/webapps/webpack.common.js @@ -34,6 +34,7 @@ const config = { taskQuickAction: './src/main/webapp/vue-app/quick-actions/main.js', taskContentLinkExtension: './src/main/webapp/vue-app/content-link/extensions.js', restrictedProject: './src/main/webapp/vue-app/restricted-project/main.js', + favoriteExtensions: './src/main/webapp/vue-app/favorite-extensions/main.js', }, output: { filename: 'js/[name].bundle.js', From c53e6044ffd4185569f5b0ff7b9e7bf6ce01e454 Mon Sep 17 00:00:00 2001 From: Benjamin Date: Sun, 28 Jun 2026 03:36:07 +0200 Subject: [PATCH 2/7] fix: load task/project favorite menu extensions in taskDrawerGrp The 3-dots menu extensions must register in the same load-group as the task drawer (taskDrawerGrp), not FavoriteDrawerGRP, so they are present when the drawer/project cards render. Split menu registrations into a dedicated task-favorite-menu bundle; keep drawer-item registrations in favorite-extensions. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../main/webapp/WEB-INF/gatein-resources.xml | 20 +++++++++++ .../vue-app/favorite-extensions/extensions.js | 17 --------- .../favorite-extensions/initComponents.js | 4 --- .../components/ProjectFavoriteMenuAction.vue | 0 .../components/TaskFavoriteMenuItem.vue | 0 .../vue-app/task-favorite-menu/extensions.js | 35 +++++++++++++++++++ .../task-favorite-menu/initComponents.js | 29 +++++++++++++++ .../webapp/vue-app/task-favorite-menu/main.js | 20 +++++++++++ webapps/webpack.common.js | 1 + 9 files changed, 105 insertions(+), 21 deletions(-) rename webapps/src/main/webapp/vue-app/{favorite-extensions => task-favorite-menu}/components/ProjectFavoriteMenuAction.vue (100%) rename webapps/src/main/webapp/vue-app/{favorite-extensions => task-favorite-menu}/components/TaskFavoriteMenuItem.vue (100%) create mode 100644 webapps/src/main/webapp/vue-app/task-favorite-menu/extensions.js create mode 100644 webapps/src/main/webapp/vue-app/task-favorite-menu/initComponents.js create mode 100644 webapps/src/main/webapp/vue-app/task-favorite-menu/main.js diff --git a/webapps/src/main/webapp/WEB-INF/gatein-resources.xml b/webapps/src/main/webapp/WEB-INF/gatein-resources.xml index d741b23f5..e18c3122b 100644 --- a/webapps/src/main/webapp/WEB-INF/gatein-resources.xml +++ b/webapps/src/main/webapp/WEB-INF/gatein-resources.xml @@ -407,4 +407,24 @@ + + taskFavoriteMenuExtension + taskDrawerGrp + + + vue + + + eXoVueI18n + + + commonVueComponents + + + extensionRegistry + + + diff --git a/webapps/src/main/webapp/vue-app/favorite-extensions/extensions.js b/webapps/src/main/webapp/vue-app/favorite-extensions/extensions.js index f616cfbf7..bc257ebb5 100644 --- a/webapps/src/main/webapp/vue-app/favorite-extensions/extensions.js +++ b/webapps/src/main/webapp/vue-app/favorite-extensions/extensions.js @@ -41,20 +41,3 @@ extensionRegistry.registerComponent('favorite-task', 'favorite-drawer-item', { id: 'task', vueComponent: Vue.options.components['task-favorite-item'], }); - -// Add the favorite toggle inside the project card 3-dots menu -// (consumed by ProjectCardFront.vue via extension-registry-components). -extensionRegistry.registerComponent('TaskProjectMenu', 'task-project-menu', { - id: 'project-favorite', - rank: 5, - vueComponent: Vue.options.components['project-favorite-menu-action'], -}); - -// Add the favorite toggle inside the task drawer 3-dots menu -// (consumed by TaskDrawer.vue via loadExtensions('Task', 'task-menu')). -extensionRegistry.registerExtension('Task', 'task-menu', { - id: 'task-favorite', - rank: 5, - enabled: true, - vueComponent: 'task-favorite-menu-item', -}); diff --git a/webapps/src/main/webapp/vue-app/favorite-extensions/initComponents.js b/webapps/src/main/webapp/vue-app/favorite-extensions/initComponents.js index 3102cbb23..3bf94e44c 100644 --- a/webapps/src/main/webapp/vue-app/favorite-extensions/initComponents.js +++ b/webapps/src/main/webapp/vue-app/favorite-extensions/initComponents.js @@ -18,14 +18,10 @@ */ import ProjectFavoriteItem from './components/ProjectFavoriteItem.vue'; import TaskFavoriteItem from './components/TaskFavoriteItem.vue'; -import ProjectFavoriteMenuAction from './components/ProjectFavoriteMenuAction.vue'; -import TaskFavoriteMenuItem from './components/TaskFavoriteMenuItem.vue'; const components = { 'project-favorite-item': ProjectFavoriteItem, 'task-favorite-item': TaskFavoriteItem, - 'project-favorite-menu-action': ProjectFavoriteMenuAction, - 'task-favorite-menu-item': TaskFavoriteMenuItem, }; for (const key in components) { diff --git a/webapps/src/main/webapp/vue-app/favorite-extensions/components/ProjectFavoriteMenuAction.vue b/webapps/src/main/webapp/vue-app/task-favorite-menu/components/ProjectFavoriteMenuAction.vue similarity index 100% rename from webapps/src/main/webapp/vue-app/favorite-extensions/components/ProjectFavoriteMenuAction.vue rename to webapps/src/main/webapp/vue-app/task-favorite-menu/components/ProjectFavoriteMenuAction.vue diff --git a/webapps/src/main/webapp/vue-app/favorite-extensions/components/TaskFavoriteMenuItem.vue b/webapps/src/main/webapp/vue-app/task-favorite-menu/components/TaskFavoriteMenuItem.vue similarity index 100% rename from webapps/src/main/webapp/vue-app/favorite-extensions/components/TaskFavoriteMenuItem.vue rename to webapps/src/main/webapp/vue-app/task-favorite-menu/components/TaskFavoriteMenuItem.vue diff --git a/webapps/src/main/webapp/vue-app/task-favorite-menu/extensions.js b/webapps/src/main/webapp/vue-app/task-favorite-menu/extensions.js new file mode 100644 index 000000000..041d72de9 --- /dev/null +++ b/webapps/src/main/webapp/vue-app/task-favorite-menu/extensions.js @@ -0,0 +1,35 @@ +/* + * This file is part of the Meeds project (https://meeds.io/). + * + * Copyright (C) 2020 - 2025 Meeds Association contact@meeds.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +// Add the favorite toggle inside the project card 3-dots menu +// (consumed by ProjectCardFront.vue via extension-registry-components). +extensionRegistry.registerComponent('TaskProjectMenu', 'task-project-menu', { + id: 'project-favorite', + rank: 5, + vueComponent: Vue.options.components['project-favorite-menu-action'], +}); + +// Add the favorite toggle inside the task drawer 3-dots menu +// (consumed by TaskDrawer.vue via loadExtensions('Task', 'task-menu')). +extensionRegistry.registerExtension('Task', 'task-menu', { + id: 'task-favorite', + rank: 5, + enabled: true, + vueComponent: 'task-favorite-menu-item', +}); diff --git a/webapps/src/main/webapp/vue-app/task-favorite-menu/initComponents.js b/webapps/src/main/webapp/vue-app/task-favorite-menu/initComponents.js new file mode 100644 index 000000000..55cfc5d79 --- /dev/null +++ b/webapps/src/main/webapp/vue-app/task-favorite-menu/initComponents.js @@ -0,0 +1,29 @@ +/* + * This file is part of the Meeds project (https://meeds.io/). + * + * Copyright (C) 2020 - 2025 Meeds Association contact@meeds.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import ProjectFavoriteMenuAction from './components/ProjectFavoriteMenuAction.vue'; +import TaskFavoriteMenuItem from './components/TaskFavoriteMenuItem.vue'; + +const components = { + 'project-favorite-menu-action': ProjectFavoriteMenuAction, + 'task-favorite-menu-item': TaskFavoriteMenuItem, +}; + +for (const key in components) { + Vue.component(key, components[key]); +} diff --git a/webapps/src/main/webapp/vue-app/task-favorite-menu/main.js b/webapps/src/main/webapp/vue-app/task-favorite-menu/main.js new file mode 100644 index 000000000..ad549d2d6 --- /dev/null +++ b/webapps/src/main/webapp/vue-app/task-favorite-menu/main.js @@ -0,0 +1,20 @@ +/* + * This file is part of the Meeds project (https://meeds.io/). + * + * Copyright (C) 2020 - 2025 Meeds Association contact@meeds.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import './initComponents.js'; +import './extensions.js'; diff --git a/webapps/webpack.common.js b/webapps/webpack.common.js index 842fcbaee..e0ecb88ce 100644 --- a/webapps/webpack.common.js +++ b/webapps/webpack.common.js @@ -35,6 +35,7 @@ const config = { taskContentLinkExtension: './src/main/webapp/vue-app/content-link/extensions.js', restrictedProject: './src/main/webapp/vue-app/restricted-project/main.js', favoriteExtensions: './src/main/webapp/vue-app/favorite-extensions/main.js', + taskFavoriteMenu: './src/main/webapp/vue-app/task-favorite-menu/main.js', }, output: { filename: 'js/[name].bundle.js', From f1525db563bf300bb01427de95a6007889e00339 Mon Sep 17 00:00:00 2001 From: Benjamin Date: Sun, 28 Jun 2026 13:00:07 +0200 Subject: [PATCH 3/7] feat: custom favorite menu labels + register project/task favorite types in drawer - Menu rows now show 'Make it a favorite' / 'Remove from favorite'. - Make taskDrawer depend on taskFavoriteExtensions so favorite-type + favorite-drawer-item registrations execute on task pages, surfacing Projects and Tasks sections in the global Favorites drawer. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../portlet/taskManagement_en.properties | 2 + .../main/webapp/WEB-INF/gatein-resources.xml | 8 +- .../components/ProjectFavoriteMenuAction.vue | 112 +++++++++++----- .../components/TaskFavoriteMenuItem.vue | 123 ++++++++++++------ 4 files changed, 168 insertions(+), 77 deletions(-) diff --git a/webapps/src/main/resources/locale/portlet/taskManagement_en.properties b/webapps/src/main/resources/locale/portlet/taskManagement_en.properties index 6d6acaa4d..d02b54c87 100644 --- a/webapps/src/main/resources/locale/portlet/taskManagement_en.properties +++ b/webapps/src/main/resources/locale/portlet/taskManagement_en.properties @@ -61,6 +61,8 @@ label.edit=Edit label.share=Share label.clone=Clone label.addAsFavorite=Add to Favorites +label.favorite.add=Make it a favorite +label.favorite.remove=Remove from favorite label.delete=Delete label.external=Guest label.addProject=Add Project diff --git a/webapps/src/main/webapp/WEB-INF/gatein-resources.xml b/webapps/src/main/webapp/WEB-INF/gatein-resources.xml index e18c3122b..f33d84935 100644 --- a/webapps/src/main/webapp/WEB-INF/gatein-resources.xml +++ b/webapps/src/main/webapp/WEB-INF/gatein-resources.xml @@ -154,6 +154,12 @@ jquery $ + + taskFavoriteMenuExtension + + + taskFavoriteExtensions + @@ -389,7 +395,7 @@ taskFavoriteExtensions - FavoriteDrawerGRP + taskDrawerGrp diff --git a/webapps/src/main/webapp/vue-app/task-favorite-menu/components/ProjectFavoriteMenuAction.vue b/webapps/src/main/webapp/vue-app/task-favorite-menu/components/ProjectFavoriteMenuAction.vue index a05e939b1..486e38bb1 100644 --- a/webapps/src/main/webapp/vue-app/task-favorite-menu/components/ProjectFavoriteMenuAction.vue +++ b/webapps/src/main/webapp/vue-app/task-favorite-menu/components/ProjectFavoriteMenuAction.vue @@ -17,19 +17,21 @@ Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. --> @@ -414,7 +408,7 @@ - taskFavoriteMenuExtension + taskFavoriteMenuTaskExtension taskDrawerGrp diff --git a/webapps/src/main/webapp/vue-app/task-favorite-menu/extensions.js b/webapps/src/main/webapp/vue-app/task-favorite-menu/extensions.js index 041d72de9..b3358ee06 100644 --- a/webapps/src/main/webapp/vue-app/task-favorite-menu/extensions.js +++ b/webapps/src/main/webapp/vue-app/task-favorite-menu/extensions.js @@ -17,8 +17,18 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -// Add the favorite toggle inside the project card 3-dots menu -// (consumed by ProjectCardFront.vue via extension-registry-components). +// Favorite as an inline star injected in the project board header +// (TasksViewDashboard.vue renders the task-board-header extension point inline: +// the favorite star + the AI icon when AI is enabled). Symmetric with AI's +// project-ask-ai-board-header-action. +extensionRegistry.registerComponent('TaskProjectBoard', 'task-board-header', { + id: 'project-favorite', + rank: 5, + vueComponent: Vue.options.components['project-favorite-board-header-action'], +}); + +// Favorite toggle as a menu row, used in the project card 3-dots menu +// (ProjectCardFront.vue) and the board header overflow 3-dots when collapsed. extensionRegistry.registerComponent('TaskProjectMenu', 'task-project-menu', { id: 'project-favorite', rank: 5, diff --git a/webapps/src/main/webapp/vue-app/task-favorite-menu/initComponents.js b/webapps/src/main/webapp/vue-app/task-favorite-menu/initComponents.js index 55cfc5d79..d79642b6a 100644 --- a/webapps/src/main/webapp/vue-app/task-favorite-menu/initComponents.js +++ b/webapps/src/main/webapp/vue-app/task-favorite-menu/initComponents.js @@ -18,10 +18,12 @@ */ import ProjectFavoriteMenuAction from './components/ProjectFavoriteMenuAction.vue'; import TaskFavoriteMenuItem from './components/TaskFavoriteMenuItem.vue'; +import ProjectFavoriteBoardHeaderAction from './components/ProjectFavoriteBoardHeaderAction.vue'; const components = { 'project-favorite-menu-action': ProjectFavoriteMenuAction, 'task-favorite-menu-item': TaskFavoriteMenuItem, + 'project-favorite-board-header-action': ProjectFavoriteBoardHeaderAction, }; for (const key in components) { diff --git a/webapps/src/main/webapp/vue-app/tasks-management/components/ProjectTasks/TasksViewDashboard.vue b/webapps/src/main/webapp/vue-app/tasks-management/components/ProjectTasks/TasksViewDashboard.vue index bc095c05d..ba6e7b189 100644 --- a/webapps/src/main/webapp/vue-app/tasks-management/components/ProjectTasks/TasksViewDashboard.vue +++ b/webapps/src/main/webapp/vue-app/tasks-management/components/ProjectTasks/TasksViewDashboard.vue @@ -43,7 +43,7 @@ type="task-board-header" parent-element="div" element="div" - class="flex-shrink-0 ms-4" /> + class="d-flex align-center flex-shrink-0 ms-4" />
Date: Mon, 29 Jun 2026 14:45:14 +0200 Subject: [PATCH 7/7] feat: hide the board-header favorite star on mobile On small screens (smAndDown) the inline favorite star in the project board header is hidden to declutter; favoriting stays available elsewhere. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../components/ProjectFavoriteBoardHeaderAction.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webapps/src/main/webapp/vue-app/task-favorite-menu/components/ProjectFavoriteBoardHeaderAction.vue b/webapps/src/main/webapp/vue-app/task-favorite-menu/components/ProjectFavoriteBoardHeaderAction.vue index 1a9bace39..86f294d2a 100644 --- a/webapps/src/main/webapp/vue-app/task-favorite-menu/components/ProjectFavoriteBoardHeaderAction.vue +++ b/webapps/src/main/webapp/vue-app/task-favorite-menu/components/ProjectFavoriteBoardHeaderAction.vue @@ -19,7 +19,7 @@