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 @@ + + + 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/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" />