Skip to content

Commit e4b610f

Browse files
authored
Merge pull request #1660 from rockingrohit9639/feature/enhance-user-import
Feature/enhance user import
2 parents 7a6be51 + 8a696f0 commit e4b610f

File tree

5 files changed

+132
-38
lines changed

5 files changed

+132
-38
lines changed

app/components/settings/import-users-dialog/import-users-dialog.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,10 @@ export default function ImportUsersDialog({
141141
Invited users will receive an email with a link to join the
142142
organization.
143143
</li>
144+
<li>
145+
<b>Optional</b>: You can populate the <b>teamMemberId</b>{" "}
146+
column if you want the user to get linked to an existing NRM.
147+
</li>
144148
</ul>
145149

146150
<h4 className="mt-2">Extra Considerations</h4>

app/modules/invite/service.server.ts

Lines changed: 116 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@ import type {
66
User,
77
} from "@prisma/client";
88
import { InviteStatuses } from "@prisma/client";
9+
import { PrismaClientKnownRequestError } from "@prisma/client/runtime/library";
910
import type { AppLoadContext, LoaderFunctionArgs } from "@remix-run/node";
1011
import jwt from "jsonwebtoken";
1112
import lodash from "lodash";
13+
import invariant from "tiny-invariant";
1214
import type { z } from "zod";
1315
import type { InviteUserFormSchema } from "~/components/settings/invite-user-dialog";
1416
import { db } from "~/database/db.server";
@@ -66,7 +68,7 @@ async function validateInvite(
6668
throw new ShelfError({
6769
cause: null,
6870
message:
69-
"This email domain uses SSO authentication. The user needs to sign up via SSO before they can be invited.",
71+
"This email domain uses SSO authentication. The user needs to sign up via SSO to get access to the organization.",
7072
label: "Invite",
7173
status: 400,
7274
shouldBeCaptured: false,
@@ -552,7 +554,7 @@ export async function bulkInviteUsers({
552554
organizationId,
553555
extraMessage,
554556
}: {
555-
users: Omit<InviteUserSchema, "teamMemberId">[];
557+
users: InviteUserSchema[];
556558
userId: User["id"];
557559
organizationId: Organization["id"];
558560
extraMessage?: string | null;
@@ -568,6 +570,23 @@ export async function bulkInviteUsers({
568570
)
569571
);
570572

573+
const teamMemberIds = uniquePayloads
574+
.filter((user) => !!user.teamMemberId)
575+
.map((user) => user.teamMemberId!);
576+
577+
const teamMembers = await db.teamMember.findMany({
578+
where: { id: { in: teamMemberIds } },
579+
select: { id: true, userId: true },
580+
});
581+
582+
/**
583+
* These teamMembers has a user already associated.
584+
* So we will skip them from the invite process.
585+
* */
586+
const teamMembersWithUserId = teamMembers
587+
.filter((tm) => !!tm.userId)
588+
.map((tm) => tm.id!);
589+
571590
// Batch check for existing users
572591
const emails = uniquePayloads.map((p) => p.email);
573592
const existingUsers = await db.user.findMany({
@@ -661,47 +680,83 @@ export async function bulkInviteUsers({
661680
* - users who have not PENDING invitation and
662681
* - users who are not part of organization already
663682
*/
664-
const validPayloads = uniquePayloads.filter(
683+
let validPayloads = uniquePayloads.filter(
665684
(p) =>
666685
!existingInviteEmails.includes(p.email) &&
667686
!existingEmailsInOrg.has(p.email)
668687
);
669688

689+
/** Remove the users with teamMemberId who already have a user associated */
690+
validPayloads = validPayloads.filter((payload) => {
691+
if (!payload.teamMemberId) {
692+
return true;
693+
}
694+
695+
return !teamMembersWithUserId.includes(payload.teamMemberId);
696+
});
697+
670698
const validPayloadsWithName = validPayloads.map((p) => ({
671699
...p,
672700
name: p.email.split("@")[0],
673701
}));
674702

675-
const createdTeamMembers = await db.teamMember.createManyAndReturn({
676-
data: validPayloadsWithName.map((p) => ({
677-
name: p.name,
678-
organizationId,
679-
})),
680-
});
681-
682703
// Prepare invite data
683704
const expiresAt = new Date();
684705
expiresAt.setDate(expiresAt.getDate() + INVITE_EXPIRY_TTL_DAYS);
685706

686-
const invitesToCreate = validPayloadsWithName.map((payload) => ({
687-
inviterId: userId,
688-
organizationId,
689-
inviteeEmail: payload.email,
690-
teamMemberId:
691-
createdTeamMembers.find((tm) => tm.name === payload.name)?.id ?? "",
692-
roles: [payload.role],
693-
expiresAt,
694-
inviteCode: generateRandomCode(6),
695-
status: InviteStatuses.PENDING,
696-
}));
707+
const INVITE_INCLUDE = {
708+
inviter: { select: { firstName: true, lastName: true } },
709+
organization: true,
710+
} satisfies Prisma.InviteInclude;
697711

698-
// Bulk create invites
699-
const createdInvites = await db.invite.createManyAndReturn({
700-
data: invitesToCreate,
701-
include: {
702-
inviter: { select: { firstName: true, lastName: true } },
703-
organization: true,
704-
},
712+
let createdInvites: Array<
713+
Prisma.InviteGetPayload<{ include: typeof INVITE_INCLUDE }>
714+
> = [];
715+
716+
await db.$transaction(async (tx) => {
717+
// Bulk create all required team members
718+
const createdTeamMembers = await tx.teamMember.createManyAndReturn({
719+
data: validPayloadsWithName.map((p) => ({
720+
name: p.name,
721+
organizationId,
722+
})),
723+
});
724+
725+
/**
726+
* This helper function returns the correct teamMemberId required for creating an invite
727+
*/
728+
function getTeamMemberId(payload: InviteUserSchema & { name: string }) {
729+
if (payload.teamMemberId) {
730+
return payload.teamMemberId;
731+
}
732+
733+
const createdTm = createdTeamMembers.find(
734+
(tm) => tm.name === payload.name
735+
);
736+
invariant(
737+
createdTm,
738+
"Unexpected situation! Could not find teamMember in createdTeamMembers."
739+
);
740+
741+
return createdTm.id;
742+
}
743+
744+
const invitesToCreate = validPayloadsWithName.map((payload) => ({
745+
inviterId: userId,
746+
organizationId,
747+
inviteeEmail: payload.email,
748+
teamMemberId: getTeamMemberId(payload),
749+
roles: [payload.role],
750+
expiresAt,
751+
inviteCode: generateRandomCode(6),
752+
status: InviteStatuses.PENDING,
753+
}));
754+
755+
// Bulk create invites
756+
createdInvites = await tx.invite.createManyAndReturn({
757+
data: invitesToCreate,
758+
include: INVITE_INCLUDE,
759+
});
705760
});
706761

707762
// Queue emails for sending - no need to await since it's handled by queue
@@ -725,24 +780,50 @@ export async function bulkInviteUsers({
725780
senderId: userId,
726781
});
727782

728-
const skippedUsers = users.filter(
729-
(user) =>
730-
existingInviteEmails.includes(user.email) ||
731-
existingEmailsInOrg.has(user.email)
732-
);
783+
const skippedUsers = users.filter((user) => {
784+
if (existingInviteEmails.includes(user.email)) {
785+
return true;
786+
}
787+
788+
if (existingEmailsInOrg.has(user.email)) {
789+
return true;
790+
}
791+
792+
if (
793+
user.teamMemberId &&
794+
teamMembersWithUserId.includes(user.teamMemberId)
795+
) {
796+
return true;
797+
}
798+
799+
return false;
800+
});
733801

734802
return {
735803
inviteSentUsers: validPayloads,
736804
skippedUsers,
737805
extraMessage:
738806
createdInvites.length > 10
739-
? "You are sending more than 10 invites, so some of the emails might get slightly delayed. If one of the invitees hasnt received the email within 5-10 minutes, you can use the Resend invite feature to send the email again."
807+
? "You are sending more than 10 invites, so some of the emails might get slightly delayed. If one of the invitees hasn't received the email within 5-10 minutes, you can use the Resend invite feature to send the email again."
740808
: undefined,
741809
};
742810
} catch (cause) {
811+
let message = "Something went wrong while inviting users.";
812+
813+
if (isLikeShelfError(cause)) {
814+
message = cause.message;
815+
}
816+
817+
if (
818+
cause instanceof PrismaClientKnownRequestError &&
819+
cause.code === "P2003"
820+
) {
821+
message = "Received invalid teamMemberId in csv";
822+
}
823+
743824
throw new ShelfError({
744825
cause,
745-
message: "Something went wrong while inviting users.",
826+
message,
746827
label,
747828
additionalData: { users },
748829
});

app/modules/invite/utils.server.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
export const IMPORT_USERS_CSV_HEADERS = ["role", "email"];
1+
export const IMPORT_USERS_CSV_HEADERS = ["role", "email", "teamMemberId"];

app/routes/_layout+/settings.team.nrm.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,8 +206,11 @@ export default function NrmSettings() {
206206
newButtonRoute: "add-member",
207207
newButtonContent: "Add NRM",
208208
}}
209+
hideFirstHeaderColumn
209210
headerChildren={
210211
<>
212+
<Th>ID</Th>
213+
<Th>Name</Th>
211214
<Th>Custodies</Th>
212215
<Th>Actions</Th>
213216
</>
@@ -235,6 +238,11 @@ function TeamMemberRow({
235238
}) {
236239
return (
237240
<>
241+
<Td>
242+
<div>
243+
<div className="pl-4 md:pl-6">{item.id}</div>
244+
</div>
245+
</Td>
238246
<Td className="w-full whitespace-normal">{item.name}</Td>
239247
<Td className="text-right">
240248
{item._count.custodies ? item._count.custodies : 0}
Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
1-
role,email
2-
1+
role,email,teamMemberId
2+
ADMIN,[email protected],cm7ep29mu0001slft9zw9e6sp
3+

0 commit comments

Comments
 (0)