feat(admin-ui): refresh products when a BatchJob is completed (#4840)

This commit is contained in:
Frane Polić authored and GitHub committed 2023-08-25 07:34:48 +00:00
1 parent f07dc0384f
commit 0c7c2eeeab
12 files changed
+121 -31

No files matched your search

+5
View File
@@ -0,0 +1,5 @@
---
"@medusajs/admin-ui": patch
---
feat(admin-ui): refresh products when import is complete
@@ -1,3 +0,0 @@
Product Variant ID,SKU,Price EUR,Price NA [USD]
,MEDUSA-SWEAT-SMALL,15,13.5
variant_1234,,15,13.5
1 Product Variant ID SKU Price EUR Price NA [USD]
2 MEDUSA-SWEAT-SMALL 15 13.5
3 variant_1234 15 13.5
@@ -48,7 +48,13 @@ function useBatchJob(initialData: BatchJob): BatchJob {
setBatchJob(batch_job) setBatchJob(batch_job)
}, [batch_job]) }, [batch_job])
return useMemo(() => batchJob!, [batchJob?.status, batchJob?.result]) return useMemo(
() =>
new Date(initialData.updated_at) > new Date(batch_job.updated_at)
? initialData
: batchJob,
[initialData.updated_at, batchJob?.updated_at]
)
} }
const BatchJobActivityList = ({ batchJobs }: { batchJobs?: BatchJob[] }) => { const BatchJobActivityList = ({ batchJobs }: { batchJobs?: BatchJob[] }) => {
@@ -171,7 +171,7 @@ type UploadModalProps = {
description1Text: string description1Text: string
description2Title: string description2Title: string
description2Text: string description2Text: string
templateLink: string onDownloadTemplate: () => any
canImport?: boolean canImport?: boolean
progress?: number progress?: number
onClose: () => void onClose: () => void
@@ -195,7 +195,7 @@ function UploadModal(props: UploadModalProps) {
onClose, onClose,
onSubmit, onSubmit,
onFileRemove, onFileRemove,
templateLink, onDownloadTemplate,
summary, summary,
hasError, hasError,
errorMessage, errorMessage,
@@ -216,8 +216,6 @@ function UploadModal(props: UploadModalProps) {
onFileRemove() onFileRemove()
} }
const download = useHref(templateLink)
return ( return (
<Modal open handleClose={onClose}> <Modal open handleClose={onClose}>
<Modal.Body> <Modal.Body>
@@ -275,7 +273,10 @@ function UploadModal(props: UploadModalProps) {
name="medusa-template.csv" name="medusa-template.csv"
size={2967} size={2967}
action={ action={
<a className="h-6 w-6 cursor-pointer" href={download} download> <a
className="h-6 w-6 cursor-pointer"
onClick={onDownloadTemplate}
>
<DownloadIcon stroke="#9CA3AF" /> <DownloadIcon stroke="#9CA3AF" />
</a> </a>
} }
@@ -0,0 +1,16 @@
const PricingImportCSV =
"data:text/csv;charset=utf-8," +
`Product Variant ID,SKU,Price EUR,Price NA [USD]
,MEDUSA-SWEAT-SMALL,15,13.5
variant_1234,,15,13.5
`
export function downloadPricingImportCSVTemplate() {
const encodedUri = encodeURI(PricingImportCSV)
const link = document.createElement("a")
link.setAttribute("href", encodedUri)
link.setAttribute("download", "product-import-template.csv")
document.body.appendChild(link)
link.click()
}
@@ -13,6 +13,7 @@ import {
import UploadModal from "../../../components/organisms/upload-modal" import UploadModal from "../../../components/organisms/upload-modal"
import useNotification from "../../../hooks/use-notification" import useNotification from "../../../hooks/use-notification"
import { usePolling } from "../../../providers/polling-provider" import { usePolling } from "../../../providers/polling-provider"
import { downloadPricingImportCSVTemplate } from "./download-template"
/** /**
* Hook returns a batch job. The endpoint is polled every 2s while the job is processing. * Hook returns a batch job. The endpoint is polled every 2s while the job is processing.
@@ -171,10 +172,6 @@ function ImportPrices(props: ImportPricesProps) {
} }
} }
const templateLink = process.env.ADMIN_PATH
? `${process.env.ADMIN_PATH}/temp/price-list-import-template.csv`
: `/temp/price-list-import-template.csv`
return ( return (
<UploadModal <UploadModal
type="prices" type="prices"
@@ -190,7 +187,7 @@ function ImportPrices(props: ImportPricesProps) {
summary={getSummary()} summary={getSummary()}
onFileRemove={onFileRemove} onFileRemove={onFileRemove}
processUpload={processUpload} processUpload={processUpload}
templateLink={templateLink} onDownloadTemplate={downloadPricingImportCSVTemplate}
/> />
) )
} }
@@ -1,6 +1,18 @@
Product Id;Product Handle;Product Title;Product Subtitle;Product Description;Product Status;Product Thumbnail;Product Weight;Product Length;Product Width;Product Height;Product HS Code;Product Origin Country;Product MID Code;Product Material;Product Collection Title;Product Collection Handle;Product Type;Product Tags;Product Discountable;Product External Id;Product Profile Name;Product Profile Type;Variant Id;Variant Title;Variant SKU;Variant Barcode;Variant Inventory Quantity;Variant Allow Backorder;Variant Manage Inventory;Variant Weight;Variant Length;Variant Width;Variant Height;Variant HS Code;Variant Origin Country;Variant MID Code;Variant Material;Price EUR;Price USD;Option 1 Name;Option 1 Value;Image 1 Url;Image 2 Url const ProductImportCSV =
"data:text/csv;charset=utf-8," +
`Product Id;Product Handle;Product Title;Product Subtitle;Product Description;Product Status;Product Thumbnail;Product Weight;Product Length;Product Width;Product Height;Product HS Code;Product Origin Country;Product MID Code;Product Material;Product Collection Title;Product Collection Handle;Product Type;Product Tags;Product Discountable;Product External Id;Product Profile Name;Product Profile Type;Variant Id;Variant Title;Variant SKU;Variant Barcode;Variant Inventory Quantity;Variant Allow Backorder;Variant Manage Inventory;Variant Weight;Variant Length;Variant Width;Variant Height;Variant HS Code;Variant Origin Country;Variant MID Code;Variant Material;Price EUR;Price USD;Option 1 Name;Option 1 Value;Image 1 Url;Image 2 Url
;coffee-mug-v2;Medusa Coffee Mug;;Every programmer's best friend.;published;https://medusa-public-images.s3.eu-west-1.amazonaws.com/coffee-mug.png;400;;;;;;;;;;;;true;;;;;One Size;;;100;false;true;;;;;;;;;1000;1200;Size;One Size;https://medusa-public-images.s3.eu-west-1.amazonaws.com/coffee-mug.png; ;coffee-mug-v2;Medusa Coffee Mug;;Every programmer's best friend.;published;https://medusa-public-images.s3.eu-west-1.amazonaws.com/coffee-mug.png;400;;;;;;;;;;;;true;;;;;One Size;;;100;false;true;;;;;;;;;1000;1200;Size;One Size;https://medusa-public-images.s3.eu-west-1.amazonaws.com/coffee-mug.png;
;sweatpants-v2;Medusa Sweatpants;;Reimagine the feeling of classic sweatpants. With our cotton sweatpants, everyday essentials no longer have to be ordinary.;published;https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatpants-gray-front.png;400;;;;;;;;;;;;true;;;;;S;;;100;false;true;;;;;;;;;2950;3350;Size;S;https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatpants-gray-front.png;https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatpants-gray-back.png ;sweatpants-v2;Medusa Sweatpants;;Reimagine the feeling of classic sweatpants. With our cotton sweatpants, everyday essentials no longer have to be ordinary.;published;https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatpants-gray-front.png;400;;;;;;;;;;;;true;;;;;S;;;100;false;true;;;;;;;;;2950;3350;Size;S;https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatpants-gray-front.png;https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatpants-gray-back.png
;sweatpants-v2;Medusa Sweatpants;;Reimagine the feeling of classic sweatpants. With our cotton sweatpants, everyday essentials no longer have to be ordinary.;published;https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatpants-gray-front.png;400;;;;;;;;;;;;true;;;;;M;;;100;false;true;;;;;;;;;2950;3350;Size;M;https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatpants-gray-front.png;https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatpants-gray-back.png ;sweatpants-v2;Medusa Sweatpants;;Reimagine the feeling of classic sweatpants. With our cotton sweatpants, everyday essentials no longer have to be ordinary.;published;https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatpants-gray-front.png;400;;;;;;;;;;;;true;;;;;M;;;100;false;true;;;;;;;;;2950;3350;Size;M;https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatpants-gray-front.png;https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatpants-gray-back.png
;sweatpants-v2;Medusa Sweatpants;;Reimagine the feeling of classic sweatpants. With our cotton sweatpants, everyday essentials no longer have to be ordinary.;published;https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatpants-gray-front.png;400;;;;;;;;;;;;true;;;;;L;;;100;false;true;;;;;;;;;2950;3350;Size;L;https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatpants-gray-front.png;https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatpants-gray-back.png ;sweatpants-v2;Medusa Sweatpants;;Reimagine the feeling of classic sweatpants. With our cotton sweatpants, everyday essentials no longer have to be ordinary.;published;https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatpants-gray-front.png;400;;;;;;;;;;;;true;;;;;L;;;100;false;true;;;;;;;;;2950;3350;Size;L;https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatpants-gray-front.png;https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatpants-gray-back.png
;sweatpants-v2;Medusa Sweatpants;;Reimagine the feeling of classic sweatpants. With our cotton sweatpants, everyday essentials no longer have to be ordinary.;published;https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatpants-gray-front.png;400;;;;;;;;;;;;true;;;;;XL;;;100;false;true;;;;;;;;;2950;3350;Size;XL;https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatpants-gray-front.png;https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatpants-gray-back.png ;sweatpants-v2;Medusa Sweatpants;;Reimagine the feeling of classic sweatpants. With our cotton sweatpants, everyday essentials no longer have to be ordinary.;published;https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatpants-gray-front.png;400;;;;;;;;;;;;true;;;;;XL;;;100;false;true;;;;;;;;;2950;3350;Size;XL;https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatpants-gray-front.png;https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatpants-gray-back.png
`
export function downloadProductImportCSVTemplate() {
const encodedUri = encodeURI(ProductImportCSV)
const link = document.createElement("a")
link.setAttribute("href", encodedUri)
link.setAttribute("download", "product-import-template.csv")
document.body.appendChild(link)
link.click()
}
@@ -13,6 +13,7 @@ import {
import UploadModal from "../../../components/organisms/upload-modal" import UploadModal from "../../../components/organisms/upload-modal"
import useNotification from "../../../hooks/use-notification" import useNotification from "../../../hooks/use-notification"
import { usePolling } from "../../../providers/polling-provider" import { usePolling } from "../../../providers/polling-provider"
import { downloadProductImportCSVTemplate } from "./download-template"
/** /**
* Hook returns a batch job. The endpoint is polled every 2s while the job is processing. * Hook returns a batch job. The endpoint is polled every 2s while the job is processing.
@@ -177,10 +178,6 @@ function ImportProducts(props: ImportProductsProps) {
} }
} }
const templateLink = process.env.ADMIN_PATH
? `${process.env.ADMIN_PATH}/temp/product-import-template.csv`
: "/temp/product-import-template.csv"
return ( return (
<UploadModal <UploadModal
type="products" type="products"
@@ -194,7 +191,7 @@ function ImportProducts(props: ImportProductsProps) {
onFileRemove={onFileRemove} onFileRemove={onFileRemove}
processUpload={processUpload} processUpload={processUpload}
fileTitle={"products list"} fileTitle={"products list"}
templateLink={templateLink} onDownloadTemplate={downloadProductImportCSVTemplate}
errorMessage={batchJob?.result?.errors?.join(" \n")} errorMessage={batchJob?.result?.errors?.join(" \n")}
description2Title="Unsure about how to arrange your list?" description2Title="Unsure about how to arrange your list?"
description2Text="Download the template below to ensure you are following the correct format." description2Text="Download the template below to ensure you are following the correct format."
@@ -96,6 +96,7 @@ const Overview = () => {
</Button> </Button>
</div> </div>
) )
default: default:
return ( return (
<div className="flex space-x-2"> <div className="flex space-x-2">
@@ -0,0 +1,42 @@
import React, { PropsWithChildren, useEffect } from "react"
import { adminProductKeys } from "medusa-react"
import { usePolling } from "./polling-provider"
import { queryClient } from "../constants/query-client"
import useNotification from "../hooks/use-notification"
/**
* Provider for refreshing product/pricing lists after batch jobs are complete
*/
export const ImportRefresh = ({ children }: PropsWithChildren) => {
const { batchJobs } = usePolling()
const notification = useNotification()
useEffect(() => {
if (!batchJobs) {
return
}
const productListQuery = Object.entries(
queryClient.getQueryCache().queriesMap
).find(([k, v]) => k.includes("admin_products"))?.[1]
if (productListQuery) {
const refreshedTimestamp = productListQuery.state.dataUpdatedAt
const completedJobs = batchJobs.filter(
(job) => job.status === "completed" && job.type === "product-import"
)
for (const job of completedJobs) {
const jobCompletedTimestamp = new Date(job.completed_at).getTime()
if (jobCompletedTimestamp > refreshedTimestamp) {
queryClient.invalidateQueries(adminProductKeys.all)
notification("Success", "Product import completed", "success")
}
}
}
}, [batchJobs])
return <>{children}</>
}
@@ -97,3 +97,16 @@ export const usePolling = () => {
return context return context
} }
/**
* Return active product import batch job if there is any.
*/
export const useActiveProductImportBatchJob = () => {
const { batchJobs } = usePolling()
return batchJobs?.find(
(job) =>
job.type === "product-import" &&
(job.status === "confirmed" || job.status === "processing")
)
}
@@ -11,6 +11,7 @@ import { PollingProvider } from "./polling-provider"
import { RouteProvider } from "./route-provider" import { RouteProvider } from "./route-provider"
import { SettingProvider } from "./setting-provider" import { SettingProvider } from "./setting-provider"
import { WidgetProvider } from "./widget-provider" import { WidgetProvider } from "./widget-provider"
import { ImportRefresh } from "./import-refresh"
type Props = PropsWithChildren<{ type Props = PropsWithChildren<{
widgetRegistry: WidgetRegistry widgetRegistry: WidgetRegistry
@@ -32,17 +33,19 @@ export const Providers = ({
<MedusaProvider> <MedusaProvider>
<FeatureFlagProvider> <FeatureFlagProvider>
<PollingProvider> <PollingProvider>
<SteppedProvider> <ImportRefresh>
<LayeredModalProvider> <SteppedProvider>
<WidgetProvider registry={widgetRegistry}> <LayeredModalProvider>
<RouteProvider registry={routeRegistry}> <WidgetProvider registry={widgetRegistry}>
<SettingProvider registry={settingRegistry}> <RouteProvider registry={routeRegistry}>
{children} <SettingProvider registry={settingRegistry}>
</SettingProvider> {children}
</RouteProvider> </SettingProvider>
</WidgetProvider> </RouteProvider>
</LayeredModalProvider> </WidgetProvider>
</SteppedProvider> </LayeredModalProvider>
</SteppedProvider>
</ImportRefresh>
</PollingProvider> </PollingProvider>
</FeatureFlagProvider> </FeatureFlagProvider>
</MedusaProvider> </MedusaProvider>