From cb0703dcc86ec0a63b674304d226d2cbad0b6aea Mon Sep 17 00:00:00 2001 From: Rohit Kumar Saini Date: Tue, 14 Jan 2025 11:09:11 +0100 Subject: [PATCH 1/5] feat(qr-id-column): create qrId column with qr preview --- .../assets-index/advanced-asset-columns.tsx | 14 ++- app/components/assets/qr-preview-dialog.tsx | 91 +++++++++++++++++++ app/components/qr/qr-preview.tsx | 60 ++++++------ app/hooks/use-api-query.ts | 47 ++++++++++ .../api+/assets.$assetId.generate-qr-obj.ts | 39 ++++++++ 5 files changed, 223 insertions(+), 28 deletions(-) create mode 100644 app/components/assets/qr-preview-dialog.tsx create mode 100644 app/hooks/use-api-query.ts create mode 100644 app/routes/api+/assets.$assetId.generate-qr-obj.ts diff --git a/app/components/assets/assets-index/advanced-asset-columns.tsx b/app/components/assets/assets-index/advanced-asset-columns.tsx index 95fe1245d..829daaf89 100644 --- a/app/components/assets/assets-index/advanced-asset-columns.tsx +++ b/app/components/assets/assets-index/advanced-asset-columns.tsx @@ -50,6 +50,7 @@ import { resolveTeamMemberName } from "~/utils/user"; import { freezeColumnClassNames } from "./freeze-column-classes"; import { AssetImage } from "../asset-image"; import { AssetStatusBadge } from "../asset-status-badge"; +import QrPreviewDialog from "../qr-preview-dialog"; export function AdvancedIndexColumn({ column, @@ -157,9 +158,20 @@ export function AdvancedIndexColumn({ ); case "id": - case "qrId": return ; + case "qrId": + return ( + + + + } + /> + ); + case "status": return ; diff --git a/app/components/assets/qr-preview-dialog.tsx b/app/components/assets/qr-preview-dialog.tsx new file mode 100644 index 000000000..b882a989e --- /dev/null +++ b/app/components/assets/qr-preview-dialog.tsx @@ -0,0 +1,91 @@ +import { cloneElement, useState } from "react"; +import type { Asset } from "@prisma/client"; +import useApiQuery from "~/hooks/use-api-query"; +import { tw } from "~/utils/tw"; +import { Dialog, DialogPortal } from "../layout/dialog"; +import { QrPreview } from "../qr/qr-preview"; +import { Button } from "../shared/button"; +import When from "../when/when"; + +type QrPreviewDialogProps = { + className?: string; + asset: Pick & { + qrId: string; + }; + trigger: React.ReactElement<{ onClick: () => void }>; +}; + +export default function QrPreviewDialog({ + className, + asset, + trigger, +}: QrPreviewDialogProps) { + const [isDialogOpen, setIsDialogOpen] = useState(false); + + const { isLoading, data, error } = useApiQuery<{ + qrObj: React.ComponentProps["qrObj"]; + }>({ + api: `/api/assets/${asset.id}/generate-qr-obj`, + enabled: isDialogOpen, + }); + + function openDialog() { + setIsDialogOpen(true); + } + + function closeDialog() { + setIsDialogOpen(false); + } + + return ( + <> + {cloneElement(trigger, { onClick: openDialog })} + + + +
+
+ +
+
+

Fetching qr code...

+
+
+
+ +

{error}

+
+ + + +
+
+ +
+
+
+
+ + ); +} diff --git a/app/components/qr/qr-preview.tsx b/app/components/qr/qr-preview.tsx index 466c9e699..1f7c04d7f 100644 --- a/app/components/qr/qr-preview.tsx +++ b/app/components/qr/qr-preview.tsx @@ -4,9 +4,12 @@ import domtoimage from "dom-to-image"; import { useReactToPrint } from "react-to-print"; import { Button } from "~/components/shared/button"; import { slugify } from "~/utils/slugify"; +import { tw } from "~/utils/tw"; + type SizeKeys = "cable" | "small" | "medium" | "large"; interface ObjectType { + className?: string; item: { name: string; type: "asset" | "kit"; @@ -20,7 +23,7 @@ interface ObjectType { }; } -export const QrPreview = ({ qrObj, item }: ObjectType) => { +export const QrPreview = ({ className, qrObj, item }: ObjectType) => { const captureDivRef = useRef(null); const downloadQrBtnRef = useRef(null); @@ -67,32 +70,35 @@ export const QrPreview = ({ qrObj, item }: ObjectType) => { content: () => captureDivRef.current, }); return ( -
-
-
- -
-
- - -
+
+
+ +
+
+ +
); diff --git a/app/hooks/use-api-query.ts b/app/hooks/use-api-query.ts new file mode 100644 index 000000000..ef4f10a64 --- /dev/null +++ b/app/hooks/use-api-query.ts @@ -0,0 +1,47 @@ +import { useEffect, useState } from "react"; + +/** + * A simple hook which calls any of our API + * + */ +type UseApiQueryParams = { + /** Any API endpoint */ + api: string; + /** Query will not execute until this is true */ + enabled?: boolean; +}; + +export default function useApiQuery({ + api, + enabled = true, +}: UseApiQueryParams) { + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(); + const [data, setData] = useState(); + + useEffect( + function handleQuery() { + if (enabled) { + setIsLoading(true); + fetch(api) + .then((response) => response.json()) + .then((data: TData) => { + setData(data); + }) + .catch((error: Error) => { + setError(error?.message ?? "Something went wrong."); + }) + .finally(() => { + setIsLoading(false); + }); + } + }, + [api, enabled] + ); + + return { + isLoading, + error, + data, + }; +} diff --git a/app/routes/api+/assets.$assetId.generate-qr-obj.ts b/app/routes/api+/assets.$assetId.generate-qr-obj.ts new file mode 100644 index 000000000..9071aee65 --- /dev/null +++ b/app/routes/api+/assets.$assetId.generate-qr-obj.ts @@ -0,0 +1,39 @@ +import { json, type LoaderFunctionArgs } from "@remix-run/node"; +import { z } from "zod"; +import { generateQrObj } from "~/modules/qr/utils.server"; +import { makeShelfError } from "~/utils/error"; +import { data, error, getParams } from "~/utils/http.server"; +import { + PermissionAction, + PermissionEntity, +} from "~/utils/permissions/permission.data"; +import { requirePermission } from "~/utils/roles.server"; + +export async function loader({ context, params, request }: LoaderFunctionArgs) { + const authSession = context.getSession(); + const { userId } = authSession; + + const { assetId } = getParams(params, z.object({ assetId: z.string() }), { + additionalData: { userId, ...params }, + }); + + try { + const { organizationId } = await requirePermission({ + userId, + request, + entity: PermissionEntity.qr, + action: PermissionAction.read, + }); + + const qrObj = await generateQrObj({ + assetId, + userId, + organizationId, + }); + + return json(data({ qrObj })); + } catch (cause) { + const reason = makeShelfError(cause, { userId, assetId }); + return json(error(reason), { status: reason.status }); + } +} From fe50de6f417f8c0732cf88a9337258de782a3609 Mon Sep 17 00:00:00 2001 From: Rohit Kumar Saini Date: Tue, 14 Jan 2025 13:41:48 +0100 Subject: [PATCH 2/5] fix(cancel-button): update cancel button behavior on asset edit page --- app/components/assets/form.tsx | 18 ++++++++++++++---- app/routes/_layout+/assets.$assetId_.edit.tsx | 4 ++-- app/routes/_layout+/assets.new.tsx | 5 +++-- 3 files changed, 19 insertions(+), 8 deletions(-) diff --git a/app/components/assets/form.tsx b/app/components/assets/form.tsx index 6f2f4aae9..78cc07d13 100644 --- a/app/components/assets/form.tsx +++ b/app/components/assets/form.tsx @@ -145,7 +145,7 @@ export const AssetForm = ({ encType="multipart/form-data" > - + {qrId ? ( @@ -157,7 +157,7 @@ export const AssetForm = ({

Basic information about your asset.

- +
@@ -420,10 +420,20 @@ export const AssetForm = ({ ); }; -const Actions = ({ disabled }: { disabled: boolean }) => ( +const Actions = ({ + assetId, + disabled, +}: { + assetId?: string; + disabled: boolean; +}) => ( <> - diff --git a/app/routes/_layout+/assets.$assetId_.edit.tsx b/app/routes/_layout+/assets.$assetId_.edit.tsx index de6d7a1af..e9330f786 100644 --- a/app/routes/_layout+/assets.$assetId_.edit.tsx +++ b/app/routes/_layout+/assets.$assetId_.edit.tsx @@ -227,7 +227,7 @@ export default function AssetEditPage() { ); return ( - <> +
- +
); } diff --git a/app/routes/_layout+/assets.new.tsx b/app/routes/_layout+/assets.new.tsx index cdcc5e038..31d7d6df5 100644 --- a/app/routes/_layout+/assets.new.tsx +++ b/app/routes/_layout+/assets.new.tsx @@ -227,12 +227,13 @@ export default function NewAssetPage() { // Get category from URL params or use the default passed prop const categoryFromUrl = searchParams.get("category"); + return ( - <> +
- +
); } From c9932bca864aff388cded292c2d0d3abc5f90c55 Mon Sep 17 00:00:00 2001 From: Rohit Kumar Saini Date: Tue, 14 Jan 2025 14:15:41 +0100 Subject: [PATCH 3/5] fix(cancel-button): update Actions component --- app/components/assets/form.tsx | 49 +++++++++++++++++----------------- 1 file changed, 24 insertions(+), 25 deletions(-) diff --git a/app/components/assets/form.tsx b/app/components/assets/form.tsx index 78cc07d13..a492b4ade 100644 --- a/app/components/assets/form.tsx +++ b/app/components/assets/form.tsx @@ -4,6 +4,7 @@ import { useActionData, useLoaderData, useNavigation, + useParams, } from "@remix-run/react"; import { useAtom, useAtomValue } from "jotai"; import type { Tag } from "react-tag-autocomplete"; @@ -145,7 +146,7 @@ export const AssetForm = ({ encType="multipart/form-data" > - + {qrId ? ( @@ -157,7 +158,7 @@ export const AssetForm = ({

Basic information about your asset.

- +
@@ -420,30 +421,28 @@ export const AssetForm = ({ ); }; -const Actions = ({ - assetId, - disabled, -}: { - assetId?: string; - disabled: boolean; -}) => ( - <> - - - - +const Actions = ({ disabled }: { disabled: boolean }) => { + const { assetId } = useParams<{ assetId?: string }>(); - - -); + return ( + <> + + + + + + + + ); +}; const AddAnother = ({ disabled }: { disabled: boolean }) => ( From b792de28375e49347bf45d6591467dbafdf32318 Mon Sep 17 00:00:00 2001 From: Donkoko Date: Tue, 14 Jan 2025 16:32:42 +0200 Subject: [PATCH 4/5] small adjustments --- .../assets/assets-index/advanced-asset-columns.tsx | 11 +++++++---- app/components/layout/header/types.ts | 1 + app/components/shared/button.tsx | 3 +++ 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/app/components/assets/assets-index/advanced-asset-columns.tsx b/app/components/assets/assets-index/advanced-asset-columns.tsx index 829daaf89..63869258c 100644 --- a/app/components/assets/assets-index/advanced-asset-columns.tsx +++ b/app/components/assets/assets-index/advanced-asset-columns.tsx @@ -166,7 +166,9 @@ export function AdvancedIndexColumn({ asset={item} trigger={ - + } /> @@ -201,13 +203,14 @@ export function AdvancedIndexColumn({ {item.location.name} - + ) : ( "" ) diff --git a/app/components/layout/header/types.ts b/app/components/layout/header/types.ts index 88291cdeb..3a4af077b 100644 --- a/app/components/layout/header/types.ts +++ b/app/components/layout/header/types.ts @@ -50,6 +50,7 @@ export type ButtonVariant = | "secondary" | "tertiary" | "link" + | "link-gray" | "block-link" | "block-link-gray" | "danger"; diff --git a/app/components/shared/button.tsx b/app/components/shared/button.tsx index 9c3d23e16..a8b663b24 100644 --- a/app/components/shared/button.tsx +++ b/app/components/shared/button.tsx @@ -109,6 +109,9 @@ const variants: Record = { link: tw( `border-none p-0 text-text-sm font-semibold text-primary-700 hover:text-primary-800` ), + "link-gray": tw( + "text-gray border-none p-0 text-text-sm font-normal underline hover:text-gray-500 " + ), "block-link": tw( "-mt-1 border-none px-2 py-1 text-[14px] font-normal hover:bg-primary-50 hover:text-primary-600" ), From 91337111a2f23d7fb6db62ce249bdab920b18c4f Mon Sep 17 00:00:00 2001 From: Donkoko Date: Tue, 14 Jan 2025 16:37:24 +0200 Subject: [PATCH 5/5] mend --- app/components/assets/assets-index/advanced-asset-columns.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/app/components/assets/assets-index/advanced-asset-columns.tsx b/app/components/assets/assets-index/advanced-asset-columns.tsx index 63869258c..dbe21ae04 100644 --- a/app/components/assets/assets-index/advanced-asset-columns.tsx +++ b/app/components/assets/assets-index/advanced-asset-columns.tsx @@ -166,9 +166,7 @@ export function AdvancedIndexColumn({ asset={item} trigger={ - + } />