Repository: Meeds-io/app-center
Pull request #394: Merge Meeds-io/MIPs#183
Author: login: hakermi
Created: 2025-04-02T08:21:34Z | Merged: 2025-04-03T09:10:40Z

Resolves  Meeds-io/MIPs#183


Diff:
diff --git a/app-center-services/src/main/java/io/meeds/appcenter/dao/ApplicationDAO.java b/app-center-services/src/main/java/io/meeds/appcenter/dao/ApplicationDAO.java
index e0096d76b..38983ec57 100644
--- a/app-center-services/src/main/java/io/meeds/appcenter/dao/ApplicationDAO.java
+++ b/app-center-services/src/main/java/io/meeds/appcenter/dao/ApplicationDAO.java
@@ -20,9 +20,13 @@
 
 import java.util.List;
 
+import io.meeds.appcenter.entity.FavoriteApplicationEntity;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
 import org.springframework.data.domain.Sort;
 import org.springframework.data.jpa.repository.JpaRepository;
 import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.repository.query.Param;
 import org.springframework.stereotype.Component;
 
 import io.meeds.appcenter.entity.ApplicationEntity;
@@ -72,4 +76,15 @@ default List<ApplicationEntity> getApplications(String keyword) {
   default List<ApplicationEntity> findAll() {
     return findAll(Sort.by(Sort.Order.asc("title").ignoreCase()));
   }
+
+  @Query("""
+      SELECT new FavoriteApplicationEntity(favoriteApp.id, app, favoriteApp.userName, favoriteApp.order)
+      FROM ApplicationEntity app
+      LEFT JOIN FavoriteApplicationEntity favoriteApp \
+            ON app.id = favoriteApp.application.id AND favoriteApp.userName = :userName
+      WHERE app.active = TRUE AND (favoriteApp.id IS NOT NULL OR app.isMandatory = TRUE)
+      ORDER BY favoriteApp.order NULLS LAST, app.isMandatory DESC
+      """)
+  Page<FavoriteApplicationEntity> findFavoriteAndMandatoryApplications(@Param("userName") String userName, Pageable pageable);
+
 }
diff --git a/app-center-services/src/main/java/io/meeds/appcenter/model/ApplicationList.java b/app-center-services/src/main/java/io/meeds/appcenter/model/ApplicationList.java
index e5439ebbc..d60675833 100644
--- a/app-center-services/src/main/java/io/meeds/appcenter/model/ApplicationList.java
+++ b/app-center-services/src/main/java/io/meeds/appcenter/model/ApplicationList.java
@@ -1,17 +1,17 @@
-/**
+/*
  * This file is part of the Meeds project (https://meeds.io/).
- * 
- * Copyright (C) 2020 - 2024 Meeds Association contact@meeds.io
+ *
+ * Copyright (C) 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.
@@ -23,53 +23,54 @@
 public class ApplicationList {
 
   private List<Application> applications;
+  private long offset;
+  private long limit;
+  private long size;
+  private boolean canAddFavorite;
 
-  private long              offset;
-
-  private long              limit;
-
-  private long              size;
-
-  private boolean           canAddFavorite;
-
+  // Fluent API style setters (return `this` for method chaining)
   public List<Application> getApplications() {
     return applications;
   }
 
-  public void setApplications(List<Application> applications) {
+  public ApplicationList setApplications(List<Application> applications) {
     this.applications = applications;
+    return this;
   }
 
   public boolean isCanAddFavorite() {
     return canAddFavorite;
   }
 
-  public void setCanAddFavorite(boolean canAddFavorite) {
+  public ApplicationList setCanAddFavorite(boolean canAddFavorite) {
     this.canAddFavorite = canAddFavorite;
+    return this;
   }
 
   public long getOffset() {
     return offset;
   }
 
-  public void setOffset(long offset) {
+  public ApplicationList setOffset(long offset) {
     this.offset = offset;
+    return this;
   }
 
   public long getLimit() {
     return limit;
   }
 
-  public void setLimit(long limit) {
+  public ApplicationList setLimit(long limit) {
     this.limit = limit;
+    return this;
   }
 
   public long getSize() {
     return size;
   }
 
-  public void setSize(long size) {
+  public ApplicationList setSize(long size) {
     this.size = size;
+    return this;
   }
-
 }
diff --git a/app-center-services/src/main/java/io/meeds/appcenter/plugin/MyApplicationsHeaderTranslation.java b/app-center-services/src/main/java/io/meeds/appcenter/plugin/MyApplicationsHeaderTranslation.java
new file mode 100644
index 000000000..e0174651b
--- /dev/null
+++ b/app-center-services/src/main/java/io/meeds/appcenter/plugin/MyApplicationsHeaderTranslation.java
@@ -0,0 +1,73 @@
+/*
+ * This file is part of the Meeds project (https://meeds.io/).
+ *
+ * Copyright (C) 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.
+ */
+
+package io.meeds.appcenter.plugin;
+
+import io.meeds.social.translation.plugin.TranslationPlugin;
+import io.meeds.social.translation.service.TranslationService;
+import jakarta.annotation.PostConstruct;
+import org.exoplatform.portal.config.UserACL;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+@Component
+public class MyApplicationsHeaderTranslation extends TranslationPlugin {
+
+  public static final String       MY_APPLICATIONS_OBJECT_TYPE = "myApplicationsPortlet";
+
+  private final TranslationService translationService;
+
+  private final UserACL            userACL;
+
+  @Autowired
+  public MyApplicationsHeaderTranslation(TranslationService translationService, UserACL userACL) {
+    this.translationService = translationService;
+    this.userACL = userACL;
+  }
+
+  @PostConstruct
+  public void init() {
+    translationService.addPlugin(this);
+  }
+
+  @Override
+  public String getObjectType() {
+    return MY_APPLICATIONS_OBJECT_TYPE;
+  }
+
+  @Override
+  public boolean hasAccessPermission(long objectId, String username) {
+    return true;
+  }
+
+  @Override
+  public boolean hasEditPermission(long objectId, String username) {
+    return userACL.isAdministrator(userACL.getUserIdentity(username));
+  }
+
+  @Override
+  public long getAudienceId(long objectId) {
+    return 0;
+  }
+
+  @Override
+  public long getSpaceId(long objectId) {
+    return 0;
+  }
+}
diff --git a/app-center-services/src/main/java/io/meeds/appcenter/rest/ApplicationFavoriteRest.java b/app-center-services/src/main/java/io/meeds/appcenter/rest/ApplicationFavoriteRest.java
index 41e375ad0..2c2f72efe 100644
--- a/app-center-services/src/main/java/io/meeds/appcenter/rest/ApplicationFavoriteRest.java
+++ b/app-center-services/src/main/java/io/meeds/appcenter/rest/ApplicationFavoriteRest.java
@@ -21,6 +21,7 @@
 import java.util.List;
 
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.domain.Pageable;
 import org.springframework.http.HttpStatus;
 import org.springframework.http.MediaType;
 import org.springframework.security.access.annotation.Secured;
@@ -62,8 +63,8 @@ public class ApplicationFavoriteRest {
              description = "Return list of applications in json format")
   @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Request fulfilled"),
                           @ApiResponse(responseCode = "500", description = "Internal server error") })
-  public ApplicationList getFavoriteApplicationsList(HttpServletRequest request) {
-    return appCenterService.getMandatoryAndFavoriteApplicationsList(request.getRemoteUser());
+  public ApplicationList getFavoriteApplicationsList(HttpServletRequest request, Pageable pageable) {
+    return appCenterService.getMandatoryAndFavoriteApplications(request.getRemoteUser(), pageable);
   }
 
   @PostMapping(path = "{applicationId}")
diff --git a/app-center-services/src/main/java/io/meeds/appcenter/service/ApplicationCenterService.java b/app-center-services/src/main/java/io/meeds/appcenter/service/ApplicationCenterService.java
index b8a257d97..0aa40ae3f 100644
--- a/app-center-services/src/main/java/io/meeds/appcenter/service/ApplicationCenterService.java
+++ b/app-center-services/src/main/java/io/meeds/appcenter/service/ApplicationCenterService.java
@@ -28,6 +28,7 @@
 import org.apache.commons.lang3.StringUtils;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Value;
+import org.springframework.data.domain.Pageable;
 import org.springframework.stereotype.Service;
 
 import org.exoplatform.commons.api.settings.SettingService;
@@ -585,6 +586,20 @@ public List<Application> getSystemApplications() {
     return appCenterStorage.getSystemApplications();
   }
 
+  public ApplicationList getMandatoryAndFavoriteApplications(String username, Pageable pageable) {
+    List<Application> applications = appCenterStorage.getMandatoryAndFavoriteApplications(username, pageable)
+                                                     .stream()
+                                                     .filter(app -> hasPermission(username, app))
+                                                     .collect(Collectors.toList());
+    long countFavorites = appCenterStorage.countFavorites(username);
+    int appCount = applications.size();
+    return new ApplicationList().setApplications(applications)
+                                .setCanAddFavorite(countFavorites < getMaxFavoriteApps())
+                                .setLimit(appCount)
+                                .setSize(appCount)
+                                .setOffset(0);
+  }
+  
   private boolean isAdministrator(String username) {
     return hasPermission(username, DEFAULT_ADMINISTRATORS_GROUP);
   }
@@ -672,5 +687,4 @@ public IdentityRegistry getIdentityRegistry() {
     }
     return identityRegistry;
   }
-
 }
diff --git a/app-center-services/src/main/java/io/meeds/appcenter/storage/ApplicationCenterStorage.java b/app-center-services/src/main/java/io/meeds/appcenter/storage/ApplicationCenterStorage.java
index f1fdaa4cf..9bd81afaa 100644
--- a/app-center-services/src/main/java/io/meeds/appcenter/storage/ApplicationCenterStorage.java
+++ b/app-center-services/src/main/java/io/meeds/appcenter/storage/ApplicationCenterStorage.java
@@ -28,6 +28,8 @@
 
 import org.apache.commons.lang3.StringUtils;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
 import org.springframework.stereotype.Component;
 
 import org.exoplatform.commons.file.model.FileInfo;
@@ -300,6 +302,10 @@ public long countApplications() {
     return applicationDAO.count();
   }
 
+  public List<UserApplication> getMandatoryAndFavoriteApplications(String username, Pageable pageable) {
+    return applicationDAO.findFavoriteAndMandatoryApplications(username, pageable).map(this::toUserApplicationDTO).getContent();
+  }
+
   private Application toDTO(ApplicationEntity applicationEntity) {
     if (applicationEntity == null) {
       return null;
diff --git a/app-center-services/src/main/resources/portlet-instances.json b/app-center-services/src/main/resources/portlet-instances.json
new file mode 100644
index 000000000..090bb0c8e
--- /dev/null
+++ b/app-center-services/src/main/resources/portlet-instances.json
@@ -0,0 +1,22 @@
+{
+  "descriptors": [
+    {
+      "nameId": "AppCenterMyApplicationsPortlet",
+      "categoryNameId": "tools",
+      "portletName": "AppCenterMyApplicationsPortlet",
+      "names": {
+        "en": "layout.portletInstance.AppCenterMyApplicationsPortlet.name"
+      },
+      "descriptions": {
+        "en": "layout.portletInstance.AppCenterMyApplicationsPortlet.description"
+      },
+      "illustrationPath": "war:/../images/portlets/myApplicationsPortlet.png",
+      "preferences": [
+      ],
+      "permissions": [
+        "*:/platform/administrators"
+      ],
+      "system": true
+    }
+  ]
+}
diff --git a/app-center-services/src/test/java/io/meeds/appcenter/plugin/MyApplicationsHeaderTranslationTest.java b/app-center-services/src/test/java/io/meeds/appcenter/plugin/MyApplicationsHeaderTranslationTest.java
new file mode 100644
index 000000000..8d3d4c14e
--- /dev/null
+++ b/app-center-services/src/test/java/io/meeds/appcenter/plugin/MyApplicationsHeaderTranslationTest.java
@@ -0,0 +1,58 @@
+/*
+ * This file is part of the Meeds project (https://meeds.io/).
+ *
+ * Copyright (C) 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.
+ */
+
+package io.meeds.appcenter.plugin;
+
+import io.meeds.social.translation.model.TranslationField;
+import io.meeds.social.translation.service.TranslationService;
+import org.exoplatform.commons.exception.ObjectNotFoundException;
+import org.junit.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+@SpringBootTest
+@ExtendWith(MockitoExtension.class)
+public class MyApplicationsHeaderTranslationTest {
+
+  @Autowired
+  private TranslationService translationService;
+
+  @Test
+  public void testTranslateHeaderTitle() throws ObjectNotFoundException {
+
+    Map<Locale, String> labels = new HashMap<>();
+    labels.put(Locale.US, "header en");
+    labels.put(Locale.FRANCE, "header fr");
+    translationService.saveTranslationLabels("myApplicationsPortlet", 1L, "headerTitle", labels);
+
+    TranslationField translationField = translationService.getTranslationField("myApplicationsPortlet", 1L, "headerTitle");
+    assertNotNull(translationField);
+    assertNotNull(translationField.getLabels());
+    assertEquals(2, translationField.getLabels().size());
+  }
+}
diff --git a/app-center-services/src/test/java/io/meeds/appcenter/service/ApplicationCenterServiceTest.java b/app-center-services/src/test/java/io/meeds/appcenter/service/ApplicationCenterServiceTest.java
index 4d972f9e4..37e45ca01 100644
--- a/app-center-services/src/test/java/io/meeds/appcenter/service/ApplicationCenterServiceTest.java
+++ b/app-center-services/src/test/java/io/meeds/appcenter/service/ApplicationCenterServiceTest.java
@@ -27,8 +27,7 @@
 import static org.junit.jupiter.api.Assertions.assertNull;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.argThat;
+import static org.mockito.ArgumentMatchers.*;
 import static org.mockito.Mockito.lenient;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
@@ -36,8 +35,10 @@
 import static org.mockito.Mockito.when;
 
 import java.io.InputStream;
+import java.lang.reflect.Method;
 import java.util.Arrays;
 import java.util.Collections;
+import java.util.List;
 
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
@@ -67,6 +68,8 @@
 import io.meeds.appcenter.storage.ApplicationCenterStorage;
 
 import lombok.SneakyThrows;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.data.domain.Pageable;
 
 @SpringBootTest(classes = { ApplicationCenterService.class })
 @ExtendWith(MockitoExtension.class)
@@ -464,6 +467,46 @@ void getImageStream() {
     assertNotNull(stream);
   }
 
+  @Test
+  @SneakyThrows
+  void testGetMandatoryAndFavoriteApplications() {
+    String username = "testuser";
+    Pageable pageable = PageRequest.of(0, 5);
+
+    UserApplication application1 = new UserApplication(application(1L));
+    UserApplication application2 = new UserApplication(application(2L));
+    UserApplication application3 = new UserApplication(application(3L));
+    UserApplication application4 = new UserApplication(application(4L));
+    UserApplication application5 = new UserApplication(application(5L));
+
+    application1.setPermissions(Collections.singletonList(PERMISSIONS_2));
+    application2.setPermissions(Collections.singletonList(PERMISSIONS_2));
+    application3.setPermissions(Collections.singletonList(PERMISSIONS_2));
+    application4.setPermissions(Collections.singletonList(PERMISSIONS_2));
+
+    when(appCenterStorage.getMandatoryAndFavoriteApplications(username, pageable))
+            .thenReturn(Arrays.asList(application1, application2, application3, application4, application5));
+    when(appCenterStorage.countFavorites(username)).thenReturn(3L);
+
+    applicationCenterService.setMaxFavoriteApps(5);
+
+    ApplicationList applicationList = applicationCenterService.getMandatoryAndFavoriteApplications(username, pageable);
+
+    assertNotNull(applicationList);
+    assertNotNull(applicationList.getApplications());
+    assertEquals(4, applicationList.getApplications().size());
+
+    assertTrue(applicationList.isCanAddFavorite());
+
+    assertEquals(4, applicationList.getSize());
+    assertEquals(4, applicationList.getLimit());
+    assertEquals(0, applicationList.getOffset());
+
+    when(appCenterStorage.countFavorites(username)).thenReturn(6L);
+    applicationList = applicationCenterService.getMandatoryAndFavoriteApplications(username, pageable);
+    assertFalse(applicationList.isCanAddFavorite());
+  }
+
   private Application application() {
     return application(ID);
   }
diff --git a/app-center-webapps/pom.xml b/app-center-webapps/pom.xml
index 0b88abc6a..db35d345c 100644
--- a/app-center-webapps/pom.xml
+++ b/app-center-webapps/pom.xml
@@ -43,6 +43,11 @@
       <classifier>sources</classifier>
       <scope>provided</scope>
     </dependency>
+    <dependency>
+      <groupId>io.meeds.social</groupId>
+      <artifactId>social-component-web</artifactId>
+      <scope>provided</scope>
+    </dependency>
   </dependencies>
   <build>
     <finalName>app-center</finalName>
diff --git a/app-center-webapps/src/main/java/io/meeds/appcenter/portlet/MyApplicationsPortlet.java b/app-center-webapps/src/main/java/io/meeds/appcenter/portlet/MyApplicationsPortlet.java
index 5f3db70cd..f2cee0de3 100644
--- a/app-center-webapps/src/main/java/io/meeds/appcenter/portlet/MyApplicationsPortlet.java
+++ b/app-center-webapps/src/main/java/io/meeds/appcenter/portlet/MyApplicationsPortlet.java
@@ -1,40 +1,101 @@
-/**
+/*
  * This file is part of the Meeds project (https://meeds.io/).
- * 
- * Copyright (C) 2020 - 2024 Meeds Association contact@meeds.io
+ *
+ * Copyright (C) 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.
  */
+
 package io.meeds.appcenter.portlet;
 
+import io.meeds.social.portlet.CMSPortlet;
+import org.apache.commons.lang3.StringUtils;
+import org.exoplatform.container.ExoContainerContext;
+import org.exoplatform.portal.config.UserACL;
+import org.exoplatform.services.security.ConversationState;
+import org.exoplatform.services.security.Identity;
+
+import javax.portlet.*;
 import java.io.IOException;
+import java.security.SecureRandom;
+import java.util.Enumeration;
+import java.util.Random;
 
-import javax.portlet.GenericPortlet;
-import javax.portlet.PortletException;
-import javax.portlet.PortletRequestDispatcher;
-import javax.portlet.RenderRequest;
-import javax.portlet.RenderResponse;
+public class MyApplicationsPortlet extends CMSPortlet {
 
+  private static final String OBJECT_TYPE    = "myApplicationsPortlet";
+
+  private static final String APPLICATION_ID = "applicationId";
+
+  private static final String IS_ADMIN       = "isAdmin";
+
+  private UserACL             userAcl;
+
+  private final SecureRandom  random         = new SecureRandom();
+
+  @Override
+  public void init(PortletConfig config) throws PortletException {
+    super.init(config);
+    this.contentType = OBJECT_TYPE;
+  }
 
-/**
- * @author Ayoub Zayati
- */
-public class MyApplicationsPortlet extends GenericPortlet {
   @Override
-  protected void doView(RenderRequest request, RenderResponse response) throws IOException, PortletException {
-    PortletRequestDispatcher dispatcher = getPortletContext().getRequestDispatcher("/WEB-INF/jsp/appCenter/myApplications/view.jsp");
-    dispatcher.forward(request, response);
+  public void processAction(ActionRequest request, ActionResponse response) throws PortletException, IOException {
+    if (!canEditSettings()) {
+      throw new PortletException("User is not allowed to edit settings");
+    }
+    PortletPreferences preferences = request.getPreferences();
+    Enumeration<String> parameterNames = request.getParameterNames();
+    while (parameterNames.hasMoreElements()) {
+      String name = parameterNames.nextElement();
+      if (StringUtils.equals(name, "action") || StringUtils.contains(name, "portal:")) {
+        continue;
+      }
+      String value = request.getParameter(name);
+      preferences.setValue(name, value);
+    }
+    preferences.store();
   }
 
+  @Override
+  public void doView(RenderRequest request, RenderResponse response) throws PortletException, IOException {
+    request.setAttribute(APPLICATION_ID, getOrCreateApplicationId(request.getPreferences()));
+    request.setAttribute(IS_ADMIN, getUserAcl().isAdministrator(getCurrentIdentity()));
+    super.doView(request, response);
+  }
+
+  private boolean canEditSettings() {
+    return getUserAcl().isAdministrator(getCurrentIdentity());
+  }
+
+  private Identity getCurrentIdentity() {
+    return ConversationState.getCurrent() == null ? null : ConversationState.getCurrent().getIdentity();
+  }
+
+  private UserACL getUserAcl() {
+    if (userAcl == null) {
+      userAcl = ExoContainerContext.getService(UserACL.class);
+    }
+    return userAcl;
+  }
+
+  private String getOrCreateApplicationId(PortletPreferences preferences) {
+    String applicationId = preferences.getValue(APPLICATION_ID, null);
+    if (applicationId == null) {
+      applicationId = String.valueOf(random.nextLong() & Long.MAX_VALUE);
+      savePreference(APPLICATION_ID, applicationId);
+    }
+    return applicationId;
+  }
 }
diff --git a/app-center-webapps/src/main/resources/locale/portlet/LayoutEditor_en.properties b/app-center-webapps/src/main/resources/locale/portlet/LayoutEditor_en.properties
new file mode 100644
index 000000000..6f78f2151
--- /dev/null
+++ b/app-center-webapps/src/main/resources/locale/portlet/LayoutEditor_en.properties
@@ -0,0 +1,2 @@
+layout.portletInstance.AppCenterMyApplicationsPortlet.name=Shortcuts
+layout.portletInstance.AppCenterMyApplicationsPortlet.description=List favorite apps of the current user
diff --git a/app-center-webapps/src/main/resources/locale/portlet/LayoutEditor_fr.properties b/app-center-webapps/src/main/resources/locale/portlet/LayoutEditor_fr.properties
new file mode 100644
index 000000000..53407f5bc
--- /dev/null
+++ b/app-center-webapps/src/main/resources/locale/portlet/LayoutEditor_fr.properties
@@ -0,0 +1,2 @@
+layout.portletInstance.AppCenterMyApplicationsPortlet.name=Raccourcis
+layout.portletInstance.AppCenterMyApplicationsPortlet.description=Liste des applications favorites de l'utilisateur actuel
diff --git a/app-center-webapps/src/main/resources/locale/portlet/MyApplications_en.properties b/app-center-webapps/src/main/resources/locale/portlet/MyApplications_en.properties
new file mode 100644
index 000000000..c4af8c63f
--- /dev/null
+++ b/app-center-webapps/src/main/resources/locale/portlet/MyApplications_en.properties
@@ -0,0 +1,15 @@
+myApplications.name.label=Shortcuts
+myApplications.add.application.label=Start adding app
+myApplications.add.application.tooltip=Add app
+myApplications.seeMore.label=See more
+myApplications.edit.settings.title=Edit Shortcuts Apps
+myApplications.settings.cancel.label=Cancel
+myApplications.settings.save.label=Save
+myApplications.displayOptions.label=Display Options
+myApplications.numberToList.label=Number to list
+myApplications.addHeader.label=Add a header
+myApplications.editSettings.tooltip=Edit Settings
+myApplications.seeMore.tooltip=See more
+myApplications.header.translation.title=Add header translation
+myApplications.settings.save.success.message=My applications settings saved successfully
+myApplications.settings.error.success.message=Error while saving my applications settings
diff --git a/app-center-webapps/src/main/resources/locale/portlet/MyApplications_fr.properties b/app-center-webapps/src/main/resources/locale/portlet/MyApplications_fr.properties
new file mode 100644
index 000000000..0cefa47c7
--- /dev/null
+++ b/app-center-webapps/src/main/resources/locale/portlet/MyApplications_fr.properties
@@ -0,0 +1,13 @@
+myApplications.name.label=Raccourcis
+myApplications.add.application.label=Commencez \u00e0 ajouter une application
+myApplications.add.application.tooltip=Ajouter une application
+myApplications.seeMore.label=Voir plus
+myApplications.edit.settings.title=Modifier les applications de raccourcis
+myApplications.settings.cancel.label=Annuler
+myApplications.settings.save.label=Enregistrer
+myApplications.displayOptions.label=Options d'affichage
+myApplications.numberToList.label=Nombre \u00e0 lister
+myApplications.addHeader.label=Ajouter un en-t\u00eate
+myApplications.editSettings.tooltip=Modifier les param\u00e8tres
+myApplications.seeMore.tooltip=Voir plus
+myApplications.header.translation.title=Ajouter une traduction d'en-t\u00ea?te
diff --git a/app-center-webapps/src/main/webapp/WEB-INF/gatein-resources.xml b/app-center-webapps/src/main/webapp/WEB-INF/gatein-resources.xml
index 2d41f1bc1..d88ab0c58 100644
--- a/app-center-webapps/src/main/webapp/WEB-INF/gatein-resources.xml
+++ b/app-center-webapps/src/main/webapp/WEB-INF/gatein-resources.xml
@@ -44,13 +44,6 @@
     <additional-module>appCenter</additional-module>
   </portlet-skin>
 
-  <portlet-skin>
-    <application-name>app-center</application-name>
-    <portlet-name>AppCenterMyApplicationsPortlet</portlet-name>
-    <skin-name>Enterprise</skin-name>
-    <additional-module>appCenter</additional-module>
-  </portlet-skin>
-
   <portlet-skin>
     <application-name>app-center</application-name>
     <portlet-name>AppCenterAppLauncherPortlet</portlet-name>
@@ -103,6 +96,10 @@
   <portlet>
     <name>AppCenterMyApplicationsPortlet</name>
     <module>
+      <script>
+        <minify>false</minify>
+        <path>/javascript/vue/myApplications.bundle.js</path>
+      </script>
       <depends>
         <module>vue</module>
       </depends>
@@ -116,7 +113,13 @@
         <module>commonVueComponents</module>
       </depends>
       <depends>
-        <module>myApplicationsBundle</module>
+        <module>extensionRegistry</module>
+      </depends>
+      <depends>
+        <module>translationField</module>
+      </depends>
+      <depends>
+        <module>vueDraggable</module>
       </depends>
     </module>
   </portlet>
@@ -170,29 +173,6 @@
     </depends>
   </module>
 
-  <module>
-    <name>myApplicationsBundle</name>
-    <script>
-      <minify>false</minify>
-      <path>/javascript/vue/myApplications.bundle.js</path>
-    </script>
-    <depends>
-      <module>vue</module>
-    </depends>
-    <depends>
-      <module>vuetify</module>
-    </depends>
-    <depends>
-      <module>eXoVueI18n</module>
-    </depends>
-    <depends>
-      <module>commonVueComponents</module>
-    </depends>
-    <depends>
-      <module>jquery</module>
-    </depends>
-  </module>
-
   <module>
     <name>appLauncherBundle</name>
     <script>
diff --git a/app-center-webapps/src/main/webapp/WEB-INF/jsp/appCenter/myApplications.jsp b/app-center-webapps/src/main/webapp/WEB-INF/jsp/appCenter/myApplications.jsp
new file mode 100644
index 000000000..22246ef0c
--- /dev/null
+++ b/app-center-webapps/src/main/webapp/WEB-INF/jsp/appCenter/myApplications.jsp
@@ -0,0 +1,38 @@
+<%@ page import="javax.portlet.PortletPreferences" %>
+<%@ page import="org.exoplatform.commons.utils.CommonsUtils" %>
+<%@ page import="org.exoplatform.portal.localization.LocaleContextInfoUtils" %>
+<%@ page import="io.meeds.social.translation.service.TranslationService" %>
+<%@ taglib uri="http://java.sun.com/portlet_2_0" prefix="portlet" %>
+<portlet:defineObjects/>
+<portlet:actionURL var="saveSettingsUrl" />
+<%
+  long applicationId;
+  String objectType = "myApplicationsPortlet";
+  String fieldName  = "headerTitle";
+  Object applicationIdParam = request.getAttribute("applicationId");
+  applicationId = Long.parseLong((applicationIdParam instanceof String[]) ? ((String[]) applicationIdParam)[0]
+          : (String) applicationIdParam);
+  boolean isAdmin = (boolean) request.getAttribute("isAdmin");
+
+  PortletPreferences preferences = renderRequest.getPreferences();
+  int maxAppsToList = Integer.parseInt(preferences.getValue("maxAppsToList", "4"));
+  boolean showHeader = Boolean.parseBoolean(preferences.getValue("showHeader", "true"));
+  String headerTitle = CommonsUtils.getService(TranslationService.class).getTranslationLabelOrDefault(objectType,
+          applicationId, fieldName, LocaleContextInfoUtils.getUserLocale(request.getRemoteUser()));
+%>
+<div class="VuetifyApp">
+  <div data-app="true"
+    class="v-application v-application--is-ltr theme--light"
+    id="myApplications">
+    <script type="text/javascript">
+      require(['PORTLET/app-center/AppCenterMyApplicationsPortlet'], app => app.init({
+        applicationId: '<%=applicationId%>',
+        maxAppsToList: '<%=maxAppsToList%>',
+        showHeader: <%=showHeader%>,
+        headerTitle: '<%=headerTitle%>' !== 'null' ? '<%=headerTitle%>' : null,
+        isAdmin: <%=isAdmin%>,
+        saveSettingsUrl: '<%=saveSettingsUrl%>'
+      }));
+    </script>
+  </div>
+</div>
diff --git a/app-center-webapps/src/main/webapp/WEB-INF/portlet.xml b/app-center-webapps/src/main/webapp/WEB-INF/portlet.xml
index ea2403a69..ec4b7f217 100644
--- a/app-center-webapps/src/main/webapp/WEB-INF/portlet.xml
+++ b/app-center-webapps/src/main/webapp/WEB-INF/portlet.xml
@@ -96,14 +96,14 @@
   <portlet>
     <portlet-name>AppCenterMyApplicationsPortlet</portlet-name>
     <display-name xml:lang="EN">App center my applications portlet</display-name>
-    <portlet-class>org.exoplatform.commons.api.portlet.GenericDispatchedViewPortlet</portlet-class>
+    <portlet-class>io.meeds.appcenter.portlet.MyApplicationsPortlet</portlet-class>
     <init-param>
       <name>portlet-view-dispatched-file-path</name>
-      <value>/html/myApplications.html</value>
+      <value>/WEB-INF/jsp/appCenter/myApplications.jsp</value>
     </init-param>
     <init-param>
       <name>preload.resource.bundles</name>
-      <value>locale.addon.appcenter</value>
+      <value>locale.portlet.MyApplications</value>
     </init-param>
     <init-param>
       <name>preload.resource.rest</name>
diff --git a/app-center-webapps/src/main/webapp/html/myApplications.html b/app-center-webapps/src/main/webapp/html/myApplications.html
deleted file mode 100644
index 28c327813..000000000
--- a/app-center-webapps/src/main/webapp/html/myApplications.html
+++ /dev/null
@@ -1,9 +0,0 @@
-<div class="VuetifyApp">
-  <div id="myApplications">
-    <script>
-      require(['SHARED/myApplicationsBundle'], function(myApplicationsPortletApp) {
-        myApplicationsPortletApp.init();
-      });
-    </script>
-  </div>
-</div>
\ No newline at end of file
diff --git a/app-center-webapps/src/main/webapp/images/portlets/myApplicationsPortlet.png b/app-center-webapps/src/main/webapp/images/portlets/myApplicationsPortlet.png
new file mode 100644
index 000000000..48b4314ef
Binary files /dev/null and b/app-center-webapps/src/main/webapp/images/portlets/myApplicationsPortlet.png differ
diff --git a/app-center-webapps/src/main/webapp/vue-apps/myApplications/components/MyApplications.vue b/app-center-webapps/src/main/webapp/vue-apps/myApplications/components/MyApplications.vue
deleted file mode 100644
index 894b6792d..000000000
--- a/app-center-webapps/src/main/webapp/vue-apps/myApplications/components/MyApplications.vue
+++ /dev/null
@@ -1,115 +0,0 @@
-<!--
-This file is part of the Meeds project (https://meeds.io/).
-Copyright (C) 2020 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.
--->
-<template>
-  <div class="myTools">
-    <div class="myToolsHeader">
-      <span>{{ $t("appCenter.userSetup.shortcuts") }}</span>
-      <a class="seeAll" href="/portal/intranet/appCenterUserSetup">{{
-        $t("appCenter.userSetup.seeAll")
-      }}</a>
-    </div>
-    <ul class="myToolsList">
-      <li v-for="favoriteApp in favoriteApplicationsList" :key="favoriteApp.id">
-        <a
-          :href="favoriteApp.computedUrl"
-          :target="favoriteApp.target">
-          <img
-            class="myToolImage"
-            referrerpolicy="no-referrer"
-            :src="`/app-center/rest/applications/illustration/${favoriteApp.id}?v=${favoriteApp.imageLastModified}`">
-          <span class="myToolTitle tooltipContent">
-            <div class="text-body">{{ favoriteApp.title }}</div>
-            <span class="tooltiptext text-subtitle">{{ favoriteApp.description }}</span>
-          </span>
-        </a>
-      </li>
-    </ul>
-    <div
-      v-if="
-        !maxFavoriteApps || favoriteApplicationsList.length < maxFavoriteApps
-      "
-      class="addTool">
-      <a :href="appCenterUserSetupLink">
-        <i class="uiIconPlus uiIconLightGray"></i>
-      </a>
-    </div>
-  </div>
-</template>
-
-<script>
-export default {
-  name: 'MyTools',
-  data() {
-    return {
-      favoriteApplicationsList: [],
-      maxFavoriteApps: '',
-      appCenterUserSetupLink: ''
-    };
-  },
-
-  created() {
-    Promise.all([
-      this.getFavoriteApplicationsList(),
-      this.getMaxFavoriteApps()
-    ]).finally(() => this.$root.$applicationLoaded());
-    this.appCenterUserSetupLink = `${eXo.env.portal.context}/${eXo.env.portal.portalName}/appCenterUserSetup`;
-  },
-
-  methods: {
-    getFavoriteApplicationsList() {
-      return fetch('/app-center/rest/favorites', {
-        credentials: 'include',
-        method: 'GET'
-      })
-        .then(resp => {
-          if (resp && resp.ok) {
-            return resp.json();
-          } else {
-            throw new Error(
-              'Error when getting the favorite applications list'
-            );
-          }
-        })
-        .then(data => {
-          this.favoriteApplicationsList = data && data.applications || [];
-          this.favoriteApplicationsList.forEach(app => {
-            app.computedUrl = app.url.replace(/^\.\//, `${eXo.env.portal.context}/${eXo.env.portal.portalName}/`);
-            app.computedUrl = app.computedUrl.replace('@user@', eXo.env.portal.userName);
-            app.target = app.computedUrl.indexOf('/') === 0 ? '_self' : '_blank';
-          });
-          return this.favoriteApplicationsList;
-        });
-    },
-    getMaxFavoriteApps() {
-      return fetch('/app-center/rest/settings', {
-        method: 'GET',
-        credentials: 'include',
-      })
-        .then(resp => {
-          if (resp && resp.ok) {
-            return resp.json();
-          } else {
-            throw new Error('Error when getting the general applications list');
-          }
-        })
-        .then(data => {
-          this.maxFavoriteApps = data.maxFavoriteApps;
-        });
-    }
-  }
-};
-</script>
diff --git a/app-center-webapps/src/main/webapp/vue-apps/myApplications/components/MyApplicationsApp.vue b/app-center-webapps/src/main/webapp/vue-apps/myApplications/components/MyApplicationsApp.vue
new file mode 100644
index 000000000..57a944bfe
--- /dev/null
+++ b/app-center-webapps/src/main/webapp/vue-apps/myApplications/components/MyApplicationsApp.vue
@@ -0,0 +1,184 @@
+<!--
+ * This file is part of the Meeds project (https://meeds.io/).
+ *
+ * Copyright (C) 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.
+-->
+
+<template>
+  <v-app id="myApplications">
+    <v-hover v-slot="{hover}">
+      <widget-wrapper
+        :loading="isLoading"
+        extra-class="application-body position-static border-box-sizing">
+        <my-applications-toolbar
+          v-if="!isLoading"
+          :hover="hover"
+          :is-admin="isAdmin"
+          :show-header="showHeader"
+          :header-title="headerTitle"
+          :has-applications="hasApplications"
+          @open-settings="openSettingsDrawer" />
+        <my-applications-list
+          :applications-list="filteredApplications"
+          :default-app-image="defaultAppImage"
+          :is-loading="isLoading"
+          @list-updated="handleListOrderUpdate" />
+      </widget-wrapper>
+    </v-hover>
+    <my-applications-settings-drawer
+      v-if="isAdmin"
+      :settings="$root.settings"
+      ref="settingsDrawer"
+      @settings-updated="settingsUpdated" />
+  </v-app>
+</template>
+
+<script>
+
+export default {
+  data() {
+    return {
+      favoriteApplications: [],
+      applicationsOrder: {},
+      defaultAppImage: null,
+      isLoading: false,
+      alphabeticalOrder: true,
+      baseUrl: `${eXo.env.portal.context}/${eXo.env.portal.portalName}/`,
+      currentUser: eXo.env.portal.userName,
+      initialized: false,
+      mobileDevices: /Android|webOS|iPhone|iPad|iPod|BlackBerry|Windows Phone/i
+    };
+  },
+  computed: {
+    isAdmin() {
+      return this.$root.settings?.isAdmin;
+    },
+    filteredApplications() {
+      return this.isMobileDevice && this.favoriteApplications.filter(application => application.mobile)
+                                 || this.favoriteApplications;
+    },
+    isMobileDevice() {
+      return this.mobileDevices.test(navigator.userAgent);
+    },
+    hasApplications() {
+      return this.favoriteApplications?.length > 0;
+    },
+    showHeader() {
+      return this.$root.settings?.showHeader;
+    },
+    headerTitle() {
+      return this.$root.settings?.headerTitle;
+    },
+    maxAppsToList() {
+      return this.$root.settings.maxAppsToList;
+    }
+  },
+  created() {
+    this.getFavoriteApplications();
+    this.$root.isLoading = true;
+  },
+  methods: {
+    openSettingsDrawer() {
+      this.$refs.settingsDrawer.open();
+    },
+    settingsUpdated(settings, headerTitle) {
+      const updateList = Number(this.maxAppsToList) !== settings.maxAppsToList;
+      this.$root.settings.maxAppsToList = settings.maxAppsToList;
+      this.$root.settings.showHeader = settings.showHeader;
+      this.$root.settings.headerTitle = headerTitle;
+      this.$refs.settingsDrawer.close();
+      if (updateList) {
+        this.getFavoriteApplications();
+      }
+    },
+    getFavoriteApplications() {
+      this.isLoading = true;
+      return this.$myApplicationsService.getFavoriteApplications(this.maxAppsToList)
+        .then((data) => {
+          this.favoriteApplications = (data?.applications || [])
+            .map(app => this.mapApplication(app))
+            .filter(app => !!app);
+          this.defaultAppImage = data?.defaultApplicationImage;
+          this.sortAndStoreApplicationsOrder();
+        })
+        .finally(() => {
+          this.isLoading = false;
+          this.initialized = true;
+        });
+    },
+    mapApplication(app) {
+      if (!app) {return null;}
+      const computedApp = this.computeApplicationUrl(app);
+      this.i18nSystemApplicationTitle(computedApp);
+      return computedApp;
+    },
+    sortAndStoreApplicationsOrder() {
+      this.alphabeticalOrder = !this.favoriteApplications.some(app => app.order !== null);
+
+      this.favoriteApplications.sort((a, b) => {
+        const orderDiff = (a.order ?? Infinity) - (b.order ?? Infinity);
+        return orderDiff || a.title.localeCompare(b.title);
+      });
+
+      this.applicationsOrder = this.favoriteApplications.reduce((orderMap, app, index) => {
+        if (app && app.id) {
+          orderMap[app.id] = index;
+        }
+        return orderMap;
+      }, {});
+    },
+    computeApplicationUrl(app) {
+      const computedUrl = app.url.replace(/^\.\//, this.baseUrl)
+        .replace('@user@', this.currentUser);
+      const target = computedUrl.startsWith('/') ? '_self' : '_blank';
+      return { ...app, computedUrl, target };
+    },
+    i18nSystemApplicationTitle(app) {
+      if (app.system) {
+        const appTitleKey = app.title.includes(' ')
+          ? app.title.replace(/ /g, '.').toLowerCase()
+          : app.title.toLowerCase();
+
+        const localizedTitle = this.$t(`appCenter.system.application.${appTitleKey}`);
+        if (!localizedTitle.startsWith('appCenter.system.application')) {
+          app.title = localizedTitle;
+        }
+      }
+    },
+    async updateApplicationsOrder(applicationList) {
+      const newApplicationsOrders = [];
+      for (const [index, app] of applicationList.entries()) {
+        const currentOrder = this.applicationsOrder[`${app.id}`];
+        if (currentOrder !== index) {
+          this.applicationsOrder[`${app.id}`] = index;
+          newApplicationsOrders.push({id: app.id, order: index});
+        }
+      }
+      if (newApplicationsOrders.length) {
+        await this.$myApplicationsService.updateApplicationsOrder(newApplicationsOrders);
+        this.favoriteApplications = [...applicationList];
+      }
+    },
+    handleListOrderUpdate(applicationList) {
+      if (!this.initialized) {
+        return;
+      }
+      this.updateApplicationsOrder(applicationList);
+    }
+  }
+};
+</script>
diff --git a/app-center-webapps/src/main/webapp/vue-apps/myApplications/components/settings/MyApplicationsSettingsDrawer.vue b/app-center-webapps/src/main/webapp/vue-apps/myApplications/components/settings/MyApplicationsSettingsDrawer.vue
new file mode 100644
index 000000000..e45f99b8e
--- /dev/null
+++ b/app-center-webapps/src/main/webapp/vue-apps/myApplications/components/settings/MyApplicationsSettingsDrawer.vue
@@ -0,0 +1,183 @@
+<!--
+ * This file is part of the Meeds project (https://meeds.io/).
+ *
+ * Copyright (C) 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.
+-->
+
+<template>
+  <exo-drawer
+    id="myApplicationsSettingsDrawer"
+    ref="myApplicationsSettingsDrawer"
+    :right="!$vuetify.rtl"
+    @closed="reset">
+    <template #title>
+      <div class="text-truncate text-header-title font-weight-bold text-color">
+        {{ $t('myApplications.edit.settings.title') }}
+      </div>
+    </template>
+    <template #content>
+      <v-form ref="form">
+        <div class="pa-5">
+          <div class="text-header">
+            {{ $t('myApplications.displayOptions.label') }}
+          </div>
+          <div class="d-flex align-center">
+            <label class="v-label mt-2 text-color">
+              {{ $t('myApplications.numberToList.label') }}
+            </label>
+            <div class="ms-auto">
+              <number-input
+                v-model="maxAppsToList"
+                :min="1"
+                :max="100"
+                :step="1"
+                editable />
+            </div>
+          </div>
+          <div class="d-flex mt-1 align-center justify-space-between">
+            <label class="v-label text-color align-start">
+              {{ $t('myApplications.addHeader.label') }}
+            </label>
+            <div class="align-end">
+              <v-switch
+                v-model="showHeader"
+                :disabled="isSaving"
+                color="primary"
+                class="pa-0 my-auto"
+                hide-details />
+            </div>
+          </div>
+          <translation-text-field
+            v-if="showHeader"
+            :object-id="applicationId"
+            :object-type="objectType"
+            :field-name="fieldName"
+            :field-value="displayedValue"
+            :drawer-title="$t('myApplications.header.translation.title')"
+            class="mt-2"
+            no-expand-icon
+            back-icon
+            required
+            @update:field-value="updateFieldValue"
+            @input="translationUpdated" />
+        </div>
+      </v-form>
+    </template>
+    <template #footer>
+      <div class="d-flex width-fit-content ms-auto">
+        <v-btn
+          class="btn me-5"
+          @click="reset">
+          {{ $t('myApplications.settings.cancel.label') }}
+        </v-btn>
+        <v-btn
+          class="btn btn-primary"
+          :disabled="!saveEnabled"
+          :loading="isSaving"
+          @click="save">
+          {{ $t('myApplications.settings.save.label') }}
+        </v-btn>
+      </div>
+    </template>
+  </exo-drawer>
+</template>
+
+<script>
+export default {
+  data() {
+    return {
+      isSaving: false,
+      showHeader: true,
+      maxAppsToList: 4,
+      objectType: 'myApplicationsPortlet',
+      fieldName: 'headerTitle',
+      translations: [],
+      userLocale: eXo.env.portal.language,
+      defaultLangValue: this.$t('myApplications.name.label'),
+      translationsInitialized: false,
+      currentTranslations: []
+    };
+  },
+  props: {
+    settings: {
+      type: Object,
+      default: null
+    }
+  },
+  computed: {
+    saveEnabled() {
+      return this.settings.showHeader !== this.showHeader || Number(this.settings.maxAppsToList) !== this.maxAppsToList
+          || JSON.stringify(this.currentTranslations) !== JSON.stringify(this.translations);
+    },
+    applicationId() {
+      return this.settings.applicationId;
+    },
+    saveSettingsUrl() {
+      return this.settings?.saveSettingsUrl;
+    },
+    displayedValue() {
+      return this.translations?.[this.userLocale] || this.defaultLangValue;
+    },
+  },
+  methods: {
+    translationUpdated(translations) {
+      this.translations = translations;
+      if (!this.translationsInitialized) {
+        this.currentTranslations = structuredClone(this.translations);
+        this.translationsInitialized = true;
+      }
+    },
+    updateFieldValue(value) {
+      this.defaultLangValue = value;
+    },
+    open() {
+      this.restoreSavedSettings();
+      this.$refs.myApplicationsSettingsDrawer.open();
+    },
+    close() {
+      this.$refs.myApplicationsSettingsDrawer.close();
+    },
+    save() {
+      const settings = {
+        maxAppsToList: this.maxAppsToList,
+        showHeader: this.showHeader
+      };
+      this.$myApplicationsService.saveSettings(this.saveSettingsUrl, settings).then(() => {
+        this.saveHeaderTranslations();
+        this.$emit('settings-updated', settings, this.displayedValue);
+        this.$root.$emit('alert-message', this.$t('myApplications.settings.save.success.message'), 'success');
+      }).catch(() => {
+        this.$root.$emit('alert-message', this.$t('myApplications.settings.save.error.message'), 'error');
+      });
+    },
+    async saveHeaderTranslations() {
+      if (this.showHeader) {
+        await this.$translationService.saveTranslations(this.objectType, this.applicationId, this.fieldName, this.translations);
+        this.currentTranslations = structuredClone(this.translations);
+      }
+    },
+    reset() {
+      this.restoreSavedSettings();
+      this.close();
+    },
+    restoreSavedSettings() {
+      this.maxAppsToList = Number(this.settings.maxAppsToList);
+      this.showHeader = this.settings.showHeader;
+    }
+  }
+};
+</script>
diff --git a/app-center-webapps/src/main/webapp/vue-apps/myApplications/components/view/MyApplicationItem.vue b/app-center-webapps/src/main/webapp/vue-apps/myApplications/components/view/MyApplicationItem.vue
new file mode 100644
index 000000000..26a32db8d
--- /dev/null
+++ b/app-center-webapps/src/main/webapp/vue-apps/myApplications/components/view/MyApplicationItem.vue
@@ -0,0 +1,98 @@
+<!--
+ * This file is part of the Meeds project (https://meeds.io/).
+ *
+ * Copyright (C) 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.
+-->
+
+<template>
+  <v-hover v-slot="{hover}">
+    <v-card
+      :id="`App${application.id}`"
+      :title="applicationDescription"
+      :href="applicationUrl"
+      :target="applicationUrlTarget"
+      :class="{'background-grey-primary': hover}"
+      class="pa-4 d-flex flex-column align-center"
+      :width="$attrs.width"
+      :max-width="$attrs['max-width']"
+      :min-height="$attrs['min-height']"
+      :max-height="$attrs['max-height']"
+      link
+      flat>
+      <v-img
+        :src="appImageUrl"
+        :alt="applicationTitle"
+        max-width="60"
+        max-height="60"
+        contain
+        class="mx-auto"
+        referrerpolicy="no-referrer" />
+      <v-tooltip
+        bottom>
+        <template #activator="{ on, attrs }">
+          <span
+            class="text-truncate-2 mt-2 text-color"
+            v-bind="attrs"
+            v-on="on">
+            {{ applicationTitle }}
+          </span>
+        </template>
+        <span>{{ applicationTitle }}</span>
+      </v-tooltip>
+    </v-card>
+  </v-hover>
+</template>
+
+<script>
+export default {
+  data() {
+    return {
+      illustrationBaseUrl: '/app-center/rest/applications/illustration/'
+    };
+  },
+  props: {
+    application: {
+      type: Object,
+      default: null
+    },
+    defaultAppImage: {
+      type: Object,
+      default: null
+    }
+  },
+  computed: {
+    appImageUrl() {
+      const { imageFileId, imageFileName, id, imageLastModified } = this.application;
+      return (imageFileId && imageFileName) || this.defaultAppImage?.fileBody
+        ? `${this.illustrationBaseUrl}${id}?v=${imageLastModified}`
+        : '/app-center/skin/images/defaultApp.png';
+    },
+    applicationUrl() {
+      return this.application?.computedUrl;
+    },
+    applicationUrlTarget() {
+      return this.application?.target;
+    },
+    applicationTitle() {
+      return this.application?.title;
+    },
+    applicationDescription() {
+      return this.application?.description;
+    }
+  }
+};
+</script>
diff --git a/app-center-webapps/src/main/webapp/vue-apps/myApplications/components/view/MyApplicationsList.vue b/app-center-webapps/src/main/webapp/vue-apps/myApplications/components/view/MyApplicationsList.vue
new file mode 100644
index 000000000..98a187061
--- /dev/null
+++ b/app-center-webapps/src/main/webapp/vue-apps/myApplications/components/view/MyApplicationsList.vue
@@ -0,0 +1,83 @@
+<!--
+ * This file is part of the Meeds project (https://meeds.io/).
+ *
+ * Copyright (C) 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.
+-->
+
+<template>
+  <div
+    v-if="!applicationsList.length && !isLoading"
+    class="d-flex flex-column justify-center align-center flex-grow-1">
+    <v-icon size="60" class="secondary--text mb-2">
+      fas fa-th
+    </v-icon>
+    <p class="mt-2 mb-0 text-sub-title">
+      {{ $t('myApplications.add.application.label') }}
+    </p>
+  </div>
+  <v-layout
+    v-else>
+    <component
+      :is="isMobile && 'div' || 'draggable'"
+      v-model="applicationsList"
+      :item-key="'id'"
+      class="d-flex flex-wrap flex-grow-0 justify-start"
+      @start="onDragStart"
+      @end="onDragEnd">
+      <my-application-item
+        v-for="application in applicationsList"
+        :key="application.id"
+        :application="application"
+        :default-app-image="defaultAppImage"
+        :width="125"
+        :max-width="125"
+        :min-height="125"
+        :max-height="150"
+        class="d-flex ma-0 flex-grow-1" />
+    </component>
+  </v-layout>
+</template>
+
+<script>
+export default {
+  props: {
+    applicationsList: {
+      type: Array,
+      default: () => []
+    },
+    defaultAppImage: {
+      type: Object,
+      default: null
+    },
+    isLoading: {
+      type: Boolean,
+      default: false
+    }
+  },
+  computed: {
+    isMobile() {
+      return this.$vuetify.breakpoint.smAndDown;
+    }
+  },
+  methods: {
+    onDragEnd() {
+      this.$emit('list-updated', this.applicationsList);
+    }
+  }
+};
+</script>
+
diff --git a/app-center-webapps/src/main/webapp/vue-apps/myApplications/components/view/MyApplicationsToolbar.vue b/app-center-webapps/src/main/webapp/vue-apps/myApplications/components/view/MyApplicationsToolbar.vue
new file mode 100644
index 000000000..723c7e919
--- /dev/null
+++ b/app-center-webapps/src/main/webapp/vue-apps/myApplications/components/view/MyApplicationsToolbar.vue
@@ -0,0 +1,150 @@
+<!--
+ * This file is part of the Meeds project (https://meeds.io/).
+ *
+ * Copyright (C) 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.
+-->
+
+<template>
+  <div class="d-flex align-center justify-space-between">
+    <div
+      v-if="showHeader"
+      class="widget-text-header align-start">
+      {{ headerLabel }}
+    </div>
+    <div v-else class="flex-grow-1"></div>
+    <div
+      class="d-flex align-center ms-auto">
+      <template v-if="hasApplications">
+        <v-btn
+          v-if="!hover || !isAdmin"
+          :href="userAppSetupUrl"
+          color="primary"
+          target="_self"
+          class="pa-0 text-font-size"
+          small
+          text
+          link>
+          <span class="primary--text text-none">
+            {{ $t('myApplications.seeMore.label') }}
+          </span>
+        </v-btn>
+        <v-tooltip
+          v-else
+          bottom>
+          <template #activator="{ on, attrs }">
+            <v-btn
+              :href="userAppSetupUrl"
+              target="_self"
+              color="primary"
+              class="ms-2 text-font-size"
+              small
+              link
+              icon
+              v-bind="attrs"
+              v-on="on">
+              <v-icon
+                :size="18"
+                class="icon-default-size">
+                fas fa-external-link-alt
+              </v-icon>
+            </v-btn>
+          </template>
+          {{ $t('myApplications.seeMore.tooltip') }}
+        </v-tooltip>
+      </template>
+      <v-tooltip
+        v-if="hover && isAdmin"
+        bottom>
+        <template #activator="{ on, attrs }">
+          <v-btn
+            class="ms-2 text-font-size"
+            small
+            link
+            icon
+            v-bind="attrs"
+            v-on="on"
+            @click="$emit('open-settings')">
+            <v-icon
+              :size="18"
+              class="icon-default-size icon-default-color">
+              fas fa-cog
+            </v-icon>
+          </v-btn>
+        </template>
+        {{ $t('myApplications.editSettings.tooltip') }}
+      </v-tooltip>
+      <v-tooltip
+        v-if="!hasApplications"
+        bottom>
+        <template #activator="{ on, attrs }">
+          <v-btn
+            :href="userAppSetupUrl"
+            target="_self"
+            class="ms-2 text-font-size"
+            small
+            link
+            icon
+            v-bind="attrs"
+            v-on="on">
+            <v-icon
+              :size="18"
+              class="icon-default-color icon-default-size">
+              fas fa-plus
+            </v-icon>
+          </v-btn>
+        </template>
+        {{ $t('myApplications.add.application.tooltip') }}
+      </v-tooltip>
+    </div>
+  </div>
+</template>
+<script>
+export default {
+  data() {
+    return {
+      userAppSetupUrl: `${eXo.env.portal.context}/${eXo.env.portal.portalName}/appCenterUserSetup`,
+    };
+  },
+  props: {
+    hasApplications: {
+      type: Boolean,
+      default: false
+    },
+    hover: {
+      type: Boolean,
+      default: false
+    },
+    isAdmin: {
+      type: Boolean,
+      default: false
+    },
+    headerTitle: {
+      type: String,
+      default: null
+    },
+    showHeader: {
+      type: Boolean,
+      default: true
+    }
+  },
+  computed: {
+    headerLabel() {
+      return this.headerTitle || this.$t('myApplications.name.label');
+    }
+  }
+};
+</script>
diff --git a/app-center-webapps/src/main/webapp/vue-apps/myApplications/initComponents.js b/app-center-webapps/src/main/webapp/vue-apps/myApplications/initComponents.js
index 16bcde50a..41a62cd92 100644
--- a/app-center-webapps/src/main/webapp/vue-apps/myApplications/initComponents.js
+++ b/app-center-webapps/src/main/webapp/vue-apps/myApplications/initComponents.js
@@ -1,29 +1,45 @@
 /*
- * This file is part of the Meeds project (https://meeds.io/).
- * Copyright (C) 2020 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 UserAuthorizedApplications from './UserAuthorizedApplications.vue';
-import UserFavoriteApplications from './UserFavoriteApplications.vue';
-import AppCenterUserSetup from './UserSetup.vue';
+  * This file is part of the Meeds project (https://meeds.io/).
+  *
+  * Copyright (C) 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 MyApplicationsApp from './components/MyApplicationsApp.vue';
+import MyApplicationsList from './components/view/MyApplicationsList.vue';
+import MyApplicationsToolbar from './components/view/MyApplicationsToolbar.vue';
+import MyApplicationItem from './components/view/MyApplicationItem.vue';
+import MyApplicationsSettingsDrawer from './components/settings/MyApplicationsSettingsDrawer.vue';
+
+import * as myApplicationsService from './myApplicationsService.js';
 
 const components = {
-  'user-authorizedApplications': UserAuthorizedApplications,
-  'user-favoriteApplications': UserFavoriteApplications,
-  'app-center-user-setup': AppCenterUserSetup,
+  'my-applications-app': MyApplicationsApp,
+  'my-applications-list': MyApplicationsList,
+  'my-applications-toolbar': MyApplicationsToolbar,
+  'my-application-item': MyApplicationItem,
+  'my-applications-settings-drawer': MyApplicationsSettingsDrawer
 };
 
 for (const key in components) {
   Vue.component(key, components[key]);
 }
+
+if (!Vue.prototype.$myApplicationsService) {
+  window.Object.defineProperty(Vue.prototype, '$myApplicationsService', {
+    value: myApplicationsService,
+  });
+}
+
diff --git a/app-center-webapps/src/main/webapp/vue-apps/myApplications/main.js b/app-center-webapps/src/main/webapp/vue-apps/myApplications/main.js
index 2c5c5a0e8..69dc5e0d3 100644
--- a/app-center-webapps/src/main/webapp/vue-apps/myApplications/main.js
+++ b/app-center-webapps/src/main/webapp/vue-apps/myApplications/main.js
@@ -1,32 +1,52 @@
 /*
- * This file is part of the Meeds project (https://meeds.io/).
- * Copyright (C) 2020 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 MyApplicationsApp from './components/MyApplications.vue';
+* This file is part of the Meeds project (https://meeds.io/).
+*
+* Copyright (C) 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';
 
-//should expose the locale ressources as REST API
-const lang = eXo && eXo.env && eXo.env.portal && eXo.env.portal.language || 'en';
-const url = `/app-center/i18n/locale.addon.appcenter?lang=${lang}`;
+// get overridden components if exists
+if (extensionRegistry) {
+  const components = extensionRegistry.loadComponents('MyApplications');
+  if (components && components.length > 0) {
+    components.forEach(cmp => {
+      Vue.component(cmp.componentName, cmp.componentOptions);
+    });
+  }
+}
+
+const lang = eXo?.env?.portal?.language || 'en';
+const url = `/app-center/i18n/locale.portlet.MyApplications?lang=${lang}`;
+const vuetify = Vue.prototype.vuetifyOptions;
+const appId = 'myApplications';
 
-export function init() {
+export function init(settings) {
   exoi18n.loadLanguageAsync(lang, url).then(i18n => {
     Vue.createApp({
-      render: h => h(MyApplicationsApp),
-      i18n,
-      vuetify: Vue.prototype.vuetifyOptions,
-    }, '#myApplications', 'My Application Center');
+      data() {
+        return {
+          settings: settings
+        };
+      },
+      template: '<my-applications-app />',
+      vuetify,
+      i18n
+    }, `#${appId}`, 'My Application Center');
   });
 }
+
diff --git a/app-center-webapps/src/main/webapp/vue-apps/myApplications/myApplicationsService.js b/app-center-webapps/src/main/webapp/vue-apps/myApplications/myApplicationsService.js
new file mode 100644
index 000000000..3ccee100e
--- /dev/null
+++ b/app-center-webapps/src/main/webapp/vue-apps/myApplications/myApplicationsService.js
@@ -0,0 +1,76 @@
+/*
+* This file is part of the Meeds project (https://meeds.io/).
+*
+* Copyright (C) 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.
+*/
+
+export function getFavoriteApplications(size) {
+  return fetch(`/app-center/rest/favorites?size=${size}`, {
+    credentials: 'include',
+    method: 'GET'
+  })
+    .then(resp => {
+      if (resp?.ok) {
+        return resp.json();
+      } else {
+        throw new Error(
+          'Error when getting the favorite applications list'
+        );
+      }
+    });
+}
+
+export async function updateApplicationsOrder(applicationsOrder) {
+  try {
+    const response = await fetch('/app-center/rest/favorites', {
+      headers: {
+        'Content-Type': 'application/json',
+      },
+      credentials: 'include',
+      method: 'PUT',
+      body: JSON.stringify(applicationsOrder),
+    });
+    if (!response.ok) {
+      console.error(`Failed to update applications order, status: ${response.status}`);
+    }
+    return true;
+  } catch (error) {
+    console.error('Error updating applications order:', error);
+    return false;
+  }
+}
+
+export function saveSettings(saveSettingsURL, settings) {
+  const formData = new FormData();
+  if (settings) {
+    Object.keys(settings).forEach(name => {
+      formData.append(name, settings[name]);
+    });
+  }
+  return fetch(saveSettingsURL.replaceAll('&amp;', '&'), {
+    method: 'POST',
+    credentials: 'include',
+    headers: {
+      'Content-Type': 'application/x-www-form-urlencoded',
+    },
+    body: new URLSearchParams(formData).toString(),
+  }).then(resp => {
+    if (!resp.ok) {
+      throw new Error('Error while saving my applications settings');
+    }
+  });
+}
diff --git a/crowdin.yml b/crowdin.yml
index b33310d11..3c312d232 100644
--- a/crowdin.yml
+++ b/crowdin.yml
@@ -42,4 +42,22 @@ files: [
     "escape_special_characters": 0,
     "escape_quotes" : 0,
   },
+  {
+    "source": "/app-center-webapps/src/main/resources/locale/portlet/MyApplications_en.properties",
+    "translation": "%original_path%/%file_name%!_%locale_with_underscore%.%file_extension%",
+    "translation_replace": { YML_CROWDIN_LANGUAGES_ARG },
+    "dest": "hub/app__center/MyApplications.properties",
+    "update_option": "update_as_unapproved",
+    "escape_special_characters": 0,
+    "escape_quotes": 0,
+  },
+  {
+    "source": "/app-center-webapps/src/main/resources/locale/portlet/LayoutEditor_en.properties",
+    "translation": "%original_path%/%file_name%!_%locale_with_underscore%.%file_extension%",
+    "translation_replace": { YML_CROWDIN_LANGUAGES_ARG },
+    "dest": "hub/app__center/LayoutEditor.properties",
+    "update_option": "update_as_unapproved",
+    "escape_special_characters": 0,
+    "escape_quotes": 0,
+  }
 ]


Reviews:
user: login: boubaker
body: IMO, it would be better to delete the definition of a specific CSS file for the newly introduced portlet since its style can be fully controlled by generic CSS Helper Classes.
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-02T09:23:42Z
user: login: boubaker
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-02T09:27:30Z
user: login: rdenarie
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-03T09:10:38Z

Review comments:
diff_hunk: @@ -0,0 +1,32 @@
+/*
+  This file is part of the Meeds project (https://meeds.io/).
+  Copyright (C) 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 "./variables.less";
+
+#myApplications {
+  .background-grey-primary {
+    background-color: @primaryBackground;
+  }
path: app-center-webapps/src/main/webapp/skin/less/myApplications.less
user: login: boubaker
body: In order to optimise the CSS Definition Map, can you please move this into `helpers.less` in `Meeds-io/platform-ui` project in order to allow reuse it
created_at: 2025-04-02T09:21:30Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,32 @@
+/*
+  This file is part of the Meeds project (https://meeds.io/).
+  Copyright (C) 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 "./variables.less";
+
+#myApplications {
+  .background-grey-primary {
+    background-color: @primaryBackground;
+  }
+}
+
+#myApplicationsSettingsDrawer {
+  .switch-input {
+    .v-input__slot {
+      margin: 0;
+    }
path: app-center-webapps/src/main/webapp/skin/less/myApplications.less
user: login: boubaker
body: You can delete the margin by adding the property `hide-details` in `v-switch`

```suggestion
```
created_at: 2025-04-02T09:22:26Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+  This file is part of the Meeds project (https://meeds.io/).
+
+  Copyright (C) 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.
+
+-->
+<configuration xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+               xsi:schemaLocation="http://www.exoplatform.org/xml/ns/kernel_1_3.xsd http://www.exoplatform.org/xml/ns/kernel_1_3.xsd"
+               xmlns="http://www.exoplatform.org/xml/ns/kernel_1_3.xsd">
+
+<external-component-plugins>
+    <target-component>io.meeds.social.translation.service.TranslationService</target-component>
+    <component-plugin>
+        <name>myApplicationsPortlet</name>
+        <set-method>addPlugin</set-method>
+        <type>io.meeds.appcenter.plugin.MyApplicationsHeaderTranslation</type>
+    </component-plugin>
+</external-component-plugins>
path: app-center-webapps/src/main/webapp/WEB-INF/conf/app-center/plugins-configuration.xml
user: login: boubaker
body: In order to get rid of Kernel Configuration component definitions, can you please define this plugin the same way the plugin `io.meeds.social.category.plugin.CategoryTranslationPlugin` is defined, using Sprin annotations rather than Kernel definition ?

```suggestion
```
created_at: 2025-04-02T09:27:29Z
author_association: MEMBER

PR comments:
user: login: hakermi
created_at: 2025-04-02T09:38:33Z
body: @boubaker Done, Thanks
author_association: MEMBER
user: login: boubaker
created_at: 2025-04-02T09:41:05Z
body: > @boubaker Done, Thanks

Thanks :+1: 
author_association: MEMBER
user: login: sonarqubecloud[bot]
created_at: 2025-04-02T10:56:25Z
body: ## [![Quality Gate Failed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-failed-20px.png 'Quality Gate Failed')](https://sonarcloud.io/dashboard?id=io.meeds.app-center&pullRequest=394) **Quality Gate failed**  
Failed conditions  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/failed-16px.png '') [23.3% Coverage on New Code](https://sonarcloud.io/component_measures?id=io.meeds.app-center&pullRequest=394&metric=new_coverage&view=list) (required ≥ 60%)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=io.meeds.app-center&pullRequest=394)


author_association: NONE