Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/improve 404 handling #1433

Merged
merged 36 commits into from
Dec 2, 2024
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
fb1ef89
feat(404): add additionalData in getAsset for special case
rockingrohit9639 Nov 19, 2024
2514f86
feat(special-error): create UI for handling special error cases
rockingrohit9639 Nov 19, 2024
74fe491
Merge branch 'main' of github.com:Shelf-nu/shelf.nu into feature/impr…
rockingrohit9639 Nov 19, 2024
9d580a2
Merge branch 'main' of github.com:Shelf-nu/shelf.nu into feature/impr…
rockingrohit9639 Nov 20, 2024
fb73871
pr review changes
rockingrohit9639 Nov 20, 2024
bbe1653
Merge branch 'main' of github.com:Shelf-nu/shelf.nu into feature/impr…
rockingrohit9639 Nov 22, 2024
82f3147
feat(improve-404): fix pr review issues
rockingrohit9639 Nov 22, 2024
c08bcb1
Merge branch 'main' into feature/improve-404-handling
DonKoko Nov 22, 2024
532f6d8
feat(improve-404): add request in getAsset for proper redirecting
rockingrohit9639 Nov 23, 2024
ca5fe65
feat(improve-404): update getKit function to make it more generic
rockingrohit9639 Nov 23, 2024
7583eef
feat(improve-404): improve 404 handling for kit
rockingrohit9639 Nov 25, 2024
96f723b
Merge branch 'main' of github.com:Shelf-nu/shelf.nu into feature/impr…
rockingrohit9639 Nov 26, 2024
a1e3c36
feat(improve-404): improve 404 handling for location
rockingrohit9639 Nov 26, 2024
ff01bef
feature(workflow): create separate function for team with 404 handling
rockingrohit9639 Nov 26, 2024
cf330c2
feat(improve-404): handling special case of multiple organization for…
rockingrohit9639 Nov 26, 2024
e460b90
feat(improve-404): fix style issue on ErrorBoundary for team settings
rockingrohit9639 Nov 27, 2024
5ec5d9e
feat(improve-404): improve 404 handling for bookings
rockingrohit9639 Nov 27, 2024
bf91979
feat(404): add additionalData in getAsset for special case
rockingrohit9639 Nov 19, 2024
a708ae4
feat(special-error): create UI for handling special error cases
rockingrohit9639 Nov 19, 2024
b9f50e6
pr review changes
rockingrohit9639 Nov 20, 2024
473c9dc
feat(improve-404): fix pr review issues
rockingrohit9639 Nov 22, 2024
5b1496b
feat(improve-404): add request in getAsset for proper redirecting
rockingrohit9639 Nov 23, 2024
9cb7f02
feat(improve-404): update getKit function to make it more generic
rockingrohit9639 Nov 23, 2024
3bedc33
feat(improve-404): improve 404 handling for kit
rockingrohit9639 Nov 25, 2024
8cfd653
feat(improve-404): improve 404 handling for location
rockingrohit9639 Nov 26, 2024
92bc9aa
feature(workflow): create separate function for team with 404 handling
rockingrohit9639 Nov 26, 2024
e023233
feat(improve-404): handling special case of multiple organization for…
rockingrohit9639 Nov 26, 2024
1476c9d
feat(improve-404): fix style issue on ErrorBoundary for team settings
rockingrohit9639 Nov 27, 2024
bcea05b
feat(improve-404): improve 404 handling for bookings
rockingrohit9639 Nov 27, 2024
6342658
Merge branch 'main' into feature/improve-404-handling
rockingrohit9639 Nov 27, 2024
c15bcae
Merge branch 'feature/improve-404-handling' of github.com:rockingrohi…
rockingrohit9639 Nov 28, 2024
64607bd
Merge branch 'main' of github.com:Shelf-nu/shelf.nu into feature/impr…
rockingrohit9639 Nov 28, 2024
9145221
feat(improve-404): done pr review changes
rockingrohit9639 Nov 28, 2024
260e1e7
feat(improve-404): improve 404 handling for customField
rockingrohit9639 Nov 28, 2024
331623f
Merge branch 'main' of github.com:Shelf-nu/shelf.nu into feature/impr…
rockingrohit9639 Nov 29, 2024
c3afdb5
feat(improve-404): fix eslint issues
rockingrohit9639 Nov 29, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
136 changes: 136 additions & 0 deletions app/components/errors/error-404-handler.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import { useMemo } from "react";
import { tw } from "~/utils/tw";
import { Error404AdditionalData } from "./utils";
import { Button } from "../shared/button";
import { useFetcher } from "@remix-run/react";
import { isFormProcessing } from "~/utils/form";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "../forms/select";

export type Error404HandlerProps = {
className?: string;
style?: React.CSSProperties;
additionalData: Error404AdditionalData;
};

export default function Error404Handler({
className,
style,
additionalData,
}: Error404HandlerProps) {
const fetcher = useFetcher();
const disabled = isFormProcessing(fetcher.state);

const content = useMemo(() => {
switch (additionalData.model) {
case "asset":
case "kit":
case "location":
case "booking": {
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.
</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{" "}
<span className="font-bold">
"{additionalData.organization.organization.name}"
</span>{" "}
to view the {additionalData.model}?
</p>
<fetcher.Form
action="/api/user/change-current-organization"
method="POST"
>
<input
type="hidden"
name="organizationId"
value={additionalData.organization.organization.id}
/>
<input
type="hidden"
name="redirectTo"
value={additionalData.redirectTo}
/>
<Button disabled={disabled}>Switch workspace</Button>
</fetcher.Form>
</div>
</div>
);
}

rockingrohit9639 marked this conversation as resolved.
Show resolved Hide resolved
case "teamMember": {
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">Team Member</span> belongs to
another workspace(s).
</h2>
<p className="mb-4">
The team member you are trying to view belongs to one/some of
your different workspace you are part of. Would you like to
switch to workspace to view the team member?
</p>
<fetcher.Form
action="/api/user/change-current-organization"
method="POST"
className="flex items-center flex-col"
>
<Select name="organizationId" disabled={disabled}>
<SelectTrigger className="mb-4 px-3.5 py-2 text-left text-gray-500 max-w-80">
<SelectValue placeholder="Select workspace to switch" />
</SelectTrigger>
<SelectContent
position="popper"
className="w-full min-w-80 overflow-auto p-1"
align="start"
>
{additionalData.organizations.map(({ organization }) => (
<SelectItem
value={organization.id}
key={organization.id}
className="px-4 py-2"
>
{organization.name}
</SelectItem>
))}
</SelectContent>
</Select>
<input
type="hidden"
name="redirectTo"
value={additionalData.redirectTo}
/>
<Button disabled={disabled}>Switch workspace</Button>
</fetcher.Form>
</div>
</div>
);
}

default: {
return null;
}
}
}, [additionalData]);

return (
<div
className={tw("flex size-full items-center justify-center", className)}
style={style}
>
{content}
</div>
);
}
24 changes: 22 additions & 2 deletions app/components/errors/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,13 @@ import { useLocation, useRouteError } from "@remix-run/react";

import { isRouteError } from "~/utils/http";
import { Button } from "../shared/button";
import { parse404ErrorData } from "./utils";
import Error404Handler from "./error-404-handler";
import { tw } from "~/utils/tw";

export const ErrorContent = () => {
type ErrorContentProps = { className?: string };

export const ErrorContent = ({ className }: ErrorContentProps) => {
const loc = useLocation();
const response = useRouteError();

Expand All @@ -18,11 +23,26 @@ export const ErrorContent = () => {
traceId = response.data.error.traceId;
}

const error404 = parse404ErrorData(response);
if (error404.isError404) {
return (
<Error404Handler
className={className}
additionalData={error404.additionalData}
/>
);
}

// Creating a string with <br/> tags for line breaks
const messageHtml = { __html: message.split("\n").join("<br/>") };

return (
<div className="flex size-full items-center justify-center">
<div
className={tw(
"h-screen flex size-full items-center justify-center",
className
)}
>
<div className="flex flex-col items-center text-center">
<span className="mb-5 size-[56px] text-primary">
<ErrorIcon />
Expand Down
52 changes: 52 additions & 0 deletions app/components/errors/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { z } from "zod";
import { isRouteError } from "~/utils/http";

const baseAdditionalDataSchema = z.object({
id: z.string(),
redirectTo: z.string().optional(),
});

const organizationSchema = z.object({
organization: z.object({
id: z.string(),
name: z.string(),
}),
});

export const error404AdditionalDataSchema = z.discriminatedUnion("model", [
/* For common and general use case */
baseAdditionalDataSchema.extend({
model: z.enum(["asset", "kit", "location", "booking"]),
organization: organizationSchema,
}),
/* A team member (user) can be in multiple organization's of user so we do this. */
baseAdditionalDataSchema.extend({
model: z.literal("teamMember"),
organizations: organizationSchema.array(),
}),
]);

export type Error404AdditionalData = z.infer<
typeof error404AdditionalDataSchema
>;

export function parse404ErrorData(response: unknown):
| { isError404: false; additionalData: null }
| {
isError404: true;
additionalData: Error404AdditionalData;
} {
if (!isRouteError(response)) {
return { isError404: false, additionalData: null };
}

const parsedDataResponse = error404AdditionalDataSchema.safeParse(
response.data.error.additionalData
);

if (!parsedDataResponse.success) {
return { isError404: false, additionalData: null };
}

return { isError404: true, additionalData: parsedDataResponse.data };
}
51 changes: 49 additions & 2 deletions app/modules/asset/service.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import type {
Booking,
Kit,
AssetIndexSettings,
UserOrganization,
} from "@prisma/client";
import {
AssetStatus,
Expand Down Expand Up @@ -101,25 +102,71 @@ type AssetWithInclude<T extends Prisma.AssetInclude | undefined> =
export async function getAsset<T extends Prisma.AssetInclude | undefined>({
id,
organizationId,
userOrganizations,
request,
include,
}: Pick<Asset, "id"> & {
organizationId: Asset["organizationId"];
userOrganizations?: Pick<UserOrganization, "organizationId">[];
request?: Request;
include?: T;
}): Promise<AssetWithInclude<T>> {
try {
const otherOrganizationIds = userOrganizations?.map(
(org) => org.organizationId
);

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

/* User is accessing the asset in the wrong organization. In that case we need special 404 handling. */
if (
userOrganizations?.length &&
asset.organizationId !== organizationId &&
otherOrganizationIds?.includes(asset.organizationId)
) {
const redirectTo =
typeof request !== "undefined"
? new URL(request.url).pathname
: undefined;

throw new ShelfError({
cause: null,
rockingrohit9639 marked this conversation as resolved.
Show resolved Hide resolved
title: "Asset not found",
message: "",
additionalData: {
model: "asset",
organization: userOrganizations.find(
(org) => org.organizationId === asset.organizationId
),
redirectTo,
},
label,
status: 404,
});
}

return asset as AssetWithInclude<T>;
} catch (cause) {
throw new ShelfError({
cause,
title: "Asset not found",
message:
"The asset 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,
shouldBeCaptured: !isNotFoundError(cause),
});
Expand Down
Loading
Loading