feat(admin-ui): refresh products when a BatchJob is completed (#4840)
This commit is contained in:
@@ -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
|
||||
|
@@ -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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
+13
-1
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user