mirror of
https://github.com/documenso/documenso.git
synced 2026-07-25 01:15:49 +10:00
fix: paginate and search member/group pickers (#2768)
This commit is contained in:
@@ -17,8 +17,16 @@ export const findOrganisationGroupsRoute = authenticatedProcedure
|
||||
.input(ZFindOrganisationGroupsRequestSchema)
|
||||
.output(ZFindOrganisationGroupsResponseSchema)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const { organisationId, types, query, page, perPage, organisationGroupId, organisationRoles } =
|
||||
input;
|
||||
const {
|
||||
organisationId,
|
||||
types,
|
||||
query,
|
||||
page,
|
||||
perPage,
|
||||
organisationGroupId,
|
||||
organisationRoles,
|
||||
excludeTeamId,
|
||||
} = input;
|
||||
const { user } = ctx;
|
||||
|
||||
ctx.logger.info({
|
||||
@@ -36,6 +44,7 @@ export const findOrganisationGroupsRoute = authenticatedProcedure
|
||||
query,
|
||||
page,
|
||||
perPage,
|
||||
excludeTeamId,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -48,6 +57,7 @@ type FindOrganisationGroupsOptions = {
|
||||
query?: string;
|
||||
page?: number;
|
||||
perPage?: number;
|
||||
excludeTeamId?: number;
|
||||
};
|
||||
|
||||
export const findOrganisationGroups = async ({
|
||||
@@ -59,6 +69,7 @@ export const findOrganisationGroups = async ({
|
||||
query,
|
||||
page = 1,
|
||||
perPage = 10,
|
||||
excludeTeamId,
|
||||
}: FindOrganisationGroupsOptions) => {
|
||||
const organisation = await prisma.organisation.findFirst({
|
||||
where: buildOrganisationWhereQuery({ organisationId, userId }),
|
||||
@@ -92,6 +103,14 @@ export const findOrganisationGroups = async ({
|
||||
};
|
||||
}
|
||||
|
||||
// Exclude organisation groups that already have a team-group entry pointing
|
||||
// at the given team — i.e. they're already attached.
|
||||
if (excludeTeamId !== undefined) {
|
||||
whereClause.teamGroups = {
|
||||
none: { teamId: excludeTeamId },
|
||||
};
|
||||
}
|
||||
|
||||
const [data, count] = await Promise.all([
|
||||
prisma.organisationGroup.findMany({
|
||||
where: whereClause,
|
||||
|
||||
@@ -19,6 +19,12 @@ export const ZFindOrganisationGroupsRequestSchema = ZFindSearchParamsSchema.exte
|
||||
organisationGroupId: z.string().optional(),
|
||||
organisationRoles: z.nativeEnum(OrganisationMemberRole).array().optional(),
|
||||
types: z.nativeEnum(OrganisationGroupType).array().optional(),
|
||||
/**
|
||||
* Exclude organisation groups that are already attached to the given team.
|
||||
* Useful for "add groups to team" pickers so that groups already on the
|
||||
* team don't appear in the dropdown.
|
||||
*/
|
||||
excludeTeamId: z.number().optional(),
|
||||
});
|
||||
|
||||
export const ZFindOrganisationGroupsResponseSchema = ZFindResultResponse.extend({
|
||||
|
||||
@@ -28,6 +28,7 @@ export const findOrganisationMembersRoute = authenticatedProcedure
|
||||
query: input.query,
|
||||
page: input.page,
|
||||
perPage: input.perPage,
|
||||
excludeTeamId: input.excludeTeamId,
|
||||
});
|
||||
|
||||
return {
|
||||
@@ -55,6 +56,7 @@ type FindOrganisationMembersOptions = {
|
||||
query?: string;
|
||||
page?: number;
|
||||
perPage?: number;
|
||||
excludeTeamId?: number;
|
||||
};
|
||||
|
||||
export const findOrganisationMembers = async ({
|
||||
@@ -63,6 +65,7 @@ export const findOrganisationMembers = async ({
|
||||
query,
|
||||
page = 1,
|
||||
perPage = 10,
|
||||
excludeTeamId,
|
||||
}: FindOrganisationMembersOptions) => {
|
||||
const organisation = await prisma.organisation.findFirst({
|
||||
where: buildOrganisationWhereQuery({ organisationId, userId }),
|
||||
@@ -95,6 +98,21 @@ export const findOrganisationMembers = async ({
|
||||
};
|
||||
}
|
||||
|
||||
// Exclude organisation members who are already part of the given team —
|
||||
// i.e. they belong to an organisation group that has a team-group entry
|
||||
// pointing at the team.
|
||||
if (excludeTeamId !== undefined) {
|
||||
whereClause.organisationGroupMembers = {
|
||||
none: {
|
||||
group: {
|
||||
teamGroups: {
|
||||
some: { teamId: excludeTeamId },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const [data, count] = await Promise.all([
|
||||
prisma.organisationMember.findMany({
|
||||
where: whereClause,
|
||||
|
||||
@@ -17,6 +17,12 @@ import { OrganisationMemberSchema } from '@documenso/prisma/generated/zod/modelS
|
||||
|
||||
export const ZFindOrganisationMembersRequestSchema = ZFindSearchParamsSchema.extend({
|
||||
organisationId: z.string(),
|
||||
/**
|
||||
* Exclude organisation members who are already members of the given team.
|
||||
* Useful for "add members to team" pickers so that members already on the
|
||||
* team don't appear in the dropdown.
|
||||
*/
|
||||
excludeTeamId: z.number().optional(),
|
||||
});
|
||||
|
||||
export const ZFindOrganisationMembersResponseSchema = ZFindResultResponse.extend({
|
||||
|
||||
@@ -65,6 +65,8 @@ export const createTeamGroupsRoute = authenticatedProcedure
|
||||
},
|
||||
});
|
||||
|
||||
// Hard validation — these failures indicate programming or authorisation
|
||||
// errors and should reject the whole request.
|
||||
const isValid = groups.every((group) => {
|
||||
const organisationGroup = team.organisation.groups.find(
|
||||
({ id }) => id === group.organisationGroupId,
|
||||
@@ -84,11 +86,6 @@ export const createTeamGroupsRoute = authenticatedProcedure
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check that the group is not already added to the team.
|
||||
if (organisationGroup.teamGroups.some((teamGroup) => teamGroup.teamId === teamId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check that the user has permission to add the group to the team.
|
||||
if (!isTeamRoleWithinUserHierarchy(currentUserTeamRole, group.teamRole)) {
|
||||
return false;
|
||||
@@ -103,8 +100,23 @@ export const createTeamGroupsRoute = authenticatedProcedure
|
||||
});
|
||||
}
|
||||
|
||||
// Silently drop groups already attached to the team. Makes the create
|
||||
// idempotent for the common race where a group was added between the
|
||||
// picker fetch and the submit.
|
||||
const filteredGroups = groups.filter((group) => {
|
||||
const organisationGroup = team.organisation.groups.find(
|
||||
({ id }) => id === group.organisationGroupId,
|
||||
);
|
||||
|
||||
return !organisationGroup?.teamGroups.some((teamGroup) => teamGroup.teamId === teamId);
|
||||
});
|
||||
|
||||
if (filteredGroups.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
await prisma.teamGroup.createMany({
|
||||
data: groups.map((group) => ({
|
||||
data: filteredGroups.map((group) => ({
|
||||
id: generateDatabaseId('team_group'),
|
||||
teamId,
|
||||
organisationGroupId: group.organisationGroupId,
|
||||
|
||||
@@ -146,15 +146,55 @@ export const createTeamMembers = async ({
|
||||
});
|
||||
}
|
||||
|
||||
const teamRoleGroupId = (role: TeamMemberRole) =>
|
||||
match(role)
|
||||
.with(TeamMemberRole.MEMBER, () => teamMemberGroup.organisationGroupId)
|
||||
.with(TeamMemberRole.MANAGER, () => teamManagerGroup.organisationGroupId)
|
||||
.with(TeamMemberRole.ADMIN, () => teamAdminGroup.organisationGroupId)
|
||||
.exhaustive();
|
||||
|
||||
// Silently drop additions that would duplicate an existing membership in
|
||||
// the same internal-team role group (the only case that would hit the
|
||||
// (organisationMemberId, groupId) unique constraint). Members who are
|
||||
// implicitly part of the team via an INTERNAL_ORGANISATION group are NOT
|
||||
// dropped, so this still allows assigning an explicit team role on top
|
||||
// of the inherited org-level membership.
|
||||
const existingTeamGroupMemberships = await prisma.organisationGroupMember.findMany({
|
||||
where: {
|
||||
organisationMemberId: {
|
||||
in: membersToCreate.map((member) => member.organisationMemberId),
|
||||
},
|
||||
groupId: {
|
||||
in: [
|
||||
teamMemberGroup.organisationGroupId,
|
||||
teamManagerGroup.organisationGroupId,
|
||||
teamAdminGroup.organisationGroupId,
|
||||
],
|
||||
},
|
||||
},
|
||||
select: { organisationMemberId: true, groupId: true },
|
||||
});
|
||||
|
||||
const existingPairs = new Set(
|
||||
existingTeamGroupMemberships.map(
|
||||
({ organisationMemberId, groupId }) => `${organisationMemberId}:${groupId}`,
|
||||
),
|
||||
);
|
||||
|
||||
const filteredMembersToCreate = membersToCreate.filter(
|
||||
(member) =>
|
||||
!existingPairs.has(`${member.organisationMemberId}:${teamRoleGroupId(member.teamRole)}`),
|
||||
);
|
||||
|
||||
if (filteredMembersToCreate.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
await prisma.organisationGroupMember.createMany({
|
||||
data: membersToCreate.map((member) => ({
|
||||
data: filteredMembersToCreate.map((member) => ({
|
||||
id: generateDatabaseId('group_member'),
|
||||
organisationMemberId: member.organisationMemberId,
|
||||
groupId: match(member.teamRole)
|
||||
.with(TeamMemberRole.MEMBER, () => teamMemberGroup.organisationGroupId)
|
||||
.with(TeamMemberRole.MANAGER, () => teamManagerGroup.organisationGroupId)
|
||||
.with(TeamMemberRole.ADMIN, () => teamAdminGroup.organisationGroupId)
|
||||
.exhaustive(),
|
||||
groupId: teamRoleGroupId(member.teamRole),
|
||||
})),
|
||||
});
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user