Skip to content

Commit

Permalink
Merge branch 'main' into refactor/sidebar
Browse files Browse the repository at this point in the history
  • Loading branch information
DonKoko authored Dec 2, 2024
2 parents e1b3c20 + f2f1669 commit 1328db0
Show file tree
Hide file tree
Showing 13 changed files with 177 additions and 46 deletions.
24 changes: 17 additions & 7 deletions app/components/errors/error-404-handler.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { useFetcher } from "@remix-run/react";
import { isFormProcessing } from "~/utils/form";
import { tw } from "~/utils/tw";
import type { Error404AdditionalData } from "./utils";
import { getModelLabelForEnumValue } from "./utils";
import {
Select,
SelectContent,
Expand Down Expand Up @@ -31,22 +32,26 @@ export default function Error404Handler({
case "asset":
case "kit":
case "location":
case "booking": {

case "booking":
case "customField": {
const modelLabel = getModelLabelForEnumValue(additionalData.model);

return (
<div className="flex flex-col items-center text-center">
<div className="w-full md:max-w-screen-sm">
<h2 className="mb-2">
<span className="capitalize">{additionalData.model}</span>{" "}
belongs to another workspace.

<span className="capitalize">{modelLabel}</span> belongs to
another workspace.
</h2>
<p className="mb-4">
The {additionalData.model} you are trying to view belongs to a
different workspace you are part of. Would you like to switch to
workspace{" "}
The {modelLabel} you are trying to view belongs to a different
workspace you are part of. Would you like to switch to workspace{" "}
<span className="font-bold">
"{additionalData.organization.organization.name}"
</span>{" "}
to view the {additionalData.model}?
to view the {modelLabel}?
</p>
<fetcher.Form
action="/api/user/change-current-organization"
Expand All @@ -69,6 +74,11 @@ export default function Error404Handler({
);
}


/**
* User can have a teamMember in multiple organizations, so in this case we
* show a Select to choose from the organization and switch to that.
**/
case "teamMember": {
return (
<div className="flex flex-col items-center text-center">
Expand Down
60 changes: 50 additions & 10 deletions app/components/errors/utils.ts
Original file line number Diff line number Diff line change
@@ -1,41 +1,66 @@
import { z } from "zod";
import { isRouteError } from "~/utils/http";

/**
* Base schema for additional error data.
* Contains common fields used across different error types.
*/
const baseAdditionalDataSchema = z.object({
/** Unique identifier for the error instance */
id: z.string(),
/** Optional URL to redirect the user to after error handling */
redirectTo: z.string().optional(),
});

/**
* Schema defining organization structure used in error data
*/
const organizationSchema = z.object({
organization: z.object({
/** Organization's unique identifier */
id: z.string(),
/** Organization's display name */
name: z.string(),
}),
});

/**
* Schema for 404 error additional data.
* Uses a discriminated union to handle different model types with specific requirements.
*/
export const error404AdditionalDataSchema = z.discriminatedUnion("model", [
/* For common and general use case */
baseAdditionalDataSchema.extend({
model: z.enum(["asset", "kit", "location", "booking"]),
/** Type of resource that wasn't found */
model: z.enum(["asset", "kit", "location", "booking", "customField"]),
/** Organization context where the resource wasn't found */
organization: organizationSchema,
}),
/* A team member (user) can be in multiple organization's of user so we do this. */
baseAdditionalDataSchema.extend({
/** Specific case for team member not found errors */
model: z.literal("teamMember"),
/** List of organizations the team member could belong to */
organizations: organizationSchema.array(),
}),
]);

export type Error404AdditionalData = z.infer<
typeof error404AdditionalDataSchema
>;
/**
* Type definition for the 404 error additional data structure
*/
export type Error404AdditionalData = z.infer<typeof error404AdditionalDataSchema>;

export function parse404ErrorData(response: unknown):
| { isError404: false; additionalData: null }
| {
isError404: true;
additionalData: Error404AdditionalData;
} {
/**
* Parses and validates the structure of a 404 error response.
*
* @param response - The unknown response to be parsed
* @returns An object indicating whether it's a valid 404 error and its additional data
* If it's not a valid 404 error or parsing fails, returns {isError404: false, additionalData: null}
* If it's a valid 404 error, returns {isError404: true, additionalData: Error404AdditionalData}
*/
export function parse404ErrorData(
response: unknown
): { isError404: false; additionalData: null } | { isError404: true; additionalData: Error404AdditionalData } {
if (!isRouteError(response)) {
return { isError404: false, additionalData: null };
}
Expand All @@ -50,3 +75,18 @@ export function parse404ErrorData(response: unknown):

return { isError404: true, additionalData: parsedDataResponse.data };
}

/**
* Converts a model enum value to a human-readable label.
*
* @param model - The model type from Error404AdditionalData
* @returns A string representing the human-readable label for the model
*/
export function getModelLabelForEnumValue(
model: Error404AdditionalData["model"]
): string {
if (model === "customField") {
return "Custom field";
}
return model;
}
3 changes: 2 additions & 1 deletion app/modules/asset/service.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ import {
isNotFoundError,
maybeUniqueConstraintViolation,
} from "~/utils/error";
import { getRedirectUrlFromRequest } from "~/utils/http";
import { getCurrentSearchParams } from "~/utils/http.server";
import { id } from "~/utils/id/id.server";
import { ALL_SELECTED_KEY, getParamsValues } from "~/utils/list";
Expand Down Expand Up @@ -136,7 +137,7 @@ export async function getAsset<T extends Prisma.AssetInclude | undefined>({
) {
const redirectTo =
typeof request !== "undefined"
? new URL(request.url).pathname
? getRedirectUrlFromRequest(request)
: undefined;

throw new ShelfError({
Expand Down
4 changes: 2 additions & 2 deletions app/modules/booking/service.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { calcTimeDifference } from "~/utils/date-fns";
import { sendNotification } from "~/utils/emitter/send-notification.server";
import type { ErrorLabel } from "~/utils/error";
import { isLikeShelfError, isNotFoundError, ShelfError } from "~/utils/error";
import { getRedirectUrlFromRequest } from "~/utils/http";
import { getCurrentSearchParams } from "~/utils/http.server";
import { ALL_SELECTED_KEY } from "~/utils/list";
import { Logger } from "~/utils/logger";
Expand Down Expand Up @@ -887,7 +888,6 @@ export async function getBooking<T extends Prisma.BookingInclude | undefined>(
* For reserving a booking, we need to make sure that the assets in the booking dont have any other bookings that overlap with the current booking
* Moreover we just query certain statuses as they are the only ones that matter for an asset being considered unavailable
*/

const mergedInclude = {
...BOOKING_WITH_ASSETS_INCLUDE,
...extraInclude,
Expand Down Expand Up @@ -917,7 +917,7 @@ export async function getBooking<T extends Prisma.BookingInclude | undefined>(
) {
const redirectTo =
typeof request !== "undefined"
? new URL(request.url).pathname
? getRedirectUrlFromRequest(request)
: undefined;

throw new ShelfError({
Expand Down
76 changes: 70 additions & 6 deletions app/modules/custom-field/service.server.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import type { CustomField, Organization, Prisma, User } from "@prisma/client";
import type {
CustomField,
Organization,
Prisma,
User,
UserOrganization,
} from "@prisma/client";
import { db } from "~/database/db.server";
import { getDefinitionFromCsvHeader } from "~/utils/custom-fields";
import type { ErrorLabel } from "~/utils/error";
Expand All @@ -7,6 +13,7 @@ import {
isLikeShelfError,
maybeUniqueConstraintViolation,
} from "~/utils/error";
import { getRedirectUrlFromRequest } from "~/utils/http";
import type { CustomFieldDraftPayload } from "./types";
import type { CreateAssetFromContentImportPayload } from "../asset/types";
import type { Column } from "../asset-index-settings/helpers";
Expand Down Expand Up @@ -140,24 +147,81 @@ export async function getFilteredAndPaginatedCustomFields(params: {
}
}

export async function getCustomField({
type CustomFieldWithInclude<T extends Prisma.CustomFieldInclude | undefined> =
T extends Prisma.CustomFieldInclude
? Prisma.CustomFieldGetPayload<{ include: T }>
: CustomField;

export async function getCustomField<
T extends Prisma.CustomFieldInclude | undefined,
>({
organizationId,
id,
userOrganizations,
request,
include,
}: Pick<CustomField, "id"> & {
organizationId: Organization["id"];
userOrganizations?: Pick<UserOrganization, "organizationId">[];
request?: Request;
include?: T;
}) {
try {
return await db.customField.findFirstOrThrow({
where: { id, organizationId },
include: { categories: { select: { id: true } } },
const otherOrganizationIds = userOrganizations?.map(
(org) => org.organizationId
);

const customField = await db.customField.findFirstOrThrow({
where: {
OR: [
{ id, organizationId },
...(userOrganizations?.length
? [{ id, organizationId: { in: otherOrganizationIds } }]
: []),
],
},
include: { ...include },
});

/* User is trying to access customField in wrong organization. */
if (
userOrganizations?.length &&
customField.organizationId !== organizationId &&
otherOrganizationIds?.includes(customField.organizationId)
) {
const redirectTo =
typeof request !== "undefined"
? getRedirectUrlFromRequest(request)
: undefined;

throw new ShelfError({
cause: null,
title: "Custom field not found",
message: "",
additionalData: {
model: "customField",
organization: userOrganizations.find(
(org) => org.organizationId === customField.organizationId
),
redirectTo,
},
label,
status: 404,
});
}

return customField as CustomFieldWithInclude<T>;
} catch (cause) {
throw new ShelfError({
cause,
title: "Custom field not found",
message:
"The custom field you are trying to access does not exist or you do not have permission to access it.",
additionalData: { id, organizationId },
additionalData: {
id,
organizationId,
...(isLikeShelfError(cause) ? cause.additionalData : {}),
},
label,
});
}
Expand Down
3 changes: 2 additions & 1 deletion app/modules/kit/service.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
ShelfError,
} from "~/utils/error";
import { extractImageNameFromSupabaseUrl } from "~/utils/extract-image-name-from-supabase-url";
import { getRedirectUrlFromRequest } from "~/utils/http";
import { getCurrentSearchParams } from "~/utils/http.server";
import { id } from "~/utils/id/id.server";
import { ALL_SELECTED_KEY, getParamsValues } from "~/utils/list";
Expand Down Expand Up @@ -396,7 +397,7 @@ export async function getKit<T extends Prisma.KitInclude | undefined>({
) {
const redirectTo =
typeof request !== "undefined"
? new URL(request.url).pathname
? getRedirectUrlFromRequest(request)
: undefined;

throw new ShelfError({
Expand Down
3 changes: 2 additions & 1 deletion app/modules/location/service.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
isNotFoundError,
maybeUniqueConstraintViolation,
} from "~/utils/error";
import { getRedirectUrlFromRequest } from "~/utils/http";
import { ALL_SELECTED_KEY } from "~/utils/list";
import type { CreateAssetFromContentImportPayload } from "../asset/types";

Expand Down Expand Up @@ -104,7 +105,7 @@ export async function getLocation(
) {
const redirectTo =
typeof request !== "undefined"
? new URL(request.url).pathname
? getRedirectUrlFromRequest(request)
: undefined;

throw new ShelfError({
Expand Down
4 changes: 2 additions & 2 deletions app/modules/user/service.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import {
import { dateTimeInUnix } from "~/utils/date-time-in-unix";
import type { ErrorLabel } from "~/utils/error";
import { ShelfError, isLikeShelfError, isNotFoundError } from "~/utils/error";
import type { ValidationError } from "~/utils/http";
import { getRedirectUrlFromRequest, type ValidationError } from "~/utils/http";
import { getCurrentSearchParams } from "~/utils/http.server";
import { id as generateId } from "~/utils/id/id.server";
import { getParamsValues } from "~/utils/list";
Expand Down Expand Up @@ -1308,7 +1308,7 @@ export async function getUserFromOrg<T extends Prisma.UserInclude | undefined>({
) {
const redirectTo =
typeof request !== "undefined"
? new URL(request.url).pathname
? getRedirectUrlFromRequest(request)
: undefined;

throw new ShelfError({
Expand Down
17 changes: 6 additions & 11 deletions app/routes/_layout+/bookings.$bookingId.cal[.ics].ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,20 +21,15 @@ export async function loader({ request, context, params }: LoaderFunctionArgs) {

try {
/** Check if the current user is allowed to read booking */
const { organizationId, isSelfServiceOrBase, userOrganizations } =
await requirePermission({
userId: authSession.userId,
request,
entity: PermissionEntity.booking,
action: PermissionAction.read,
});
const booking = await getBooking({
id: bookingId,
organizationId,
userOrganizations,
const { organizationId, isSelfServiceOrBase } = await requirePermission({
userId: authSession.userId,
request,
entity: PermissionEntity.booking,
action: PermissionAction.read,
});

const booking = await getBooking({ id: bookingId, organizationId });

/** For self service & base users, we only allow them to read their own bookings */
if (isSelfServiceOrBase && booking.custodianUserId !== authSession.userId) {
throw new ShelfError({
Expand Down
3 changes: 3 additions & 0 deletions app/routes/_layout+/locations.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { LoaderFunctionArgs } from "@remix-run/node";
import { Link, Outlet, json } from "@remix-run/react";
import { ErrorContent } from "~/components/errors";
import { makeShelfError } from "~/utils/error";
import { data, error } from "~/utils/http.server";
import {
Expand Down Expand Up @@ -34,3 +35,5 @@ export const handle = {
export default function LocationsPage() {
return <Outlet />;
}

export const ErrorBoundary = () => <ErrorContent />;
Loading

0 comments on commit 1328db0

Please sign in to comment.