diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 000000000..6f5e20997
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1,63 @@
+# CLAUDE.md
+
+This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
+
+## What this is
+
+The **Meeds/eXo Task Management add-on** (`io.meeds.task`): projects, tasks, statuses (kanban), comments, labels, and assignments inside the Meeds/eXo social platform. It builds a `task-management-services` JAR + a `task-management-webapps` WAR + a `task-management-packaging` ZIP, deployed into a running eXo/Meeds server.
+
+## Build & test
+
+Maven multi-module build (`services` → `webapps` → `packaging`). Inherits from `io.meeds.addons:addons-parent-pom`.
+
+```bash
+mvn install # full build (compiles Java, runs frontend build + JS lint, packages WAR/ZIP)
+mvn install -pl services # backend module only
+mvn test -pl services # run all backend tests
+mvn test -pl services -Dtest=TestPermission # single test class
+mvn test -pl services -Dtest=TestPermission#method # single test method
+```
+
+Frontend (in `webapps/`, driven by `frontend-maven-plugin` during the Maven build, or run directly):
+
+```bash
+cd webapps
+npm run build # webpack production build → src/main/webapp/js/*.bundle.js
+npm run watch # webpack dev watch
+npm run lint # eslint --fix over vue-app/ (also runs at build time via eslint-webpack-plugin)
+```
+
+i18n: edit only the `_en` source bundles under `*/src/main/resources/locale/`; Crowdin syncs the rest (see `crowdin.yml`).
+
+## Architecture
+
+### Two-generation backend (important — DI differs by package)
+
+- **Legacy core `org.exoplatform.task.*`** — wired via **eXo Kernel `configuration.xml`**, NOT Spring. Services use constructor injection of kernel components and `CommonsUtils.getService(...)` / `PortalContainer.getComponentInstanceOfType(...)` for lookups. Component bindings live in `services/src/main/resources/conf/portal/configuration.xml` (`interfaceimpl`). Layering:
+ - `service/` (interfaces) + `service/impl/` — business logic (`TaskService`, `ProjectService`, `StatusService`, `CommentService`, `LabelService`, `UserService`).
+ - `storage/` + `storage/impl/` — maps domain entities ↔ DTOs, sits between services and DAOs.
+ - `dao/` + `dao/jpa/` — JPA handlers (`DAOHandler` aggregates `TaskHandler`, `ProjectHandler`, etc.). `TaskQuery`/`ProjectQuery` + `dao/condition/` build dynamic queries.
+ - `domain/` — JPA `@Entity` classes (`Task`, `Project`, `Status`, `Comment`, `Label`); `dto/` — transport objects; `rest/` — JAX-RS endpoints (`TaskRestService`, `ProjectRestService`, `StatusRestService`).
+- **Newer integrations `io.meeds.task.*`** — use **Spring** (`@Service`/`@Component`/`@Autowired`). These cover platform plugins (`plugin/` — ACL, content-link, permanent-link), `listener/` (content-link + indexing), `search/TaskSearchConnector`, and `mcp/TaskMcpTool`. They `@Autowired` the legacy kernel services across the bridge.
+
+This is the reverse of the platform-wide default (where core is Spring); here the legacy core is still Kernel-wired. When editing, follow the convention of the package you're in.
+
+### DB migrations
+
+Liquibase changelogs in `services/src/main/resources/db/changelog/task.db.changelog-*.xml`, applied at startup. Add a new versioned changelog file for schema changes (e.g. the recent `task.db.changelog-7.2.0.xml`); never edit shipped ones.
+
+### MCP / AI integration
+
+`io.meeds.task.mcp.TaskMcpTool implements io.meeds.mcp.server.plugin.McpToolPlugin`. Public methods become snake_case MCP tools (`listProjects` → `list_projects`). **Every tool method REQUIRES a matching entry in `services/src/main/resources/ai-tool-definitions.json`** (root `{"tools":[…]}`, `name` = method snake_case) or it is silently dropped. Named-arg binding depends on the `-parameters` javac flag (configured via the parent pom). The tool acts as the calling user, so platform ACLs apply.
+
+### Frontend (Vue 2)
+
+Per-feature apps under `webapps/src/main/webapp/vue-app//main.js`, each a webpack entry in `webapps/webpack.common.js` → `js/.bundle.js` (AMD, `libraryTarget: 'amd'`). Entries include `tasks`, `taskDrawer`, `tasksManagement`, `taskCommentsDrawer`, `taskSearch`, plus extension bundles (`engagementCenterExtensions`, `connectorEventExtensions`, `notificationExtension`, `taskQuickAction`, `taskContentLinkExtension`, `restrictedProject`). Modules + load-groups are declared in `webapps/src/main/webapp/WEB-INF/gatein-resources.xml`. WAR packaging excludes raw `vue-app/**`, `*.vue`, `*.less` (`packagingExcludes`).
+
+### Platform wiring (webapp side)
+
+`external-component-plugins` and integration config live in `webapps/src/main/webapp/WEB-INF/conf/task-addon/*.xml`: `task-service-configuration.xml`, `acl-configuration.xml`, `search-configuration.xml`, `indexing-configuration.xml`, `social-configuration.xml`, `notification-configuration.xml`, `analytics-configuration.xml`, `gamification-integration-configuration.xml`, `ckeditor-configuration.xml`, `dynamic-container-configuration.xml`.
+
+## Tests
+
+Backend tests use eXo Kernel container bootstrap, not Spring. Extend `org.exoplatform.task.AbstractTest`, which manages the JPA `RequestLifeCycle` (begin/end per test, `restartTransaction()` between DAO ops) and provides `deleteAll()` for cleanup. Resolve components via `PortalContainer.getInstance().getComponentInstanceOfType(...)`.
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/resources/locale/portlet/Portlets_en.properties b/webapps/src/main/resources/locale/portlet/Portlets_en.properties
index 8a0cc773b..6467dce29 100644
--- a/webapps/src/main/resources/locale/portlet/Portlets_en.properties
+++ b/webapps/src/main/resources/locale/portlet/Portlets_en.properties
@@ -1,3 +1,7 @@
#SpaceApplications
SpaceSettings.application.tasksmanagement.title=Tasks Management
SpaceSettings.application.tasksmanagement.description=Tasks Management
+
+#Favorites drawer type labels (merged into the social UITopBarFavoritesPortlet bundle)
+UITopBarFavoritesPortlet.types.project=Projects
+UITopBarFavoritesPortlet.types.task=Tasks
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 915a5fe6c..f55e08b43 100644
--- a/webapps/src/main/webapp/WEB-INF/gatein-resources.xml
+++ b/webapps/src/main/webapp/WEB-INF/gatein-resources.xml
@@ -387,4 +387,44 @@
+
+ TaskFavoriteDrawerExtension
+ FavoriteDrawerGRP
+
+
+ vue
+
+
+ eXoVueI18n
+
+
+ commonVueComponents
+
+
+ extensionRegistry
+
+
+
+
+ taskFavoriteMenuTaskExtension
+ taskDrawerGrp
+
+
+ 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 @@
+
+
+
+
+
+ fa-tasks
+
+
+
+ {{ projectName }}
+
+
+
+
+
+
+
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 @@
+
+
+
+
+
+ fa-check-square
+
+
+
+ {{ taskTitle }}
+
+
+
+
+
+
+
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..f392976b9
--- /dev/null
+++ b/webapps/src/main/webapp/vue-app/favorite-extensions/extensions.js
@@ -0,0 +1,43 @@
+/*
+ * 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: 70,
+ 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: 71,
+ id: 'task',
+ name: 'Tasks',
+ icon: 'fa-check-square',
+});
+extensionRegistry.registerComponent('favorite-task', 'favorite-drawer-item', {
+ id: 'task',
+ vueComponent: Vue.options.components['task-favorite-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..3bf94e44c
--- /dev/null
+++ b/webapps/src/main/webapp/vue-app/favorite-extensions/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 ProjectFavoriteItem from './components/ProjectFavoriteItem.vue';
+import TaskFavoriteItem from './components/TaskFavoriteItem.vue';
+
+const components = {
+ 'project-favorite-item': ProjectFavoriteItem,
+ 'task-favorite-item': TaskFavoriteItem,
+};
+
+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/src/main/webapp/vue-app/task-favorite-menu/components/ProjectFavoriteBoardHeaderAction.vue b/webapps/src/main/webapp/vue-app/task-favorite-menu/components/ProjectFavoriteBoardHeaderAction.vue
new file mode 100644
index 000000000..86f294d2a
--- /dev/null
+++ b/webapps/src/main/webapp/vue-app/task-favorite-menu/components/ProjectFavoriteBoardHeaderAction.vue
@@ -0,0 +1,50 @@
+
+
+
+
+
+
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
new file mode 100644
index 000000000..486e38bb1
--- /dev/null
+++ b/webapps/src/main/webapp/vue-app/task-favorite-menu/components/ProjectFavoriteMenuAction.vue
@@ -0,0 +1,127 @@
+
+
+
+
+
diff --git a/webapps/src/main/webapp/vue-app/task-favorite-menu/components/TaskFavoriteMenuItem.vue b/webapps/src/main/webapp/vue-app/task-favorite-menu/components/TaskFavoriteMenuItem.vue
new file mode 100644
index 000000000..d14a7c52b
--- /dev/null
+++ b/webapps/src/main/webapp/vue-app/task-favorite-menu/components/TaskFavoriteMenuItem.vue
@@ -0,0 +1,127 @@
+
+
+
+
+
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..b3358ee06
--- /dev/null
+++ b/webapps/src/main/webapp/vue-app/task-favorite-menu/extensions.js
@@ -0,0 +1,45 @@
+/*
+ * 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.
+ */
+
+// 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,
+ 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..d79642b6a
--- /dev/null
+++ b/webapps/src/main/webapp/vue-app/task-favorite-menu/initComponents.js
@@ -0,0 +1,31 @@
+/*
+ * 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';
+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) {
+ 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/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" />