diff --git a/.changeset/ten-steaks-promise.md b/.changeset/ten-steaks-promise.md new file mode 100644 index 0000000000..50b52e8f00 --- /dev/null +++ b/.changeset/ten-steaks-promise.md @@ -0,0 +1,5 @@ +--- +"@medusajs/admin-ui": patch +--- + +feat(admin-ui): refresh products when import is complete diff --git a/packages/admin-ui/ui/public/temp/price-list-import-template.csv b/packages/admin-ui/ui/public/temp/price-list-import-template.csv deleted file mode 100644 index bc812524a2..0000000000 --- a/packages/admin-ui/ui/public/temp/price-list-import-template.csv +++ /dev/null @@ -1,3 +0,0 @@ -Product Variant ID,SKU,Price EUR,Price NA [USD] -,MEDUSA-SWEAT-SMALL,15,13.5 -variant_1234,,15,13.5 diff --git a/packages/admin-ui/ui/src/components/organisms/batch-jobs-activity-list/index.tsx b/packages/admin-ui/ui/src/components/organisms/batch-jobs-activity-list/index.tsx index 4bdc7689dd..2ebc12541c 100644 --- a/packages/admin-ui/ui/src/components/organisms/batch-jobs-activity-list/index.tsx +++ b/packages/admin-ui/ui/src/components/organisms/batch-jobs-activity-list/index.tsx @@ -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[] }) => { diff --git a/packages/admin-ui/ui/src/components/organisms/upload-modal/index.tsx b/packages/admin-ui/ui/src/components/organisms/upload-modal/index.tsx index 243eea2b06..94ff880dbf 100644 --- a/packages/admin-ui/ui/src/components/organisms/upload-modal/index.tsx +++ b/packages/admin-ui/ui/src/components/organisms/upload-modal/index.tsx @@ -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 ( @@ -275,7 +273,10 @@ function UploadModal(props: UploadModalProps) { name="medusa-template.csv" size={2967} action={ - + } diff --git a/packages/admin-ui/ui/src/domain/pricing/batch-job/download-template.ts b/packages/admin-ui/ui/src/domain/pricing/batch-job/download-template.ts new file mode 100644 index 0000000000..a1f64ecec5 --- /dev/null +++ b/packages/admin-ui/ui/src/domain/pricing/batch-job/download-template.ts @@ -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() +} diff --git a/packages/admin-ui/ui/src/domain/pricing/batch-job/import.tsx b/packages/admin-ui/ui/src/domain/pricing/batch-job/import.tsx index 3a2f5864a4..a4edf69be4 100644 --- a/packages/admin-ui/ui/src/domain/pricing/batch-job/import.tsx +++ b/packages/admin-ui/ui/src/domain/pricing/batch-job/import.tsx @@ -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 ( ) } diff --git a/packages/admin-ui/ui/public/temp/product-import-template.csv b/packages/admin-ui/ui/src/domain/products/batch-job/download-template.ts similarity index 66% rename from packages/admin-ui/ui/public/temp/product-import-template.csv rename to packages/admin-ui/ui/src/domain/products/batch-job/download-template.ts index 9804745c2f..1225153aad 100644 --- a/packages/admin-ui/ui/public/temp/product-import-template.csv +++ b/packages/admin-ui/ui/src/domain/products/batch-job/download-template.ts @@ -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() +} diff --git a/packages/admin-ui/ui/src/domain/products/batch-job/import.tsx b/packages/admin-ui/ui/src/domain/products/batch-job/import.tsx index 783c1a5c02..562f9c52c6 100644 --- a/packages/admin-ui/ui/src/domain/products/batch-job/import.tsx +++ b/packages/admin-ui/ui/src/domain/products/batch-job/import.tsx @@ -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 ( { ) + default: return (
diff --git a/packages/admin-ui/ui/src/providers/import-refresh.tsx b/packages/admin-ui/ui/src/providers/import-refresh.tsx new file mode 100644 index 0000000000..8d57705aa4 --- /dev/null +++ b/packages/admin-ui/ui/src/providers/import-refresh.tsx @@ -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} +} diff --git a/packages/admin-ui/ui/src/providers/polling-provider.tsx b/packages/admin-ui/ui/src/providers/polling-provider.tsx index 3c7665fb94..cd209db37e 100644 --- a/packages/admin-ui/ui/src/providers/polling-provider.tsx +++ b/packages/admin-ui/ui/src/providers/polling-provider.tsx @@ -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") + ) +} diff --git a/packages/admin-ui/ui/src/providers/providers.tsx b/packages/admin-ui/ui/src/providers/providers.tsx index 7fbadad250..1afe427450 100644 --- a/packages/admin-ui/ui/src/providers/providers.tsx +++ b/packages/admin-ui/ui/src/providers/providers.tsx @@ -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 = ({ - - - - - - {children} - - - - - + + + + + + + {children} + + + + + +