Skip to content

Commit

Permalink
Merge branch 'main' into 1547-bug-generating-a-big-booking-overview-p…
Browse files Browse the repository at this point in the history
…df-breaks-the-whole-app
  • Loading branch information
DonKoko authored Jan 6, 2025
2 parents b0291e6 + d6edad3 commit 8081284
Show file tree
Hide file tree
Showing 13 changed files with 483 additions and 133 deletions.
2 changes: 1 addition & 1 deletion app/components/assets/custom-fields-inputs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ export default function AssetCustomFields({
name={`cf-${field.id}`}
disabled={disabled}
defaultChecked={
getCustomFieldVal(field.id) === "true" || field.required
getCustomFieldVal(field.id) === "Yes" || field.required
}
/>
<label className="font-medium text-gray-700 lg:hidden">
Expand Down
4 changes: 4 additions & 0 deletions app/components/workspace/users-actions-dropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
} from "~/components/shared/dropdown";

import { useControlledDropdownMenu } from "~/hooks/use-controlled-dropdown-menu";
import type { UserFriendlyRoles } from "~/routes/_layout+/settings.team";
import { isFormProcessing } from "~/utils/form";
import { Button } from "../shared/button";
import { Spinner } from "../shared/spinner";
Expand All @@ -26,6 +27,7 @@ export function TeamUsersActionsDropdown({
email,
isSSO,
customTrigger,
role,
}: {
userId: User["id"] | null;
inviteStatus: InviteStatuses;
Expand All @@ -34,6 +36,7 @@ export function TeamUsersActionsDropdown({
email: string;
isSSO: boolean;
customTrigger?: (disabled: boolean) => ReactNode;
role: UserFriendlyRoles;
}) {
const fetcher = useFetcher();
const disabled = isFormProcessing(fetcher.state);
Expand Down Expand Up @@ -85,6 +88,7 @@ export function TeamUsersActionsDropdown({
<input type="hidden" name="name" value={name} />
<input type="hidden" name="email" value={email} />
<input type="hidden" name="teamMemberId" value={teamMemberId} />
<input type="hidden" name="userFriendlyRole" value={role} />
<Button
type="submit"
variant="link"
Expand Down
5 changes: 5 additions & 0 deletions app/hooks/use-sidebar-nav-items.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,11 @@ export function useSidebarNavItems() {
to: "/settings/team/users",
hidden: isPersonalOrganization,
},
{
title: "Pending invites",
to: "/settings/team/invites",
hidden: isPersonalOrganization,
},
{
title: "Non-registered members",
to: "/settings/team/nrm",
Expand Down
119 changes: 117 additions & 2 deletions app/modules/invite/service.server.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
import type { Invite, TeamMember } from "@prisma/client";
import type { Invite, Organization, Prisma, TeamMember } from "@prisma/client";
import { InviteStatuses } from "@prisma/client";
import type { AppLoadContext } from "@remix-run/node";
import type { AppLoadContext, LoaderFunctionArgs } from "@remix-run/node";
import jwt from "jsonwebtoken";
import { db } from "~/database/db.server";
import { invitationTemplateString } from "~/emails/invite-template";
import { sendEmail } from "~/emails/mail.server";
import { organizationRolesMap } from "~/routes/_layout+/settings.team";
import { INVITE_EXPIRY_TTL_DAYS } from "~/utils/constants";
import { updateCookieWithPerPage } from "~/utils/cookies.server";
import { sendNotification } from "~/utils/emitter/send-notification.server";
import { INVITE_TOKEN_SECRET } from "~/utils/env";
import type { ErrorLabel } from "~/utils/error";
import { ShelfError, isLikeShelfError } from "~/utils/error";
import { getCurrentSearchParams } from "~/utils/http.server";
import { getParamsValues } from "~/utils/list";
import { checkDomainSSOStatus, doesSSOUserExist } from "~/utils/sso.server";
import { generateRandomCode, inviteEmailText } from "./helpers";
import { createTeamMember } from "../team-member/service.server";
Expand Down Expand Up @@ -419,3 +423,114 @@ export async function checkUserAndInviteMatch({
});
}
}

/** Gets invites for settings.team.invites page */
export async function getPaginatedAndFilterableSettingInvites({
organizationId,
request,
}: {
organizationId: Organization["id"];
request: LoaderFunctionArgs["request"];
}) {
const searchParams = getCurrentSearchParams(request);
const paramsValues = getParamsValues(searchParams);

const { page, perPageParam, search } = paramsValues;

const inviteStatus =
searchParams.get("inviteStatus") === "ALL"
? null
: (searchParams.get("inviteStatus") as InviteStatuses);

const cookie = await updateCookieWithPerPage(request, perPageParam);
const { perPage } = cookie;

try {
const skip = page > 1 ? (page - 1) * perPage : 0;
const take = perPage >= 1 && perPage <= 100 ? perPage : 200;

const inviteWhere: Prisma.InviteWhereInput = {
organizationId,
status: InviteStatuses.PENDING,
inviteeEmail: { not: "" },
};

if (search) {
/** Or search the input against input user/teamMember */
inviteWhere.OR = [
{
inviteeTeamMember: {
name: { contains: search, mode: "insensitive" },
},
},
{
inviteeUser: {
OR: [
{ firstName: { contains: search, mode: "insensitive" } },
{ lastName: { contains: search, mode: "insensitive" } },
],
},
},
];
}

if (inviteStatus) {
inviteWhere.status = inviteStatus;
}

const [invites, totalItemsGrouped] = await Promise.all([
/** Get the invites */
db.invite.findMany({
where: inviteWhere,
distinct: ["inviteeEmail"],
skip,
take,
select: {
id: true,
teamMemberId: true,
inviteeEmail: true,
status: true,
inviteeTeamMember: { select: { name: true } },
roles: true,
},
}),

db.invite.groupBy({
by: ["inviteeEmail"],
where: inviteWhere,
}),
]);

/**
* Create the same structure for the invites
*/
const items = invites.map((invite) => ({
id: invite.id,
name: invite.inviteeTeamMember.name,
img: "/static/images/default_pfp.jpg",
email: invite.inviteeEmail,
status: invite.status,
role: organizationRolesMap[invite?.roles[0]],
userId: null,
sso: false,
}));
const totalItems = totalItemsGrouped.length;
const totalPages = Math.ceil(totalItems / perPage);

return {
page,
perPage,
totalPages,
search,
items,
totalItems,
};
} catch (cause) {
throw new ShelfError({
cause,
message: "Something went wrong while getting registered users",
additionalData: { organizationId },
label,
});
}
}
119 changes: 19 additions & 100 deletions app/modules/settings/service.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,6 @@ export async function getPaginatedAndFilterableSettingUsers({

const { page, perPageParam, search } = paramsValues;

const inviteStatus =
searchParams.get("inviteStatus") === "ALL"
? null
: (searchParams.get("inviteStatus") as InviteStatuses);

const cookie = await updateCookieWithPerPage(request, perPageParam);
const { perPage } = cookie;

Expand All @@ -54,12 +49,6 @@ export async function getPaginatedAndFilterableSettingUsers({
organizationId,
};

const inviteWhere: Prisma.InviteWhereInput = {
organizationId,
status: InviteStatuses.PENDING,
inviteeEmail: { not: "" },
};

if (search) {
/** Either search the input against organization's user */
userOrganizationWhere.user = {
Expand All @@ -68,86 +57,33 @@ export async function getPaginatedAndFilterableSettingUsers({
{ lastName: { contains: search, mode: "insensitive" } },
],
};

/** Or search the input against input user/teamMember */
inviteWhere.OR = [
{
inviteeTeamMember: {
name: { contains: search, mode: "insensitive" },
},
},
{
inviteeUser: {
OR: [
{ firstName: { contains: search, mode: "insensitive" } },
{ lastName: { contains: search, mode: "insensitive" } },
],
},
},
];
}

if (inviteStatus) {
Object.assign(userOrganizationWhere, {
user: {
receivedInvites: { some: { status: inviteStatus } },
},
});
inviteWhere.status = inviteStatus;
}

/**
* We have to get the items from two different data models, so have to
* divide skip and take into two part to get equal items from each model
*/
const finalSkip = skip / 2;
const finalTake = take / 2;

const [userMembers, invites, totalUserMembers, totalInvites] =
await Promise.all([
/** Get Users */
db.userOrganization.findMany({
where: userOrganizationWhere,
skip: finalSkip,
take: finalTake,
select: {
user: {
include: {
teamMembers: {
where: { organizationId },
include: {
_count: {
select: { custodies: true },
},
const [userMembers, totalItems] = await Promise.all([
/** Get Users */
db.userOrganization.findMany({
where: userOrganizationWhere,
skip,
take,
select: {
user: {
include: {
teamMembers: {
where: { organizationId },
include: {
_count: {
select: { custodies: true },
},
},
},
},
roles: true,
},
}),
/** Get the invites */
db.invite.findMany({
where: inviteWhere,
distinct: ["inviteeEmail"],
skip: finalSkip,
take: finalTake,
select: {
id: true,
teamMemberId: true,
inviteeEmail: true,
status: true,
inviteeTeamMember: { select: { name: true } },
roles: true,
},
}),
db.userOrganization.count({ where: userOrganizationWhere }),
roles: true,
},
}),

db.invite.groupBy({
by: ["inviteeEmail"],
where: inviteWhere,
}),
]);
db.userOrganization.count({ where: userOrganizationWhere }),
]);

/**
* Create a structure for the users org members and merge it with invites
Expand All @@ -167,23 +103,6 @@ export async function getPaginatedAndFilterableSettingUsers({
custodies: um?.user?.teamMembers?.[0]?._count?.custodies || 0,
}));

/**
* Create the same structure for the invites
*/
for (const invite of invites) {
teamMembersWithUserOrInvite.push({
id: invite.id,
name: invite.inviteeTeamMember.name,
img: "/static/images/default_pfp.jpg",
email: invite.inviteeEmail,
status: invite.status,
role: organizationRolesMap[invite?.roles[0]],
userId: null,
sso: false,
});
}

const totalItems = totalUserMembers + totalInvites.length;
const totalPages = Math.ceil(totalItems / perPage);

return {
Expand Down
Loading

0 comments on commit 8081284

Please sign in to comment.