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

This commit is contained in:
Frane Polić
2023-08-25 09:34:48 +02:00
committed by GitHub
parent f07dc0384f
commit 0c7c2eeeab
12 changed files with 121 additions and 31 deletions
+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)
}, [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[] }) => {
@@ -171,7 +171,7 @@ type UploadModalProps = {
description1Text: string
description2Title: string
description2Text: string
templateLink: string
onDownloadTemplate: () => any
canImport?: boolean
progress?: number
onClose: () => void
@@ -195,7 +195,7 @@ function UploadModal(props: UploadModalProps) {
onClose,
onSubmit,
onFileRemove,
templateLink,
onDownloadTemplate,
summary,
hasError,
errorMessage,
@@ -216,8 +216,6 @@ function UploadModal(props: UploadModalProps) {
onFileRemove()
}
const download = useHref(templateLink)
return (
<Modal open handleClose={onClose}>
<Modal.Body>
@@ -275,7 +273,10 @@ function UploadModal(props: UploadModalProps) {
name="medusa-template.csv"
size={2967}
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" />
</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 useNotification from "../../../hooks/use-notification"
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.
@@ -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 (
<UploadModal
type="prices"
@@ -190,7 +187,7 @@ function ImportPrices(props: ImportPricesProps) {
summary={getSummary()}
onFileRemove={onFileRemove}
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;
;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;;;;;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
`
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 useNotification from "../../../hooks/use-notification"
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.
@@ -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 (
<UploadModal
type="products"
@@ -194,7 +191,7 @@ function ImportProducts(props: ImportProductsProps) {
onFileRemove={onFileRemove}
processUpload={processUpload}
fileTitle={"products list"}
templateLink={templateLink}
onDownloadTemplate={downloadProductImportCSVTemplate}
errorMessage={batchJob?.result?.errors?.join(" \n")}
description2Title="Unsure about how to arrange your list?"
description2Text="Download the template below to ensure you are following the correct format."
@@ -96,6 +96,7 @@ const Overview = () => {
</Button>
</div>
)
default:
return (
<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 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 { SettingProvider } from "./setting-provider"
import { WidgetProvider } from "./widget-provider"
import { ImportRefresh } from "./import-refresh"
type Props = PropsWithChildren<{
widgetRegistry: WidgetRegistry
@@ -32,17 +33,19 @@ export const Providers = ({
<MedusaProvider>
<FeatureFlagProvider>
<PollingProvider>
<SteppedProvider>
<LayeredModalProvider>
<WidgetProvider registry={widgetRegistry}>
<RouteProvider registry={routeRegistry}>
<SettingProvider registry={settingRegistry}>
{children}
</SettingProvider>
</RouteProvider>
</WidgetProvider>
</LayeredModalProvider>
</SteppedProvider>
<ImportRefresh>
<SteppedProvider>
<LayeredModalProvider>
<WidgetProvider registry={widgetRegistry}>
<RouteProvider registry={routeRegistry}>
<SettingProvider registry={settingRegistry}>
{children}
</SettingProvider>
</RouteProvider>
</WidgetProvider>
</LayeredModalProvider>
</SteppedProvider>
</ImportRefresh>
</PollingProvider>
</FeatureFlagProvider>
</MedusaProvider>