feat(backend): sort applications and workspaces alphabetically (#41253)

## Description
Made changes in backend to sort applications and workspaces in
alphabetic order
Also added feature flag control to this functionality.

Fixes #31108

## Automation

/test Workspace

### 🔍 Cypress test results
<!-- This is an auto-generated comment: Cypress test results  -->
> [!TIP]
> 🟢 🟢 🟢 All cypress tests have passed! 🎉 🎉 🎉
> Workflow run:
<https://github.com/appsmithorg/appsmith/actions/runs/17998282833>
> Commit: ff76753e19106314d21cc4b9548177fe8f93339d
> <a
href="https://internal.appsmith.com/app/cypress-dashboard/rundetails-65890b3c81d7400d08fa9ee5?branch=master&workflowId=17998282833&attempt=1"
target="_blank">Cypress dashboard</a>.
> Tags: `@tag.Workspace`
> Spec:
> <hr>Thu, 25 Sep 2025 06:09:23 UTC
<!-- end of auto-generated comment: Cypress test results  -->


## Communication
Should the DevRel and Marketing teams inform users about this change?
- [ ] Yes
- [ ] No


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Optional alphabetical ordering for workspaces and applications on the
Home page, toggleable via a new feature flag.
* Home view now chooses between case-insensitive alphabetical sorting
and the existing “recently used” ordering based on that flag.

* **Tests**
* Added automated tests verifying alphabetical workspace ordering and
exact name sequencing.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Abhijeet <abhi.nagarnaik@gmail.com>
This commit is contained in:
tomjose92 2025-09-25 11:55:06 +05:30 committed by GitHub
parent e742df0bfa
commit 44d61529ea
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 170 additions and 10 deletions

View File

@ -31,6 +31,10 @@ public enum FeatureFlagEnum {
release_git_autocommit_eligibility_enabled, release_git_autocommit_eligibility_enabled,
release_dynamodb_connection_time_to_live_enabled, release_dynamodb_connection_time_to_live_enabled,
release_reactive_actions_enabled, release_reactive_actions_enabled,
/**
* Feature flag to enable alphabetical ordering for workspaces and applications
*/
release_alphabetical_ordering_enabled,
// Add EE flags below this line, to avoid conflicts. // Add EE flags below this line, to avoid conflicts.
} }

View File

@ -29,6 +29,10 @@ public interface ApplicationServiceCE extends CrudService<Application, String> {
Flux<Application> findByWorkspaceIdAndBaseApplicationsInRecentlyUsedOrder(String workspaceId); Flux<Application> findByWorkspaceIdAndBaseApplicationsInRecentlyUsedOrder(String workspaceId);
Flux<Application> findByWorkspaceIdAndBaseApplicationsInAlphabeticalOrder(String workspaceId);
Flux<Application> findByWorkspaceIdAndBaseApplicationsForHome(String workspaceId);
Mono<Application> save(Artifact application); Mono<Application> save(Artifact application);
Mono<Application> updateApplicationWithPresets(String branchedApplicationId, Application application); Mono<Application> updateApplicationWithPresets(String branchedApplicationId, Application application);

View File

@ -1,5 +1,6 @@
package com.appsmith.server.applications.base; package com.appsmith.server.applications.base;
import com.appsmith.external.enums.FeatureFlagEnum;
import com.appsmith.external.models.ActionDTO; import com.appsmith.external.models.ActionDTO;
import com.appsmith.external.models.Policy; import com.appsmith.external.models.Policy;
import com.appsmith.server.acl.AclPermission; import com.appsmith.server.acl.AclPermission;
@ -36,6 +37,7 @@ import com.appsmith.server.repositories.NewActionRepository;
import com.appsmith.server.services.AnalyticsService; import com.appsmith.server.services.AnalyticsService;
import com.appsmith.server.services.AssetService; import com.appsmith.server.services.AssetService;
import com.appsmith.server.services.BaseService; import com.appsmith.server.services.BaseService;
import com.appsmith.server.services.FeatureFlagService;
import com.appsmith.server.services.PermissionGroupService; import com.appsmith.server.services.PermissionGroupService;
import com.appsmith.server.services.SessionUserService; import com.appsmith.server.services.SessionUserService;
import com.appsmith.server.services.UserDataService; import com.appsmith.server.services.UserDataService;
@ -59,6 +61,7 @@ import reactor.core.publisher.Mono;
import java.time.Instant; import java.time.Instant;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
@ -90,6 +93,7 @@ public class ApplicationServiceCEImpl extends BaseService<ApplicationRepository,
private final WorkspaceService workspaceService; private final WorkspaceService workspaceService;
private final WorkspacePermission workspacePermission; private final WorkspacePermission workspacePermission;
private final ObservationRegistry observationRegistry; private final ObservationRegistry observationRegistry;
private final FeatureFlagService featureFlagService;
private static final Integer MAX_RETRIES = 5; private static final Integer MAX_RETRIES = 5;
@ -108,7 +112,8 @@ public class ApplicationServiceCEImpl extends BaseService<ApplicationRepository,
UserDataService userDataService, UserDataService userDataService,
WorkspaceService workspaceService, WorkspaceService workspaceService,
WorkspacePermission workspacePermission, WorkspacePermission workspacePermission,
ObservationRegistry observationRegistry) { ObservationRegistry observationRegistry,
FeatureFlagService featureFlagService) {
super(validator, repository, analyticsService); super(validator, repository, analyticsService);
this.policySolution = policySolution; this.policySolution = policySolution;
@ -122,6 +127,7 @@ public class ApplicationServiceCEImpl extends BaseService<ApplicationRepository,
this.workspaceService = workspaceService; this.workspaceService = workspaceService;
this.workspacePermission = workspacePermission; this.workspacePermission = workspacePermission;
this.observationRegistry = observationRegistry; this.observationRegistry = observationRegistry;
this.featureFlagService = featureFlagService;
} }
@Override @Override
@ -214,6 +220,63 @@ public class ApplicationServiceCEImpl extends BaseService<ApplicationRepository,
}))); })));
} }
/**
* This method is used to fetch all the applications for a given workspaceId. It sorts the applications in
* alphabetical order by name.
* For git connected applications only default branched application is returned.
*
* @param workspaceId workspaceId for which applications are to be fetched
* @return Flux of applications sorted alphabetically
*/
@Override
public Flux<Application> findByWorkspaceIdAndBaseApplicationsInAlphabeticalOrder(String workspaceId) {
if (!StringUtils.hasLength(workspaceId)) {
return Flux.error(new AppsmithException(AppsmithError.INVALID_PARAMETER, FieldName.WORKSPACE_ID));
}
// Read the workspace
Mono<Workspace> workspaceMono = workspaceService
.findById(workspaceId, workspacePermission.getReadPermission())
.switchIfEmpty(Mono.error(
new AppsmithException(AppsmithError.ACL_NO_RESOURCE_FOUND, FieldName.WORKSPACE, workspaceId)));
return workspaceMono.thenMany(this.findByWorkspaceId(workspaceId, applicationPermission.getReadPermission())
.sort(Comparator.comparing(application -> application.getName().toLowerCase()))
.filter(application -> {
/*
* Filter applications based on the following criteria:
* - Applications that are not connected to Git.
* - Applications that, when connected, revert with default branch only.
*/
return !GitUtils.isArtifactConnectedToGit(application.getGitArtifactMetadata())
|| GitUtils.isDefaultBranchedArtifact(application.getGitArtifactMetadata());
}));
}
/**
* This method is used to fetch all the applications for a given workspaceId. It sorts the applications based
* on feature flag - either alphabetically or by recently used order.
* For git connected applications only default branched application is returned.
*
* @param workspaceId workspaceId for which applications are to be fetched
* @return Flux of applications sorted based on feature flag
*/
@Override
public Flux<Application> findByWorkspaceIdAndBaseApplicationsForHome(String workspaceId) {
Mono<Boolean> isAlphabeticalOrderingEnabled =
featureFlagService.check(FeatureFlagEnum.release_alphabetical_ordering_enabled);
return isAlphabeticalOrderingEnabled.flatMapMany(isEnabled -> {
if (isEnabled) {
// If alphabetical ordering is enabled, then we need to sort the applications in alphabetical order
return findByWorkspaceIdAndBaseApplicationsInAlphabeticalOrder(workspaceId);
} else {
// If alphabetical ordering is disabled, then we need to sort the applications in recently used order
return findByWorkspaceIdAndBaseApplicationsInRecentlyUsedOrder(workspaceId);
}
});
}
@Override @Override
public Mono<Application> save(Artifact artifact) { public Mono<Application> save(Artifact artifact) {
Application application = (Application) artifact; Application application = (Application) artifact;

View File

@ -6,6 +6,7 @@ import com.appsmith.server.repositories.ApplicationRepository;
import com.appsmith.server.repositories.NewActionRepository; import com.appsmith.server.repositories.NewActionRepository;
import com.appsmith.server.services.AnalyticsService; import com.appsmith.server.services.AnalyticsService;
import com.appsmith.server.services.AssetService; import com.appsmith.server.services.AssetService;
import com.appsmith.server.services.FeatureFlagService;
import com.appsmith.server.services.PermissionGroupService; import com.appsmith.server.services.PermissionGroupService;
import com.appsmith.server.services.SessionUserService; import com.appsmith.server.services.SessionUserService;
import com.appsmith.server.services.UserDataService; import com.appsmith.server.services.UserDataService;
@ -39,7 +40,8 @@ public class ApplicationServiceImpl extends ApplicationServiceCECompatibleImpl
UserDataService userDataService, UserDataService userDataService,
WorkspaceService workspaceService, WorkspaceService workspaceService,
WorkspacePermission workspacePermission, WorkspacePermission workspacePermission,
ObservationRegistry observationRegistry) { ObservationRegistry observationRegistry,
FeatureFlagService featureFlagService) {
super( super(
validator, validator,
repository, repository,
@ -54,6 +56,7 @@ public class ApplicationServiceImpl extends ApplicationServiceCECompatibleImpl
userDataService, userDataService,
workspaceService, workspaceService,
workspacePermission, workspacePermission,
observationRegistry); observationRegistry,
featureFlagService);
} }
} }

View File

@ -130,7 +130,7 @@ public class ApplicationControllerCE {
public Mono<ResponseDTO<List<Application>>> findByWorkspaceIdAndRecentlyUsedOrder( public Mono<ResponseDTO<List<Application>>> findByWorkspaceIdAndRecentlyUsedOrder(
@RequestParam(required = false) String workspaceId) { @RequestParam(required = false) String workspaceId) {
log.debug("Going to get all applications by workspace id {}", workspaceId); log.debug("Going to get all applications by workspace id {}", workspaceId);
return service.findByWorkspaceIdAndBaseApplicationsInRecentlyUsedOrder(workspaceId) return service.findByWorkspaceIdAndBaseApplicationsForHome(workspaceId)
.collectList() .collectList()
.map(applications -> new ResponseDTO<>(HttpStatus.OK, applications)); .map(applications -> new ResponseDTO<>(HttpStatus.OK, applications));
} }

View File

@ -107,7 +107,7 @@ public class WorkspaceControllerCE {
@GetMapping("/home") @GetMapping("/home")
public Mono<ResponseDTO<List<Workspace>>> workspacesForHome() { public Mono<ResponseDTO<List<Workspace>>> workspacesForHome() {
return userWorkspaceService return userWorkspaceService
.getUserWorkspacesByRecentlyUsedOrder() .getUserWorkspacesForHome()
.map(workspaces -> new ResponseDTO<>(HttpStatus.OK, workspaces)); .map(workspaces -> new ResponseDTO<>(HttpStatus.OK, workspaces));
} }
} }

View File

@ -19,7 +19,8 @@ public class UserWorkspaceServiceImpl extends UserWorkspaceServiceCEImpl impleme
PermissionGroupService permissionGroupService, PermissionGroupService permissionGroupService,
OrganizationService organizationService, OrganizationService organizationService,
WorkspacePermission workspacePermission, WorkspacePermission workspacePermission,
PermissionGroupPermission permissionGroupPermission) { PermissionGroupPermission permissionGroupPermission,
FeatureFlagService featureFlagService) {
super( super(
sessionUserService, sessionUserService,
@ -29,6 +30,7 @@ public class UserWorkspaceServiceImpl extends UserWorkspaceServiceCEImpl impleme
permissionGroupService, permissionGroupService,
organizationService, organizationService,
workspacePermission, workspacePermission,
permissionGroupPermission); permissionGroupPermission,
featureFlagService);
} }
} }

View File

@ -25,4 +25,8 @@ public interface UserWorkspaceServiceCE {
Mono<Boolean> isLastAdminRoleEntity(PermissionGroup permissionGroup); Mono<Boolean> isLastAdminRoleEntity(PermissionGroup permissionGroup);
Mono<List<Workspace>> getUserWorkspacesByRecentlyUsedOrder(); Mono<List<Workspace>> getUserWorkspacesByRecentlyUsedOrder();
Mono<List<Workspace>> getUserWorkspacesInAlphabeticalOrder();
Mono<List<Workspace>> getUserWorkspacesForHome();
} }

View File

@ -1,5 +1,6 @@
package com.appsmith.server.services.ce; package com.appsmith.server.services.ce;
import com.appsmith.external.enums.FeatureFlagEnum;
import com.appsmith.server.constants.FieldName; import com.appsmith.server.constants.FieldName;
import com.appsmith.server.domains.PermissionGroup; import com.appsmith.server.domains.PermissionGroup;
import com.appsmith.server.domains.User; import com.appsmith.server.domains.User;
@ -13,6 +14,7 @@ import com.appsmith.server.exceptions.AppsmithError;
import com.appsmith.server.exceptions.AppsmithException; import com.appsmith.server.exceptions.AppsmithException;
import com.appsmith.server.helpers.AppsmithComparators; import com.appsmith.server.helpers.AppsmithComparators;
import com.appsmith.server.repositories.UserRepository; import com.appsmith.server.repositories.UserRepository;
import com.appsmith.server.services.FeatureFlagService;
import com.appsmith.server.services.OrganizationService; import com.appsmith.server.services.OrganizationService;
import com.appsmith.server.services.PermissionGroupService; import com.appsmith.server.services.PermissionGroupService;
import com.appsmith.server.services.SessionUserService; import com.appsmith.server.services.SessionUserService;
@ -32,6 +34,7 @@ import reactor.util.function.Tuple2;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -54,6 +57,7 @@ public class UserWorkspaceServiceCEImpl implements UserWorkspaceServiceCE {
private final OrganizationService organizationService; private final OrganizationService organizationService;
private final WorkspacePermission workspacePermission; private final WorkspacePermission workspacePermission;
private final PermissionGroupPermission permissionGroupPermission; private final PermissionGroupPermission permissionGroupPermission;
private final FeatureFlagService featureFlagService;
@Autowired @Autowired
public UserWorkspaceServiceCEImpl( public UserWorkspaceServiceCEImpl(
@ -64,7 +68,8 @@ public class UserWorkspaceServiceCEImpl implements UserWorkspaceServiceCE {
PermissionGroupService permissionGroupService, PermissionGroupService permissionGroupService,
OrganizationService organizationService, OrganizationService organizationService,
WorkspacePermission workspacePermission, WorkspacePermission workspacePermission,
PermissionGroupPermission permissionGroupPermission) { PermissionGroupPermission permissionGroupPermission,
FeatureFlagService featureFlagService) {
this.sessionUserService = sessionUserService; this.sessionUserService = sessionUserService;
this.workspaceService = workspaceService; this.workspaceService = workspaceService;
this.userRepository = userRepository; this.userRepository = userRepository;
@ -73,6 +78,7 @@ public class UserWorkspaceServiceCEImpl implements UserWorkspaceServiceCE {
this.organizationService = organizationService; this.organizationService = organizationService;
this.workspacePermission = workspacePermission; this.workspacePermission = workspacePermission;
this.permissionGroupPermission = permissionGroupPermission; this.permissionGroupPermission = permissionGroupPermission;
this.featureFlagService = featureFlagService;
} }
@Override @Override
@ -419,4 +425,39 @@ public class UserWorkspaceServiceCEImpl implements UserWorkspaceServiceCE {
// collect to list to keep the order of the workspaces // collect to list to keep the order of the workspaces
.collectList()); .collectList());
} }
/*
* Returns a list of workspaces for the current user, sorted in alphabetical order.
*
* @return Mono containing the list of workspaces
*/
@Override
public Mono<List<Workspace>> getUserWorkspacesInAlphabeticalOrder() {
return workspaceService
.getAll(workspacePermission.getReadPermission())
.sort(Comparator.comparing(workspace -> workspace.getName().toLowerCase()))
.collectList();
}
/**
* Returns a list of workspaces for the current user, sorted based on feature flag.
* If alphabetical ordering is enabled, returns workspaces in alphabetical order.
* Otherwise, returns workspaces in recently used order.
*
* @return Mono containing the list of workspaces
*/
@Override
public Mono<List<Workspace>> getUserWorkspacesForHome() {
Mono<Boolean> isAlphabeticalOrderingEnabled =
featureFlagService.check(FeatureFlagEnum.release_alphabetical_ordering_enabled);
return isAlphabeticalOrderingEnabled.flatMap(isEnabled -> {
if (isEnabled) {
// If alphabetical ordering is enabled, then we need to sort the workspaces in alphabetical order
return getUserWorkspacesInAlphabeticalOrder();
} else {
// If alphabetical ordering is disabled, then we need to sort the workspaces in recently used order
return getUserWorkspacesByRecentlyUsedOrder();
}
});
}
} }

View File

@ -5,6 +5,7 @@ import com.appsmith.server.repositories.ApplicationRepository;
import com.appsmith.server.repositories.NewActionRepository; import com.appsmith.server.repositories.NewActionRepository;
import com.appsmith.server.services.AnalyticsService; import com.appsmith.server.services.AnalyticsService;
import com.appsmith.server.services.AssetService; import com.appsmith.server.services.AssetService;
import com.appsmith.server.services.FeatureFlagService;
import com.appsmith.server.services.PermissionGroupService; import com.appsmith.server.services.PermissionGroupService;
import com.appsmith.server.services.SessionUserService; import com.appsmith.server.services.SessionUserService;
import com.appsmith.server.services.UserDataService; import com.appsmith.server.services.UserDataService;
@ -34,7 +35,8 @@ public class ApplicationServiceCECompatibleImpl extends ApplicationServiceCEImpl
UserDataService userDataService, UserDataService userDataService,
WorkspaceService workspaceService, WorkspaceService workspaceService,
WorkspacePermission workspacePermission, WorkspacePermission workspacePermission,
ObservationRegistry observationRegistry) { ObservationRegistry observationRegistry,
FeatureFlagService featureFlagService) {
super( super(
validator, validator,
repository, repository,
@ -49,6 +51,7 @@ public class ApplicationServiceCECompatibleImpl extends ApplicationServiceCEImpl
userDataService, userDataService,
workspaceService, workspaceService,
workspacePermission, workspacePermission,
observationRegistry); observationRegistry,
featureFlagService);
} }
} }

View File

@ -377,4 +377,40 @@ public class UserWorkspaceServiceTest {
.cleanPermissionGroupCacheForUsers(List.of(api_user.getId(), test_user.getId())) .cleanPermissionGroupCacheForUsers(List.of(api_user.getId(), test_user.getId()))
.block(); .block();
} }
@Test
@WithUserDetails(value = "api_user")
public void getUserWorkspacesInAlphabeticalOrder_WhenUserHasWorkspaces_ReturnsWorkspacesSortedAlphabetically() {
List<String> existingWorkspaceNames =
workspaceService.getAll().map(Workspace::getName).collectList().block();
// Arrange: Create multiple workspaces with different names
List<String> workspaceNames =
new java.util.ArrayList<>(List.of("Zebra Workspace", "Alpha Workspace", "Beta Workspace"));
assert existingWorkspaceNames != null;
for (String name : workspaceNames) {
Workspace workspace = new Workspace();
workspace.setName(name);
// Ensures default permission groups & current user access are created
workspaceService.create(workspace).block();
}
// Act: Call the method to get the user's workspaces in alphabetical order
Mono<List<Workspace>> workspacesMono = userWorkspaceService.getUserWorkspacesInAlphabeticalOrder();
// Assert: Verify the workspaces are returned in alphabetical order
StepVerifier.create(workspacesMono)
.assertNext(workspaces -> {
assertThat(workspaces).isNotNull();
assertThat(workspaces).hasSize(3 + existingWorkspaceNames.size());
List<String> workspaceNamesList =
workspaces.stream().map(Workspace::getName).collect(Collectors.toList());
workspaceNames.addAll(existingWorkspaceNames);
List<String> sortedExistingWorkspaceNames =
workspaceNames.stream().sorted().toList();
assertThat(workspaceNamesList).containsExactly(sortedExistingWorkspaceNames.toArray(new String[0]));
})
.verifyComplete();
}
} }