Skip to content

Commit

Permalink
fix: fix org user groups search and add sort to cols, and fix find po…
Browse files Browse the repository at this point in the history
…ssible group members pagination and improve search to include first and last name
  • Loading branch information
scott-ray-wilson committed Oct 18, 2024
1 parent 13c0b31 commit 15a3d66
Show file tree
Hide file tree
Showing 18 changed files with 239 additions and 76 deletions.
3 changes: 2 additions & 1 deletion backend/src/ee/routes/v1/group-router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,8 @@ export const registerGroupRouter = async (server: FastifyZodProvider) => {
querystring: z.object({
offset: z.coerce.number().min(0).max(100).default(0).describe(GROUPS.LIST_USERS.offset),
limit: z.coerce.number().min(1).max(100).default(10).describe(GROUPS.LIST_USERS.limit),
username: z.string().optional().describe(GROUPS.LIST_USERS.username)
username: z.string().trim().optional().describe(GROUPS.LIST_USERS.username),
search: z.string().trim().optional().describe(GROUPS.LIST_USERS.search)
}),
response: {
200: z.object({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,9 @@ export const accessApprovalPolicyServiceFactory = ({
const verifyAllApprovers = [...approverUserIds];

for (const groupId of groupApprovers) {
usersPromises.push(groupDAL.findAllGroupPossibleMembers({ orgId: actorOrgId, groupId, offset: 0 }));
usersPromises.push(
groupDAL.findAllGroupPossibleMembers({ orgId: actorOrgId, groupId, offset: 0 }).then((group) => group.members)
);
}
const verifyGroupApprovers = (await Promise.all(usersPromises))
.flat()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,10 +145,12 @@ export const accessApprovalRequestServiceFactory = ({
const groupUsers = (
await Promise.all(
approverGroupIds.map((groupApproverId) =>
groupDAL.findAllGroupPossibleMembers({
orgId: actorOrgId,
groupId: groupApproverId
})
groupDAL
.findAllGroupPossibleMembers({
orgId: actorOrgId,
groupId: groupApproverId
})
.then((group) => group.members)
)
)
).flat();
Expand Down
48 changes: 32 additions & 16 deletions backend/src/ee/services/group/group-dal.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Knex } from "knex";

import { TDbClient } from "@app/db";
import { TableName, TGroups } from "@app/db/schemas";
import { TableName, TGroups, TOrgMemberships, TUserGroupMembership, TUsers } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
import { buildFindFilter, ormify, selectAllTableCols, TFindFilter, TFindOpt } from "@app/lib/knex";

Expand Down Expand Up @@ -65,13 +65,15 @@ export const groupDALFactory = (db: TDbClient) => {
groupId,
offset = 0,
limit,
username
username, // depreciated in favor of search
search
}: {
orgId: string;
groupId: string;
offset?: number;
limit?: number;
username?: string;
search?: string;
}) => {
try {
let query = db
Expand All @@ -85,38 +87,52 @@ export const groupDALFactory = (db: TDbClient) => {
db.raw("?", [groupId])
);
})
.select(
.select<
(Pick<TOrgMemberships, "id"> &
Pick<TUserGroupMembership, "groupId"> &
Pick<TUsers, "email" | "username" | "firstName" | "lastName"> & {
userId: string;
total_count: number;
})[]
>(
db.ref("id").withSchema(TableName.OrgMembership),
db.ref("groupId").withSchema(TableName.UserGroupMembership),
db.ref("email").withSchema(TableName.Users),
db.ref("username").withSchema(TableName.Users),
db.ref("firstName").withSchema(TableName.Users),
db.ref("lastName").withSchema(TableName.Users),
db.ref("id").withSchema(TableName.Users).as("userId")
db.ref("id").withSchema(TableName.Users).as("userId"),
db.raw(`count(*) OVER() as total_count`)
)
.where({ isGhost: false })
.offset(offset);
.offset(offset)
.orderBy("firstName", "asc");

if (limit) {
query = query.limit(limit);
}

if (username) {
if (search) {
query = query.andWhereRaw(`CONCAT_WS(' ', "firstName", "lastName", "username") ilike '%${search}%'`);
} else if (username) {
query = query.andWhere(`${TableName.Users}.username`, "ilike", `%${username}%`);
}

const members = await query;

return members.map(
({ email, username: memberUsername, firstName, lastName, userId, groupId: memberGroupId }) => ({
id: userId,
email,
username: memberUsername,
firstName,
lastName,
isPartOfGroup: !!memberGroupId
})
);
return {
members: members.map(
({ email, username: memberUsername, firstName, lastName, userId, groupId: memberGroupId }) => ({
id: userId,
email,
username: memberUsername,
firstName,
lastName,
isPartOfGroup: !!memberGroupId
})
),
totalCount: Number(members?.[0]?.total_count ?? 0)
};
} catch (error) {
throw new DatabaseError({ error, name: "Find all org members" });
}
Expand Down
12 changes: 6 additions & 6 deletions backend/src/ee/services/group/group-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,8 @@ export const groupServiceFactory = ({
actor,
actorId,
actorAuthMethod,
actorOrgId
actorOrgId,
search
}: TListGroupUsersDTO) => {
if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID provided in request" });

Expand All @@ -244,17 +245,16 @@ export const groupServiceFactory = ({
message: `Failed to find group with ID ${id}`
});

const users = await groupDAL.findAllGroupPossibleMembers({
const { members, totalCount } = await groupDAL.findAllGroupPossibleMembers({
orgId: group.orgId,
groupId: group.id,
offset,
limit,
username
username,
search
});

const count = await orgDAL.countAllOrgMembers(group.orgId);

return { users, totalCount: count };
return { users: members, totalCount };
};

const addUserToGroup = async ({ id, username, actor, actorId, actorAuthMethod, actorOrgId }: TAddUserToGroupDTO) => {
Expand Down
1 change: 1 addition & 0 deletions backend/src/ee/services/group/group-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export type TListGroupUsersDTO = {
offset: number;
limit: number;
username?: string;
search?: string;
} & TGenericPermission;

export type TAddUserToGroupDTO = {
Expand Down
10 changes: 6 additions & 4 deletions backend/src/ee/services/scim/scim-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -834,10 +834,12 @@ export const scimServiceFactory = ({
});
}

const users = await groupDAL.findAllGroupPossibleMembers({
orgId: group.orgId,
groupId: group.id
});
const users = await groupDAL
.findAllGroupPossibleMembers({
orgId: group.orgId,
groupId: group.id
})
.then((g) => g.members);

const orgMemberships = await orgDAL.findMembership({
[`${TableName.OrgMembership}.orgId` as "orgId"]: orgId,
Expand Down
3 changes: 2 additions & 1 deletion backend/src/lib/api-docs/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ export const GROUPS = {
id: "The id of the group to list users for",
offset: "The offset to start from. If you enter 10, it will start from the 10th user.",
limit: "The number of users to return.",
username: "The username to search for."
username: "The username to search for.",
search: "The text string that user email or name will be filtered by."
},
ADD_USER: {
id: "The id of the group to add the user to.",
Expand Down
15 changes: 8 additions & 7 deletions frontend/src/hooks/api/groups/queries.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@ export const groupKeys = {
slug,
offset,
limit,
username
search
}: {
slug: string;
offset: number;
limit: number;
username: string;
}) => [...groupKeys.forGroupUserMemberships(slug), { offset, limit, username }] as const
search: string;
}) => [...groupKeys.forGroupUserMemberships(slug), { offset, limit, search }] as const
};

type TUser = {
Expand All @@ -33,27 +33,28 @@ export const useListGroupUsers = ({
groupSlug,
offset = 0,
limit = 10,
username
search
}: {
id: string;
groupSlug: string;
offset: number;
limit: number;
username: string;
search: string;
}) => {
return useQuery({
queryKey: groupKeys.specificGroupUserMemberships({
slug: groupSlug,
offset,
limit,
username
search
}),
enabled: Boolean(groupSlug),
keepPreviousData: true,
queryFn: async () => {
const params = new URLSearchParams({
offset: String(offset),
limit: String(limit),
username
search
});

const { data } = await apiRequest.get<{ users: TUser[]; totalCount: number }>(
Expand Down
1 change: 1 addition & 0 deletions frontend/src/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export { useLeaveConfirm } from "./useLeaveConfirm";
export { usePagination } from "./usePagination";
export { usePersistentState } from "./usePersistentState";
export { usePopUp } from "./usePopUp";
export { useResetPageHelper } from "./useResetPageHelper";
export { useSyntaxHighlight } from "./useSyntaxHighlight";
export { useTimedReset } from "./useTimedReset";
export { useToggle } from "./useToggle";
16 changes: 16 additions & 0 deletions frontend/src/hooks/useResetPageHelper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Dispatch, SetStateAction, useEffect } from "react";

export const useResetPageHelper = ({
totalCount,
offset,
setPage
}: {
totalCount: number;
offset: number;
setPage: Dispatch<SetStateAction<number>>;
}) => {
useEffect(() => {
// reset page if no longer valid
if (totalCount <= offset) setPage(1);
}, [totalCount]);
};
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
Tr
} from "@app/components/v2";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context";
import { useDebounce, useResetPageHelper } from "@app/hooks";
import { useAddUserToGroup, useListGroupUsers, useRemoveUserFromGroup } from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp";

Expand All @@ -33,18 +34,26 @@ export const OrgGroupMembersModal = ({ popUp, handlePopUpToggle }: Props) => {
const [page, setPage] = useState(1);
const [perPage, setPerPage] = useState(10);
const [searchMemberFilter, setSearchMemberFilter] = useState("");
const [search] = useDebounce(searchMemberFilter);

const popUpData = popUp?.groupMembers?.data as {
groupId: string;
slug: string;
};

const offset = (page - 1) * perPage;
const { data, isLoading } = useListGroupUsers({
id: popUpData?.groupId,
groupSlug: popUpData?.slug,
offset: (page - 1) * perPage,
offset,
limit: perPage,
username: searchMemberFilter
search
});

useResetPageHelper({
totalCount: data?.totalCount ?? 0,
offset,
setPage
});

const { mutateAsync: assignMutateAsync } = useAddUserToGroup();
Expand Down
Loading

0 comments on commit 15a3d66

Please sign in to comment.