feat(admin-ui): refresh products when a BatchJob is completed (#4840)
This commit is contained in:
1 parent
f07dc0384f
commit
0c7c2eeeab
12 files changed
+110
-20
No files matched your search
@@ -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)
|
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}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+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;
|
;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,6 +33,7 @@ export const Providers = ({
|
|||||||
<MedusaProvider>
|
<MedusaProvider>
|
||||||
<FeatureFlagProvider>
|
<FeatureFlagProvider>
|
||||||
<PollingProvider>
|
<PollingProvider>
|
||||||
|
<ImportRefresh>
|
||||||
<SteppedProvider>
|
<SteppedProvider>
|
||||||
<LayeredModalProvider>
|
<LayeredModalProvider>
|
||||||
<WidgetProvider registry={widgetRegistry}>
|
<WidgetProvider registry={widgetRegistry}>
|
||||||
@@ -43,6 +45,7 @@ export const Providers = ({
|
|||||||
</WidgetProvider>
|
</WidgetProvider>
|
||||||
</LayeredModalProvider>
|
</LayeredModalProvider>
|
||||||
</SteppedProvider>
|
</SteppedProvider>
|
||||||
|
</ImportRefresh>
|
||||||
</PollingProvider>
|
</PollingProvider>
|
||||||
</FeatureFlagProvider>
|
</FeatureFlagProvider>
|
||||||
</MedusaProvider>
|
</MedusaProvider>
|
||||||
|
|||||||
Reference in new issue
Block a user