Skip to content

Commit

Permalink
feat: Add support for product imports (#8298)
Browse files Browse the repository at this point in the history
* feat: Add support for product imports

* fix: Add product import template to import UI
  • Loading branch information
sradevski authored Jul 30, 2024
1 parent a9fea98 commit 1066499
Show file tree
Hide file tree
Showing 14 changed files with 482 additions and 39 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { EllipsisHorizontal } from "@medusajs/icons"
import { ReactNode } from "react"
import { Link } from "react-router-dom"

type Action = {
export type Action = {
icon: ReactNode
label: string
disabled?: boolean
Expand All @@ -19,7 +19,7 @@ type Action = {
}
)

type ActionGroup = {
export type ActionGroup = {
actions: Action[]
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import { ArrowDownTray, Spinner } from "@medusajs/icons"
import { IconButton, Text } from "@medusajs/ui"
import { ActionGroup, ActionMenu } from "../action-menu"

export const FilePreview = ({
filename,
url,
loading,
activity,
actions,
hideThumbnail,
}: {
filename: string
url?: string
loading?: boolean
activity?: string
actions?: ActionGroup[]
hideThumbnail?: boolean
}) => {
return (
<div className="shadow-elevation-card-rest bg-ui-bg-component transition-fg rounded-md px-3 py-2">
<div className="flex flex-row items-center justify-between gap-2">
<div className="flex flex-row items-center gap-3">
{!hideThumbnail && <FileThumbnail />}
<div className="flex flex-col justify-center">
<Text
size="small"
leading="compact"
className="truncate max-w-[260px]"
>
{filename}
</Text>

{loading && !!activity && (
<Text
leading="compact"
size="xsmall"
className="text-ui-fg-interactive"
>
{activity}
</Text>
)}
</div>
</div>

{loading && <Spinner className="animate-spin" />}
{!loading && actions && <ActionMenu groups={actions} />}
{!loading && url && (
<IconButton variant="transparent" asChild>
<a href={url} download={filename ?? `${Date.now()}`}>
<ArrowDownTray />
</a>
</IconButton>
)}
</div>
</div>
)
}

const FileThumbnail = () => {
return (
<svg
width="24"
height="32"
viewBox="0 0 24 32"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M20 31.75H4C1.92893 31.75 0.25 30.0711 0.25 28V4C0.25 1.92893 1.92893 0.25 4 0.25H15.9431C16.9377 0.25 17.8915 0.645088 18.5948 1.34835L22.6516 5.4052C23.3549 6.10847 23.75 7.06229 23.75 8.05685V28C23.75 30.0711 22.0711 31.75 20 31.75Z"
fill="url(#paint0_linear_6594_388107)"
stroke="url(#paint1_linear_6594_388107)"
stroke-width="0.5"
/>
<path
opacity="0.4"
d="M17.7857 12.8125V13.5357H10.3393V10.9643H15.9375C16.9569 10.9643 17.7857 11.7931 17.7857 12.8125ZM6.21429 16.9107V15.0893H8.78571V16.9107H6.21429ZM10.3393 16.9107V15.0893H17.7857V16.9107H10.3393ZM15.9375 21.0357H10.3393V18.4643H17.7857V19.1875C17.7857 20.2069 16.9569 21.0357 15.9375 21.0357ZM6.21429 19.1875V18.4643H8.78571V21.0357H8.0625C7.0431 21.0357 6.21429 20.2069 6.21429 19.1875ZM8.0625 10.9643H8.78571V13.5357H6.21429V12.8125C6.21429 11.7931 7.0431 10.9643 8.0625 10.9643Z"
fill="url(#paint2_linear_6594_388107)"
stroke="url(#paint3_linear_6594_388107)"
stroke-width="0.428571"
/>
<defs>
<linearGradient
id="paint0_linear_6594_388107"
x1="12"
y1="0"
x2="12"
y2="32"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="#F4F4F5" />
<stop offset="1" stop-color="#E4E4E7" />
</linearGradient>
<linearGradient
id="paint1_linear_6594_388107"
x1="12"
y1="0"
x2="12"
y2="32"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="#E4E4E7" />
<stop offset="1" stop-color="#D4D4D8" />
</linearGradient>
<linearGradient
id="paint2_linear_6594_388107"
x1="12"
y1="10.75"
x2="12"
y2="21.25"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="#52525B" />
<stop offset="1" stop-color="#A1A1AA" />
</linearGradient>
<linearGradient
id="paint3_linear_6594_388107"
x1="12"
y1="10.75"
x2="12"
y2="21.25"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="#18181B" />
<stop offset="1" stop-color="#52525B" />
</linearGradient>
</defs>
</svg>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./file-preview"
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export interface FileType {

export interface FileUploadProps {
label: string
multiple?: boolean
hint?: string
hasError?: boolean
formats: string[]
Expand All @@ -19,6 +20,7 @@ export interface FileUploadProps {
export const FileUpload = ({
label,
hint,
multiple = true,
hasError,
formats,
onUploaded,
Expand Down Expand Up @@ -130,7 +132,7 @@ export const FileUpload = ({
onChange={handleFileChange}
type="file"
accept={formats.join(",")}
multiple
multiple={multiple}
/>
</div>
)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import {
ArrowDownTray,
BellAlert,
BellAlertDone,
InformationCircleSolid,
Expand All @@ -13,6 +12,7 @@ import { InfiniteList } from "../../common/infinite-list"
import { sdk } from "../../../lib/client"
import { notificationQueryKeys } from "../../../hooks/api"
import { TFunction } from "i18next"
import { FilePreview } from "../../common/file-preview"

interface NotificationData {
title: string
Expand Down Expand Up @@ -132,34 +132,19 @@ const Notification = ({
</Text>
)}
</div>
<NotificationFile file={data.file} />
{!!data?.file?.url && (
<FilePreview
filename={data.file.filename ?? ""}
url={data.file.url}
hideThumbnail
/>
)}
</div>
</div>
</>
)
}

const NotificationFile = ({ file }: { file: NotificationData["file"] }) => {
if (!file?.url) {
return null
}

return (
<div className="shadow-elevation-card-rest bg-ui-bg-component transition-fg rounded-md px-3 py-2">
<div className="flex w-full flex-row items-center justify-between gap-2">
<Text size="small" leading="compact">
{file?.filename ?? file.url}
</Text>
<IconButton variant="transparent" asChild>
<a href={file.url} download={file.filename ?? `${Date.now()}`}>
<ArrowDownTray />
</a>
</IconButton>
</div>
</div>
)
}

const NotificationsEmptyState = ({ t }: { t: TFunction }) => {
return (
<div className="flex h-full flex-col items-center justify-center">
Expand Down
28 changes: 28 additions & 0 deletions packages/admin-next/dashboard/src/hooks/api/products.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -343,3 +343,31 @@ export const useExportProducts = (
...options,
})
}

export const useImportProducts = (
options?: UseMutationOptions<
HttpTypes.AdminImportProductResponse,
FetchError,
HttpTypes.AdminImportProductRequest
>
) => {
return useMutation({
mutationFn: (payload) => sdk.admin.product.import(payload),
onSuccess: (data, variables, context) => {
options?.onSuccess?.(data, variables, context)
},
...options,
})
}

export const useConfirmImportProducts = (
options?: UseMutationOptions<{}, FetchError, string>
) => {
return useMutation({
mutationFn: (payload) => sdk.admin.product.confirmImport(payload),
onSuccess: (data, variables, context) => {
options?.onSuccess?.(data, variables, context)
},
...options,
})
}
17 changes: 16 additions & 1 deletion packages/admin-next/dashboard/src/i18n/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -350,7 +350,22 @@
},
"import": {
"header": "Import Product List",
"description": "Import products by providing a CSV file in a pre-defined format"
"description": "Import products by providing a CSV file in a pre-defined format",
"template": {
"title": "Unsure about how to arrange your list?",
"description": "Download the template below to ensure you are following the correct format."
},
"upload": {
"title": "Upload a CSV file",
"description": "Through imports you can add or update products. To update existing products you must use the existing handle and ID, to update existing variants you must use the existing ID. You will be asked for confirmation before we import products.",
"preprocessing": "Preprocessing...",
"productsToCreate": "Products will be created",
"productsToUpdate": "Products will be updated"
},
"success": {
"title": "We are processing your import",
"description": "Importing data may take a while. We will notify you when we are done."
}
},
"deleteWarning": "You are about to delete the product {{title}}. This action cannot be undone.",
"variants": "Variants",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { HttpTypes } from "@medusajs/types"
import { Divider } from "../../../../components/common/divider"
import { Text } from "@medusajs/ui"
import { useTranslation } from "react-i18next"

export const ImportSummary = ({
summary,
}: {
summary: HttpTypes.AdminImportProductResponse["summary"]
}) => {
const { t } = useTranslation()

return (
<div className="shadow-elevation-card-rest bg-ui-bg-component transition-fg rounded-md flex flex-row px-3 py-2">
<Stat
title={summary.toCreate.toLocaleString()}
description={t("products.import.upload.productsToCreate")}
/>
<Divider orientation="vertical" className="px-3 h-10" />
<Stat
title={summary.toUpdate.toLocaleString()}
description={t("products.import.upload.productsToUpdate")}
/>
</div>
)
}

const Stat = ({
title,
description,
}: {
title: string
description: string
}) => {
return (
<div className="flex-1 flex flex-col justify-center">
<Text size="xlarge" className="font-sans font-medium">
{title}
</Text>

<Text
leading="compact"
size="xsmall"
weight="plus"
className="text-ui-fg-subtle"
>
{description}
</Text>
</div>
)
}
Loading

0 comments on commit 1066499

Please sign in to comment.