Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -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` (`<component><key>interface</key><type>impl</type></component>`). 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/<app>/main.js`, each a webpack entry in `webapps/webpack.common.js` → `js/<name>.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(...)`.
5 changes: 4 additions & 1 deletion services/src/main/java/org/exoplatform/task/dto/TaskDto.java
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ public class TaskDto implements Serializable {

private String activityId;

private boolean favorite;

public TaskDto clone() { // NOSONAR
return new TaskDto(id,
title,
Expand All @@ -87,7 +89,8 @@ public TaskDto clone() { // NOSONAR
startDate,
endDate,
dueDate,
activityId);
activityId,
favorite);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -82,14 +87,17 @@ public class ProjectRestService implements ResourceContainer {

private IdentityManager identityManager;

private FavoriteService favoriteService;

public ProjectRestService(TaskService taskService,
CommentService commentService,
ProjectService projectService,
StatusService statusService,
UserService userService,
SpaceService spaceService,
LabelService labelService,
IdentityManager identityManager) {
IdentityManager identityManager,
FavoriteService favoriteService) {
this.taskService = taskService;
this.commentService = commentService;
this.projectService = projectService;
Expand All @@ -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 {
Expand Down Expand Up @@ -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;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand All @@ -123,21 +131,41 @@ 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;
this.statusService = statusService;
this.userService = userService;
this.spaceService = spaceService;
this.labelService = labelService;
this.favoriteService = favoriteService;
this.identityManager = identityManager;
}


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
Expand All @@ -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);
Expand Down
Loading
Loading