From 0fbf38e176ac3d391dc43cf8b1d54a39e6366bf6 Mon Sep 17 00:00:00 2001 From: Nikolay Bonev Date: Thu, 19 Sep 2024 14:32:50 +0300 Subject: [PATCH 1/2] feat: allow for importing with setting qrId --- app/components/assets/import-content.tsx | 69 +++++++++++++ app/modules/asset/service.server.ts | 21 +++- app/modules/qr/service.server.ts | 126 +++++++++++++++++++++++ 3 files changed, 212 insertions(+), 4 deletions(-) diff --git a/app/components/assets/import-content.tsx b/app/components/assets/import-content.tsx index f6311a69e..de31a7b71 100644 --- a/app/components/assets/import-content.tsx +++ b/app/components/assets/import-content.tsx @@ -1,6 +1,7 @@ import type { ChangeEvent } from "react"; import { useRef, useState } from "react"; import { useFetcher } from "@remix-run/react"; +import type { QRCodePerImportedAsset } from "~/modules/qr/service.server"; import type { action } from "~/routes/_layout+/assets.import"; import { isFormProcessing } from "~/utils/form"; import { tw } from "~/utils/tw"; @@ -18,6 +19,7 @@ import { AlertDialogTrigger, } from "../shared/modal"; import { WarningBox } from "../shared/warning-box"; +import { Table, Td, Th, Tr } from "../table"; export const ImportBackup = () => ( <> @@ -212,6 +214,43 @@ export const FileForm = ({ intent, url }: { intent: string; url?: string }) => {
{data.error.title}

{data.error.message}

+ {data?.error?.additionalData?.duplicateCodes ? ( + + ) : null} + {data?.error?.additionalData?.nonExistentCodes ? ( + + ) : null} + {data?.error?.additionalData?.linkedCodes ? ( + + ) : null} + {data?.error?.additionalData?.connectedToOtherOrgs ? ( + + ) : null} +

Please fix your CSV file and try again. If the issue persists, don't hesitate to get in touch with us. @@ -261,3 +300,33 @@ export const FileForm = ({ intent, url }: { intent: string; url?: string }) => { ); }; + +function BrokenQrCodesTable({ + title, + data, +}: { + title: string; + data: QRCodePerImportedAsset[]; +}) { + return ( +

+
{title}
+ + + + + + + + + {data.map((code: { title: string; qrId: string }) => ( + + + + + ))} + +
Asset titleQR ID
{code.title}{code.qrId}
+
+ ); +} diff --git a/app/modules/asset/service.server.ts b/app/modules/asset/service.server.ts index da7b22b0f..a0ec35c50 100644 --- a/app/modules/asset/service.server.ts +++ b/app/modules/asset/service.server.ts @@ -28,7 +28,7 @@ import { } from "~/modules/custom-field/service.server"; import type { CustomFieldDraftPayload } from "~/modules/custom-field/types"; import { createLocationsIfNotExists } from "~/modules/location/service.server"; -import { getQr } from "~/modules/qr/service.server"; +import { getQr, parseQrCodesFromImportData } from "~/modules/qr/service.server"; import { createTagsIfNotExists } from "~/modules/tag/service.server"; import { createTeamMemberIfNotExists, @@ -740,10 +740,11 @@ export async function createAsset({ * 2. If the qr code belongs to the current organization * 3. If the qr code is not linked to an asset or a kit */ + const qr = qrId ? await getQr({ id: qrId }) : null; const qrCodes = qr && - qr.organizationId === organizationId && + (qr.organizationId === organizationId || !qr.organizationId) && qr.assetId === null && qr.kitId === null ? { connect: { id: qrId } } @@ -1737,6 +1738,12 @@ export async function createAssetsFromContentImport({ organizationId: Organization["id"]; }) { try { + const qrCodesPerAsset = await parseQrCodesFromImportData({ + data, + organizationId, + userId, + }); + const kits = await createKitsIfNotExists({ data, userId, @@ -1791,6 +1798,7 @@ export async function createAssetsFromContentImport({ }, [] as ShelfAssetCustomFieldValueType[]); await createAsset({ + qrId: qrCodesPerAsset.find((item) => item?.title === asset.title)?.qrId, organizationId, title: asset.title, description: asset.description || "", @@ -1812,12 +1820,17 @@ export async function createAssetsFromContentImport({ }); } } catch (cause) { + const isShelfError = isLikeShelfError(cause); throw new ShelfError({ cause, - message: isLikeShelfError(cause) + message: isShelfError ? cause?.message : "Something went wrong while creating assets from content import", - additionalData: { userId, organizationId }, + additionalData: { + userId, + organizationId, + ...(isShelfError && cause.additionalData), + }, label, }); } diff --git a/app/modules/qr/service.server.ts b/app/modules/qr/service.server.ts index 2e97c5c92..0665fcd13 100644 --- a/app/modules/qr/service.server.ts +++ b/app/modules/qr/service.server.ts @@ -17,6 +17,7 @@ import { id } from "~/utils/id/id.server"; import { getParamsValues } from "~/utils/list"; // eslint-disable-next-line import/no-cycle import { generateCode } from "./utils.server"; +import type { CreateAssetFromContentImportPayload } from "../asset/types"; import { generateRandomCode } from "../invite/helpers"; const label: ErrorLabel = "QR"; @@ -486,3 +487,128 @@ export async function getQrCodeMaps({ } return finalMap; } + +/** Extracts qrCodes from data and checks their validity for import + * You can only import unclaimed or unlinked codes + * - For non-existing codes - we can allow them to be imported + * - For linked codes - we don't allow any imports + * - For unlinked - we can only allowed if the code is already claimed within the current workspace the user is trying to import to + * - For unclaimed - there are not really any limitations we need to place. This should work directly + */ + +export type QRCodePerImportedAsset = { + title: string; + qrId: string; +}; + +export async function parseQrCodesFromImportData({ + data, + userId, + organizationId, +}: { + data: CreateAssetFromContentImportPayload[]; + userId: User["id"]; + organizationId: Organization["id"]; +}) { + try { + const qrCodePerAsset = data + .map((asset) => { + if (asset.qrId) { + return { + title: asset.title, + qrId: asset.qrId, + }; + } + return null; + }) + .filter((asset) => asset !== null); // Filter out null values + + const codes = await db.qr.findMany({ + where: { + id: { + in: qrCodePerAsset.map((asset) => asset?.qrId), + }, + }, + }); + + /** Check for any codes that are present more than 1 time in the data */ + const duplicateCodes = qrCodePerAsset.filter( + (asset, index, self) => + self.findIndex((t) => t?.qrId === asset?.qrId) !== index + ); + + if (duplicateCodes.length) { + throw new ShelfError({ + cause: null, + message: + "Some of the QR codes you are trying to import are present more than once in the data. Please make sure each QR code is only present once.", + additionalData: { duplicateCodes }, + label, + }); + } + + /** Check if any of the codes are non-existent */ + const nonExistentCodes = qrCodePerAsset.filter( + (asset) => !codes.find((code) => code.id === asset?.qrId) && asset?.qrId + ); + + if (nonExistentCodes.length) { + throw new ShelfError({ + cause: null, + message: "Some of the QR codes you are trying to import do not exist", + additionalData: { nonExistentCodes }, + label, + }); + } + + /** Check for codes already linked to asset or kit. Returns QRCodePerImportedAsset[] */ + const linkedCodes = qrCodePerAsset.filter((asset) => + codes.find( + (code) => code.id === asset?.qrId && (code.assetId || code.kitId) + ) + ); + if (linkedCodes.length) { + throw new ShelfError({ + cause: null, + message: + "Some of the QR codes you are trying to import are already linked to an asset or a kit. Please use unlinkned or unclaimed codes for your import.", + additionalData: { linkedCodes }, + label, + }); + } + + /** Check for codes linked to other any organization and the organization is different than the current one */ + const connectedToOtherOrgs = qrCodePerAsset.filter((asset) => + codes.find( + (code) => + code.id === asset?.qrId && + code.organizationId && + code.organizationId !== organizationId + ) + ); + if (connectedToOtherOrgs.length) { + throw new ShelfError({ + cause: null, + message: + "Some of the QR codes you are trying to import don't belong to your current organization. You can only import codes that are unclaimed, unlinked or linked to your organization.", + additionalData: { connectedToOtherOrgs }, + label, + }); + } + + return qrCodePerAsset; + } catch (cause) { + const isShelfError = isLikeShelfError(cause); + throw new ShelfError({ + cause, + message: isShelfError ? cause.message : "Failed to get qr codes", + additionalData: { + data, + userId, + organizationId, + ...(isShelfError && cause.additionalData), + }, + label, + }); + } +} From 6ce6b3f58abfd1b0e5e99b42f3c880e7a6d34ee5 Mon Sep 17 00:00:00 2001 From: Nikolay Bonev Date: Thu, 19 Sep 2024 14:46:30 +0300 Subject: [PATCH 2/2] updated template and added explanation on how importing with qrId works --- app/components/assets/import-content.tsx | 29 +++++++++++++++++++ ...f.nu-example-asset-import-from-content.csv | 6 ++-- 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/app/components/assets/import-content.tsx b/app/components/assets/import-content.tsx index de31a7b71..4bb6964c9 100644 --- a/app/components/assets/import-content.tsx +++ b/app/components/assets/import-content.tsx @@ -112,6 +112,35 @@ export const ImportContent = () => ( "cf:purchase date, type:date"
+

Importing with QR codes

+
+ You also have the option to se a Shelf QR code for each asset. This is + very valuable if you already have Shelf QR codes printed and you want to + link them to the assets you are importing. +
+ This feature comes with the following limitations: + + If no "qrId" is used a new QR code will be generated. +
+ If you are interesting in receiving some unclaimed or unlinked codes, feel + free to get in touch with support and we can provide those for you. +
+

Extra considerations