From b1b4ae8a3de57c482cab336fa25ed044f361cb7f Mon Sep 17 00:00:00 2001 From: Stevche Radevski Date: Thu, 25 Jul 2024 11:26:01 +0200 Subject: [PATCH 1/2] feat: Add support for product export in UI --- .../data-table-filter/data-table-filter.tsx | 15 +- .../data-table-filter/date-filter.tsx | 224 ++++++++++-------- .../data-table-filter/number-filter.tsx | 200 ++++++++-------- .../data-table-filter/select-filter.tsx | 179 +++++++------- .../data-table-filter/string-filter.tsx | 92 +++---- .../data-table/data-table-filter/types.ts | 1 + .../dashboard/src/hooks/api/products.tsx | 17 ++ .../dashboard/src/i18n/translations/en.json | 24 +- .../providers/router-provider/route-map.tsx | 8 + .../components/export-filters.tsx | 22 ++ .../routes/products/product-export/index.ts | 1 + .../product-export/product-export.tsx | 69 ++++++ .../routes/products/product-import/index.ts | 1 + .../product-import/product-import.tsx | 21 ++ .../product-list-table/product-list-table.tsx | 17 +- packages/core/js-sdk/src/admin/product.ts | 2 +- 16 files changed, 559 insertions(+), 334 deletions(-) create mode 100644 packages/admin-next/dashboard/src/routes/products/product-export/components/export-filters.tsx create mode 100644 packages/admin-next/dashboard/src/routes/products/product-export/index.ts create mode 100644 packages/admin-next/dashboard/src/routes/products/product-export/product-export.tsx create mode 100644 packages/admin-next/dashboard/src/routes/products/product-import/index.ts create mode 100644 packages/admin-next/dashboard/src/routes/products/product-import/product-import.tsx diff --git a/packages/admin-next/dashboard/src/components/table/data-table/data-table-filter/data-table-filter.tsx b/packages/admin-next/dashboard/src/components/table/data-table/data-table-filter/data-table-filter.tsx index 8405f46f4c5c8..77efa132598d0 100644 --- a/packages/admin-next/dashboard/src/components/table/data-table/data-table-filter/data-table-filter.tsx +++ b/packages/admin-next/dashboard/src/components/table/data-table/data-table-filter/data-table-filter.tsx @@ -41,10 +41,15 @@ export type Filter = { type DataTableFilterProps = { filters: Filter[] + readonly?: boolean prefix?: string } -export const DataTableFilter = ({ filters, prefix }: DataTableFilterProps) => { +export const DataTableFilter = ({ + filters, + readonly, + prefix, +}: DataTableFilterProps) => { const { t } = useTranslation() const [searchParams] = useSearchParams() const [open, setOpen] = useState(false) @@ -127,6 +132,7 @@ export const DataTableFilter = ({ filters, prefix }: DataTableFilterProps) => { key={filter.key} filter={filter} prefix={prefix} + readonly={readonly} options={filter.options} multiple={filter.multiple} searchable={filter.searchable} @@ -139,6 +145,7 @@ export const DataTableFilter = ({ filters, prefix }: DataTableFilterProps) => { key={filter.key} filter={filter} prefix={prefix} + readonly={readonly} openOnMount={filter.openOnMount} /> ) @@ -148,6 +155,7 @@ export const DataTableFilter = ({ filters, prefix }: DataTableFilterProps) => { key={filter.key} filter={filter} prefix={prefix} + readonly={readonly} openOnMount={filter.openOnMount} /> ) @@ -157,6 +165,7 @@ export const DataTableFilter = ({ filters, prefix }: DataTableFilterProps) => { key={filter.key} filter={filter} prefix={prefix} + readonly={readonly} openOnMount={filter.openOnMount} /> ) @@ -164,7 +173,7 @@ export const DataTableFilter = ({ filters, prefix }: DataTableFilterProps) => { break } })} - {availableFilters.length > 0 && ( + {!readonly && availableFilters.length > 0 && ( - - ) - })} -
  • - +
  • + ) + })} +
  • + -
  • - - {showCustom && ( -
    -
    -
    - - {t("filters.date.from")} - -
    -
    - handleCustomDateChange(d, "start")} - /> -
    -
    -
    -
    - - {t("filters.date.to")} - +
    + +
    + {t("filters.date.custom")} + + + + {showCustom && ( +
    +
    +
    + + {t("filters.date.from")} + +
    +
    + handleCustomDateChange(d, "start")} + /> +
    -
    - { - handleCustomDateChange(d, "end") - }} - /> +
    +
    + + {t("filters.date.to")} + +
    +
    + { + handleCustomDateChange(d, "end") + }} + /> +
    -
    - )} - - + )} + + + )} ) } @@ -231,10 +239,16 @@ export const DateFilter = ({ type DateDisplayProps = { label: string value?: string + readonly?: boolean onRemove: () => void } -const DateDisplay = ({ label, value, onRemove }: DateDisplayProps) => { +const DateDisplay = ({ + label, + value, + readonly, + onRemove, +}: DateDisplayProps) => { const handleRemove = (e: MouseEvent) => { e.stopPropagation() onRemove() @@ -245,8 +259,10 @@ const DateDisplay = ({ label, value, onRemove }: DateDisplayProps) => { asChild className={clx( "bg-ui-bg-field transition-fg shadow-borders-base text-ui-fg-subtle flex cursor-pointer select-none items-center rounded-md", - "hover:bg-ui-bg-field-hover", - "data-[state=open]:bg-ui-bg-field-hover" + { + "hover:bg-ui-bg-field-hover": !readonly, + "data-[state=open]:bg-ui-bg-field-hover": !readonly, + } )} >
    @@ -268,7 +284,7 @@ const DateDisplay = ({ label, value, onRemove }: DateDisplayProps) => {
    )} - {value && ( + {!readonly && value && (
    + }} + > + + {searchable && ( +
    +
    + +
    + +
    -
    - )} - - - {t("general.noResultsTitle")} - - - - {options.map((option) => { - const isSelected = selectedParams - .get() - .includes(String(option.value)) + )} + + + {t("general.noResultsTitle")} + + + + {options.map((option) => { + const isSelected = selectedParams + .get() + .includes(String(option.value)) - return ( - { - handleSelect(option.value) - }} - > -
    { + handleSelect(option.value) + }} > - {multiple ? : } -
    - {option.label} -
    - ) - })} -
    - - - +
    + {multiple ? : } +
    + {option.label} + + ) + })} +
    + + + + )} ) } type SelectDisplayProps = { label: string + readonly?: boolean value?: string | string[] onRemove: () => void } @@ -185,6 +191,7 @@ export const SelectDisplay = ({ label, value, onRemove, + readonly, }: SelectDisplayProps) => { const { t } = useTranslation() const v = value ? (Array.isArray(value) ? value : [value]) : null @@ -200,8 +207,10 @@ export const SelectDisplay = ({
    )}
    - {v && v.length > 0 && ( + {!readonly && v && v.length > 0 && (
    + + +
    + + + ) +} diff --git a/packages/admin-next/dashboard/src/routes/products/product-import/index.ts b/packages/admin-next/dashboard/src/routes/products/product-import/index.ts new file mode 100644 index 0000000000000..4f065847b31dc --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/products/product-import/index.ts @@ -0,0 +1 @@ +export { ProductImport as Component } from "./product-import" diff --git a/packages/admin-next/dashboard/src/routes/products/product-import/product-import.tsx b/packages/admin-next/dashboard/src/routes/products/product-import/product-import.tsx new file mode 100644 index 0000000000000..d8ac5bf515955 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/products/product-import/product-import.tsx @@ -0,0 +1,21 @@ +import { Heading } from "@medusajs/ui" +import { RouteDrawer } from "../../../components/modals" +import { useTranslation } from "react-i18next" + +export const ProductImport = () => { + const { t } = useTranslation() + + return ( + + + + {t("products.import.header")} + + + {t("products.import.description")} + + + + + ) +} diff --git a/packages/admin-next/dashboard/src/routes/products/product-list/components/product-list-table/product-list-table.tsx b/packages/admin-next/dashboard/src/routes/products/product-list/components/product-list-table/product-list-table.tsx index 26e08f8a522d0..7d1279cd754c8 100644 --- a/packages/admin-next/dashboard/src/routes/products/product-list/components/product-list-table/product-list-table.tsx +++ b/packages/admin-next/dashboard/src/routes/products/product-list/components/product-list-table/product-list-table.tsx @@ -4,7 +4,7 @@ import { keepPreviousData } from "@tanstack/react-query" import { createColumnHelper } from "@tanstack/react-table" import { useMemo } from "react" import { useTranslation } from "react-i18next" -import { Link, Outlet, useLoaderData } from "react-router-dom" +import { Link, Outlet, useLoaderData, useLocation } from "react-router-dom" import { HttpTypes } from "@medusajs/types" import { ActionMenu } from "../../../../../components/common/action-menu" @@ -23,6 +23,7 @@ const PAGE_SIZE = 20 export const ProductListTable = () => { const { t } = useTranslation() + const location = useLocation() const initialData = useLoaderData() as Awaited< ReturnType> @@ -59,9 +60,17 @@ export const ProductListTable = () => {
    {t("products.domain")} - +
    + + {/* */} + +
    ( From 0d4099fc5c44dfedfa80da04efd0d98d76b748fb Mon Sep 17 00:00:00 2001 From: Stevche Radevski Date: Mon, 29 Jul 2024 20:15:57 +0300 Subject: [PATCH 2/2] fix:Return the backend URL for private files of local file provider --- .../http/__tests__/product/admin/product-export.spec.ts | 3 ++- .../modules/providers/file-local/src/services/local-file.ts | 5 ----- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/integration-tests/http/__tests__/product/admin/product-export.spec.ts b/integration-tests/http/__tests__/product/admin/product-export.spec.ts index 4ad050df52b7f..91655030cf699 100644 --- a/integration-tests/http/__tests__/product/admin/product-export.spec.ts +++ b/integration-tests/http/__tests__/product/admin/product-export.spec.ts @@ -12,7 +12,8 @@ import { ModuleRegistrationName } from "@medusajs/utils" jest.setTimeout(50000) const compareCSVs = async (filePath, expectedFilePath) => { - let fileContent = await fs.readFile(filePath, { encoding: "utf-8" }) + const asLocalPath = filePath.replace("http://localhost:9000", process.cwd()) + let fileContent = await fs.readFile(asLocalPath, { encoding: "utf-8" }) let fixturesContent = await fs.readFile(expectedFilePath, { encoding: "utf-8", }) diff --git a/packages/modules/providers/file-local/src/services/local-file.ts b/packages/modules/providers/file-local/src/services/local-file.ts index 312246f076b65..808127aac4d58 100644 --- a/packages/modules/providers/file-local/src/services/local-file.ts +++ b/packages/modules/providers/file-local/src/services/local-file.ts @@ -77,7 +77,6 @@ export class LocalFileService extends AbstractFileProviderService { return } - // For private files, we simply return the file path, which can then be loaded manually by the backend. // The local file provider doesn't support presigned URLs for private files (i.e files not placed in /static). async getPresignedDownloadUrl( file: FileTypes.ProviderGetFileDTO @@ -96,10 +95,6 @@ export class LocalFileService extends AbstractFileProviderService { ) } - if (isPrivate) { - return filePath - } - return this.getUploadFileUrl(file.fileKey) }