Skip to content

get potential group members #1794

New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Merged
merged 1 commit into from
Jun 21, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
package org.lowcoder.domain.user.repository;

import java.util.Collection;
import java.util.List;

import org.lowcoder.domain.user.model.User;
import org.springframework.data.domain.Pageable;
import org.springframework.data.mongodb.repository.ReactiveMongoRepository;
import org.springframework.stereotype.Repository;

import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.data.mongodb.repository.Query;

@Repository
public interface UserRepository extends ReactiveMongoRepository<User, String> {
Expand All @@ -23,4 +26,10 @@ public interface UserRepository extends ReactiveMongoRepository<User, String> {

//email1 and email2 should be equal
Flux<User> findByEmailOrConnections_Email(String email1, String email2);

@Query("{ '_id': { $in: ?0 }, 'state': ?1, 'isEnabled': ?2, $or: [ { 'name': { $regex: ?3, $options: 'i' } }, { '_id': { $regex: ?3, $options: 'i' } } ] }")
Flux<User> findUsersByIdsAndSearchNameForPagination(Collection<String> ids, String state, boolean isEnabled, String searchRegex, Pageable pageable);

@Query(value = "{ '_id': { $in: ?0 }, 'state': ?1, 'isEnabled': ?2, $or: [ { 'name': { $regex: ?3, $options: 'i' } }, { '_id': { $regex: ?3, $options: 'i' } } ] }", count = true)
Mono<Long> countUsersByIdsAndSearchName(Collection<String> ids, String state, boolean isEnabled, String searchRegex);
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,10 @@
import java.util.Collection;
import java.util.Map;

import org.lowcoder.domain.user.model.AuthUser;
import org.lowcoder.domain.user.model.Connection;
import org.lowcoder.domain.user.model.User;
import org.lowcoder.domain.user.model.UserDetail;
import org.lowcoder.domain.user.model.*;
import org.lowcoder.infra.annotation.NonEmptyMono;
import org.lowcoder.infra.mongo.MongoUpsertHelper.PartialResourceWithId;
import org.springframework.data.domain.Pageable;
import org.springframework.http.codec.multipart.Part;
import org.springframework.web.server.ServerWebExchange;

Expand Down Expand Up @@ -68,5 +66,7 @@ public interface UserService {

Flux<User> findBySourceAndIds(String connectionSource, Collection<String> connectionSourceUuids);

}
Flux<User> findUsersByIdsAndSearchNameForPagination(Collection<String> ids, String state, boolean isEnabled, String searchRegex, Pageable pageable);

Mono<Long> countUsersByIdsAndSearchName(Collection<String> ids, String state, boolean isEnabled, String searchRegex);
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
import org.lowcoder.sdk.util.HashUtils;
import org.lowcoder.sdk.util.LocaleUtils;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.data.domain.Pageable;
import org.springframework.http.codec.multipart.Part;
import org.springframework.stereotype.Service;
import org.springframework.web.server.ServerWebExchange;
Expand Down Expand Up @@ -473,4 +474,13 @@ public Flux<User> findBySourceAndIds(String connectionSource, Collection<String>
return repository.findByConnections_SourceAndConnections_RawIdIn(connectionSource, connectionSourceUuids);
}

@Override
public Flux<User> findUsersByIdsAndSearchNameForPagination(Collection<String> ids, String state, boolean isEnabled, String searchRegex, Pageable pageable) {
return repository.findUsersByIdsAndSearchNameForPagination(ids, state, isEnabled, searchRegex, pageable);
}

@Override
public Mono<Long> countUsersByIdsAndSearchName(Collection<String> ids, String state, boolean isEnabled, String searchRegex) {
return repository.countUsersByIdsAndSearchName(ids, state, isEnabled, searchRegex);
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.lowcoder.api.usermanagement;

import org.lowcoder.api.usermanagement.view.*;
import org.lowcoder.api.usermanagement.view.OrgMemberListView;
import org.lowcoder.domain.group.model.Group;
import reactor.core.publisher.Mono;

Expand All @@ -24,4 +25,6 @@ public interface GroupApiService {
Mono<Boolean> update(String groupId, UpdateGroupRequest updateGroupRequest);

Mono<Boolean> removeUser(String groupId, String userId);

Mono<OrgMemberListView> getPotentialGroupMembers(String groupId, String searchName, Integer pageNum, Integer pageSize);
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,34 +9,31 @@
import static org.lowcoder.sdk.util.StreamUtils.collectList;
import static org.lowcoder.sdk.util.StreamUtils.collectMap;

import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.*;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

import com.github.f4b6a3.uuid.UuidCreator;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.tuple.Pair;
import org.lowcoder.api.bizthreshold.AbstractBizThresholdChecker;
import org.lowcoder.api.home.SessionUserService;
import org.lowcoder.api.usermanagement.view.CreateGroupRequest;
import org.lowcoder.api.usermanagement.view.GroupMemberAggregateView;
import org.lowcoder.api.usermanagement.view.GroupMemberView;
import org.lowcoder.api.usermanagement.view.GroupView;
import org.lowcoder.api.usermanagement.view.UpdateGroupRequest;
import org.lowcoder.api.usermanagement.view.UpdateRoleRequest;
import org.lowcoder.api.usermanagement.view.*;
import org.lowcoder.domain.group.model.Group;
import org.lowcoder.domain.group.model.GroupMember;
import org.lowcoder.domain.user.model.UserState;
import org.lowcoder.api.usermanagement.view.OrgMemberListView;
import org.lowcoder.domain.group.service.GroupMemberService;
import org.lowcoder.domain.group.service.GroupService;
import org.lowcoder.domain.organization.model.MemberRole;
import org.lowcoder.domain.organization.model.OrgMember;
import org.lowcoder.domain.organization.service.OrgMemberService;
import org.lowcoder.domain.organization.service.OrganizationService;
import org.lowcoder.domain.user.model.User;
import org.lowcoder.domain.user.service.UserService;
import org.lowcoder.infra.util.TupleUtils;
import org.lowcoder.sdk.exception.BizError;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.PageRequest;
import org.springframework.stereotype.Service;

import reactor.core.publisher.Flux;
Expand All @@ -53,7 +50,6 @@ public class GroupApiServiceImpl implements GroupApiService {
private final UserService userService;
private final GroupService groupService;
private final AbstractBizThresholdChecker bizThresholdChecker;
private final OrganizationService organizationService;
private final OrgMemberService orgMemberService;

@Override
Expand Down Expand Up @@ -311,4 +307,63 @@ public Mono<Boolean> removeUser(String groupId, String userId) {
return groupMemberService.removeMember(groupId, userId);
});
}

@Override
public Mono<OrgMemberListView> getPotentialGroupMembers(String groupId, String searchName, Integer pageNum, Integer pageSize) {
return groupService.getById(groupId)
.flatMap(group -> {
String orgId = group.getOrganizationId();
Mono<List<OrgMember>> orgMemberUserIdsMono = orgMemberService.getOrganizationMembers(orgId).collectList();
Mono<List<GroupMember>> groupMemberUserIdsMono = groupMemberService.getGroupMembers(groupId);

return Mono.zip(orgMemberUserIdsMono, groupMemberUserIdsMono)
.flatMap(tuple -> {
List<OrgMember> orgMembers = tuple.getT1();
List<GroupMember> groupMembers = tuple.getT2();

Set<String> groupMemberUserIds = groupMembers.stream()
.map(GroupMember::getUserId)
.collect(Collectors.toSet());

Collection<String> potentialUserIds = orgMembers.stream()
.map(OrgMember::getUserId)
.filter(uid -> !groupMemberUserIds.contains(uid))
.collect(Collectors.toList());

if (potentialUserIds.isEmpty()) {
return Mono.just(OrgMemberListView.builder()
.members(List.of())
.total(0)
.pageNum(pageNum)
.pageSize(pageSize)
.build());
}

Pageable pageable = PageRequest.of(pageNum - 1, pageSize);
String searchRegex = searchName != null && !searchName.isBlank() ? ".*" + Pattern.quote(searchName) + ".*" : ".*";

return userService.findUsersByIdsAndSearchNameForPagination(
potentialUserIds, String.valueOf(UserState.ACTIVATED), true, searchRegex, pageable)
.collectList()
.zipWith(userService.countUsersByIdsAndSearchName(
potentialUserIds, String.valueOf(UserState.ACTIVATED), true, searchRegex))
.map(tupleUser -> {
List<User> users = tupleUser.getT1();
long total = tupleUser.getT2();
List<OrgMemberListView.OrgMemberView> memberViews = users.stream()
.map(u -> OrgMemberListView.OrgMemberView.builder()
.userId(u.getId())
.name(u.getName())
.build())
.collect(Collectors.toList());
return OrgMemberListView.builder()
.members(memberViews)
.total((int) total)
.pageNum(pageNum)
.pageSize(pageSize)
.build();
});
});
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,8 @@
import org.lowcoder.domain.organization.service.OrgMemberService;
import org.lowcoder.sdk.exception.BizError;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.*;
import org.lowcoder.api.usermanagement.view.OrgMemberListView;

import reactor.core.publisher.Mono;
import reactor.util.function.Tuple2;
Expand Down Expand Up @@ -180,4 +178,15 @@ public Mono<ResponseView<Boolean>> removeUser(@PathVariable String groupId,
.map(Tuple2::getT2)
.map(ResponseView::success));
}

@Override
public Mono<ResponseView<OrgMemberListView>> searchPotentialGroupMembers(
@PathVariable String groupId,
@RequestParam(required = false) String searchName,
@RequestParam(required = false, defaultValue = "1") Integer pageNum,
@RequestParam(required = false, defaultValue = "1000") Integer pageSize) {
return gidService.convertGroupIdToObjectId(groupId).flatMap(id ->
groupApiService.getPotentialGroupMembers(id, searchName, pageNum, pageSize)
.map(ResponseView::success));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -115,4 +115,19 @@ public Mono<ResponseView<Boolean>> updateRoleForMember(@RequestBody UpdateRoleRe
@DeleteMapping("/{groupId}/remove")
public Mono<ResponseView<Boolean>> removeUser(@PathVariable String groupId,
@RequestParam String userId);

@Operation(
tags = TAG_GROUP_MEMBERS,
operationId = "searchPotentialGroupMembers",
summary = "Search Potential Group Members",
description = "Retrieve a list of users who are not currently members of the specified group within an organization."
)

@GetMapping("/{groupId}/potential-members")
public Mono<ResponseView<OrgMemberListView>> searchPotentialGroupMembers(
@PathVariable String groupId,
@RequestParam(required = false) String searchName,
@RequestParam(required = false, defaultValue = "1") Integer pageNum,
@RequestParam(required = false, defaultValue = "1000") Integer pageSize
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,5 @@ public interface OrgApiService {
Mono<ConfigView> getOrganizationConfigs(String orgId);

Mono<Long> getApiUsageCount(String orgId, Boolean lastMonthOnly);

Mono<OrgMemberListView> getOrganizationMembersForSearch(String orgId, String searchMemberName, String searchGroupId, Integer pageNum, Integer pageSize);
}

Original file line number Diff line number Diff line change
Expand Up @@ -90,78 +90,6 @@ public Mono<OrgMemberListView> getOrganizationMembers(String orgId, int page, in
.then(getOrgMemberListView(orgId, page, count));
}

// Update getOrgMemberListViewForSearch to filter by group membership
private Mono<OrgMemberListView> getOrgMemberListViewForSearch(String orgId, String searchMemberName, String searchGroupId, Integer page, Integer pageSize) {
return orgMemberService.getOrganizationMembers(orgId)
.collectList()
.flatMap(orgMembers -> {
List<String> userIds = orgMembers.stream()
.map(OrgMember::getUserId)
.collect(Collectors.toList());
Mono<Map<String, User>> users = userService.getByIds(userIds);

// If searchGroupId is provided, fetch group members
Mono<Set<String>> groupUserIdsMono = StringUtils.isBlank(searchGroupId)
? Mono.just(Collections.emptySet())
: groupMemberService.getGroupMembers(searchGroupId)
.map(list -> list.stream()
.map(GroupMember::getUserId)
.collect(Collectors.toSet()));

return Mono.zip(users, groupUserIdsMono)
.map(tuple -> {
Map<String, User> userMap = tuple.getT1();
Set<String> groupUserIds = tuple.getT2();

var list = orgMembers.stream()
.map(orgMember -> {
User user = userMap.get(orgMember.getUserId());
if (user == null) {
log.warn("user {} not exist and will be removed from the result.", orgMember.getUserId());
return null;
}
return buildOrgMemberView(user, orgMember);
})
.filter(Objects::nonNull)
.filter(orgMemberView -> {
// Filter by name
boolean matchesName = StringUtils.isBlank(searchMemberName) ||
StringUtils.containsIgnoreCase(orgMemberView.getName(), searchMemberName);

// Filter by group
boolean matchesGroup = StringUtils.isBlank(searchGroupId) ||
groupUserIds.contains(orgMemberView.getUserId());

return matchesName && matchesGroup;
})
.collect(Collectors.toList());
var pageTotal = list.size();
list = list.subList((page - 1) * pageSize, pageSize == 0 ? pageTotal : Math.min(page * pageSize, pageTotal));
return Pair.of(list, pageTotal);
});
})
.zipWith(sessionUserService.getVisitorOrgMemberCache())
.map(tuple -> {
List<OrgMemberView> memberViews = tuple.getT1().getLeft();
var pageTotal = tuple.getT1().getRight();
OrgMember orgMember = tuple.getT2();
return OrgMemberListView.builder()
.members(memberViews)
.total(pageTotal)
.pageNum(page)
.pageSize(pageSize)
.visitorRole(orgMember.getRole().getValue())
.build();
});
}
@Override
public Mono<OrgMemberListView> getOrganizationMembersForSearch(String orgId, String searchMemberName, String searchGroupId, Integer page, Integer pageSize) {
return sessionUserService.getVisitorId()
.flatMap(visitorId -> orgMemberService.getOrgMember(orgId, visitorId))
.switchIfEmpty(deferredError(BizError.NOT_AUTHORIZED, "NOT_AUTHORIZED"))
.then(getOrgMemberListViewForSearch(orgId, searchMemberName, searchGroupId, page, pageSize));
}

private Mono<OrgMemberListView> getOrgMemberListView(String orgId, int page, int count) {
return orgMemberService.getOrganizationMembers(orgId)
.collectList()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,16 +118,6 @@ public Mono<ResponseView<OrgMemberListView>> getOrgMembers(@PathVariable String
orgApiService.getOrganizationMembers(id, pageNum, pageSize)
.map(ResponseView::success));
}
@Override
public Mono<ResponseView<OrgMemberListView>> getOrgMembersForSearch(@PathVariable String orgId,
@PathVariable String searchMemberName,
@PathVariable String searchGroupId,
@RequestParam(required = false, defaultValue = "1") int pageNum,
@RequestParam(required = false, defaultValue = "1000") int pageSize) {
return gidService.convertOrganizationIdToObjectId(orgId).flatMap(id ->
orgApiService.getOrganizationMembersForSearch(id, searchMemberName, searchGroupId, pageNum, pageSize)
.map(ResponseView::success));
}

@Override
public Mono<ResponseView<Boolean>> updateRoleForMember(@RequestBody UpdateRoleRequest updateRoleRequest,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,13 +98,6 @@ public Mono<ResponseView<OrgMemberListView>> getOrgMembers(@PathVariable String
@RequestParam(required = false, defaultValue = "1") int pageNum,
@RequestParam(required = false, defaultValue = "1000") int pageSize);

@GetMapping("/{orgId}/{searchMemberName}/{searchGroupId}/members")
public Mono<ResponseView<OrgMemberListView>> getOrgMembersForSearch(@PathVariable String orgId,
@PathVariable String searchMemberName,
@PathVariable String searchGroupId,
@RequestParam(required = false, defaultValue = "1") int pageNum,
@RequestParam(required = false, defaultValue = "1000") int pageSize);

@Operation(
tags = TAG_ORGANIZATION_MEMBERS,
operationId = "updateOrganizationMemberRole",
Expand Down
Loading