Skip to content

Commit

Permalink
Merge pull request #1625 from rockingrohit9639/fix/imports
Browse files Browse the repository at this point in the history
fix: update validation logic for csv headers
  • Loading branch information
DonKoko authored Feb 4, 2025
2 parents d4c0299 + 785bb18 commit 67d5650
Show file tree
Hide file tree
Showing 8 changed files with 106 additions and 44 deletions.
42 changes: 36 additions & 6 deletions app/components/assets/import-content.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
} from "../shared/modal";
import { WarningBox } from "../shared/warning-box";
import { Table, Td, Th, Tr } from "../table";
import When from "../when/when";

export const ImportBackup = () => (
<>
Expand Down Expand Up @@ -241,10 +242,11 @@ export const FileForm = ({ intent, url }: { intent: string; url?: string }) => {
</>
) : null}
</AlertDialogHeader>
{data?.error ? (

<When truthy={!!data?.error}>
<div>
<h5 className="text-red-500">{data.error.title}</h5>
<p className="text-red-500">{data.error.message}</p>
<h5 className="text-red-500">{data?.error?.title}</h5>
<p className="text-red-500">{data?.error?.message}</p>
{data?.error?.additionalData?.duplicateCodes ? (
<BrokenQrCodesTable
title="Duplicate codes"
Expand Down Expand Up @@ -282,19 +284,47 @@ export const FileForm = ({ intent, url }: { intent: string; url?: string }) => {
/>
) : null}

{Array.isArray(data?.error?.additionalData?.defectedHeaders) ? (
<table className="mt-4 w-full rounded-md border text-left text-sm">
<thead className="bg-error-100 text-xs">
<tr>
<th scope="col" className="px-2 py-1">
Incorrect Header
</th>
<th scope="col" className="px-2 py-1">
Error
</th>
</tr>
</thead>
<tbody>
{data?.error?.additionalData?.defectedHeaders?.map(
(data: {
incorrectHeader: string;
errorMessage: string;
}) => (
<tr key={data.incorrectHeader}>
<td className="px-2 py-1">{data.incorrectHeader}</td>
<td className="px-2 py-1">{data.errorMessage}</td>
</tr>
)
)}
</tbody>
</table>
) : null}

<p className="mt-2">
Please fix your CSV file and try again. If the issue persists,
don't hesitate to get in touch with us.
</p>
</div>
) : null}
</When>

{isSuccessful ? (
<When truthy={isSuccessful}>
<div>
<b className="text-green-500">Success!</b>
<p>Your assets have been imported.</p>
</div>
) : null}
</When>

<AlertDialogFooter>
{isSuccessful ? (
Expand Down
17 changes: 12 additions & 5 deletions app/modules/asset/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,7 @@ import type {
Organization,
} from "@prisma/client";
import type { Return } from "@prisma/client/runtime/library";
import type { z } from "zod";
import type { assetIndexFields } from "./fields";
import type { importAssetsSchema } from "./utils.server";

export interface ICustomFieldValueJson {
raw: string | number | boolean;
Expand Down Expand Up @@ -49,9 +47,18 @@ export interface UpdateAssetPayload {
organizationId: Organization["id"];
}

export type CreateAssetFromContentImportPayload = z.infer<
typeof importAssetsSchema
>;
export interface CreateAssetFromContentImportPayload
extends Record<string, any> {
title: string;
description?: string;
category?: string;
kit?: string;
tags: string[];
location?: string;
custodian?: string;
bookable?: "yes" | "no";
imageUrl?: string; // URL of the image to import
}

export interface CreateAssetFromBackupImportPayload
extends Record<string, any> {
Expand Down
26 changes: 13 additions & 13 deletions app/modules/asset/utils.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -237,16 +237,16 @@ export function formatAssetsRemindersDates({
});
}

export const importAssetsSchema = z
.object({
title: z.string(),
description: z.string().optional(),
category: z.string().optional(),
kit: z.string().optional(),
tags: z.string().array(),
location: z.string().optional(),
custodian: z.string().optional(),
bookable: z.enum(["yes", "no"]).optional().nullable(),
imageUrl: z.string().url().optional(),
})
.and(z.record(z.string(), z.any()));
export const ASSET_CSV_HEADERS = [
"title",
"description",
"category",
"kit",
"tags",
"location",
"custodian",
"bookable",
"imageUrl",
"valuation",
"qrId",
];
1 change: 1 addition & 0 deletions app/modules/invite/utils.server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const IMPORT_USERS_CSV_HEADERS = ["role", "email"];
4 changes: 2 additions & 2 deletions app/routes/_layout+/admin-dashboard+/org.$organizationId.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import HorizontalTabs from "~/components/layout/horizontal-tabs";
import { Button } from "~/components/shared/button";
import { db } from "~/database/db.server";
import { createAssetsFromContentImport } from "~/modules/asset/service.server";
import { importAssetsSchema } from "~/modules/asset/utils.server";
import { ASSET_CSV_HEADERS } from "~/modules/asset/utils.server";
import { toggleOrganizationSso } from "~/modules/organization/service.server";
import { csvDataFromRequest } from "~/utils/csv.server";
import { ShelfError, makeShelfError } from "~/utils/error";
Expand Down Expand Up @@ -169,7 +169,7 @@ export const action = async ({

const contentData = extractCSVDataFromContentImport(
csvData,
importAssetsSchema.array()
ASSET_CSV_HEADERS
);
await createAssetsFromContentImport({
data: contentData,
Expand Down
4 changes: 2 additions & 2 deletions app/routes/_layout+/assets.import.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import {
TabsTrigger,
} from "~/components/shared/tabs";
import { createAssetsFromContentImport } from "~/modules/asset/service.server";
import { importAssetsSchema } from "~/modules/asset/utils.server";
import { ASSET_CSV_HEADERS } from "~/modules/asset/utils.server";
import { appendToMetaTitle } from "~/utils/append-to-meta-title";
import { csvDataFromRequest } from "~/utils/csv.server";
import { ShelfError, makeShelfError } from "~/utils/error";
Expand Down Expand Up @@ -65,7 +65,7 @@ export const action = async ({ context, request }: ActionFunctionArgs) => {

const contentData = extractCSVDataFromContentImport(
csvData,
importAssetsSchema.array()
ASSET_CSV_HEADERS
);

await createAssetsFromContentImport({
Expand Down
4 changes: 2 additions & 2 deletions app/routes/api+/settings.import-users.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { json, type ActionFunctionArgs } from "@remix-run/node";
import { InviteUserFormSchema } from "~/components/settings/invite-user-dialog";
import { bulkInviteUsers } from "~/modules/invite/service.server";
import { IMPORT_USERS_CSV_HEADERS } from "~/modules/invite/utils.server";
import { csvDataFromRequest } from "~/utils/csv.server";
import { makeShelfError, ShelfError } from "~/utils/error";
import { assertIsPost, data, error } from "~/utils/http.server";
Expand Down Expand Up @@ -43,7 +43,7 @@ export async function action({ context, request }: ActionFunctionArgs) {

const users = extractCSVDataFromContentImport(
csvData,
InviteUserFormSchema.array()
IMPORT_USERS_CSV_HEADERS
);

const response = await bulkInviteUsers({
Expand Down
52 changes: 38 additions & 14 deletions app/utils/import.server.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import type { ZodSchema } from "zod";
import type { CreateAssetFromContentImportPayload } from "~/modules/asset/types";
import { ShelfError } from "./error";

Expand Down Expand Up @@ -27,46 +26,71 @@ export function getUniqueValuesFromArrayOfObjects({
}

/** Takes the CSV data from a `content` import and parses it into an object that we can then use to create the entries */
export function extractCSVDataFromContentImport<Schema extends ZodSchema>(
export function extractCSVDataFromContentImport(
data: string[][],
schema: Schema
csvHeaders: string[]
) {
/**
* The first row of the CSV contains the keys for the data
* We need to trim the keys to remove any whitespace and special characters and Non-printable characters as it already causes issues with in the past
* Non-printable character: The non-printable character you encountered at the beginning of the title property key ('\ufeff') is known as the Unicode BOM (Byte Order Mark).
*/
const keys = data[0].map((key) => key.trim()); // Trim the keys
const headers = data[0].map((key) => key.trim()); // Trim the keys
const values = data.slice(1) as string[][];
const rawData = values.map((entry) =>

const csvData = values.map((entry) =>
Object.fromEntries(
entry.map((value, index) => {
switch (keys[index]) {
switch (headers[index]) {
case "tags":
return [keys[index], value.split(",").map((tag) => tag.trim())];
return [headers[index], value.split(",").map((tag) => tag.trim())];
case "imageUrl":
// Return empty string if URL is empty/undefined, otherwise trim
return [keys[index], value?.trim() || ""];
return [headers[index], value?.trim() || ""];
case "bookable":
return [keys[index], !value ? null : value];
return [headers[index], !value ? null : value];
default:
return [keys[index], value];
return [headers[index], value];
}
})
)
);

const parsedResult = schema.safeParse(rawData);
if (!parsedResult.success) {
/* Validating headers in csv file */
const defectedHeaders: Array<{
incorrectHeader: string;
errorMessage: string;
}> = [];

headers.forEach((header) => {
if (header.startsWith("cf:")) {
if (header.length < 4) {
defectedHeaders.push({
incorrectHeader: header,
errorMessage: "Custom field header name is not provided.",
});
}

return;
} else if (!csvHeaders.includes(header)) {
defectedHeaders.push({
incorrectHeader: header,
errorMessage: "Invalid header provided.",
});
}
});

if (defectedHeaders.length > 0) {
throw new ShelfError({
cause: null,
message:
"Received invalid data, please update the file with proper headers and data.",
"Invalid headers in csv file. Please fix the headers and try again.",
additionalData: { defectedHeaders },
label: "Assets",
});
}

return parsedResult.data as Schema["_output"];
return csvData;
}

/** Takes the CSV data from a `backup` import and parses it into an object that we can then use to create the entries */
Expand Down

0 comments on commit 67d5650

Please sign in to comment.