feat: Add support for product export in UI (#8281)
* feat: Add support for product export in UI * fix:Return the backend URL for private files of local file provider
This commit is contained in:
1 parent
1d773c536f
commit
b539c6d5bb
18 files changed
+248
-27
No files matched your search
@@ -12,7 +12,8 @@ import { ModuleRegistrationName } from "@medusajs/utils"
|
|||||||
jest.setTimeout(50000)
|
jest.setTimeout(50000)
|
||||||
|
|
||||||
const compareCSVs = async (filePath, expectedFilePath) => {
|
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, {
|
let fixturesContent = await fs.readFile(expectedFilePath, {
|
||||||
encoding: "utf-8",
|
encoding: "utf-8",
|
||||||
})
|
})
|
||||||
|
|||||||
+12
-3
@@ -41,10 +41,15 @@ export type Filter = {
|
|||||||
|
|
||||||
type DataTableFilterProps = {
|
type DataTableFilterProps = {
|
||||||
filters: Filter[]
|
filters: Filter[]
|
||||||
|
readonly?: boolean
|
||||||
prefix?: string
|
prefix?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DataTableFilter = ({ filters, prefix }: DataTableFilterProps) => {
|
export const DataTableFilter = ({
|
||||||
|
filters,
|
||||||
|
readonly,
|
||||||
|
prefix,
|
||||||
|
}: DataTableFilterProps) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const [searchParams] = useSearchParams()
|
const [searchParams] = useSearchParams()
|
||||||
const [open, setOpen] = useState(false)
|
const [open, setOpen] = useState(false)
|
||||||
@@ -127,6 +132,7 @@ export const DataTableFilter = ({ filters, prefix }: DataTableFilterProps) => {
|
|||||||
key={filter.key}
|
key={filter.key}
|
||||||
filter={filter}
|
filter={filter}
|
||||||
prefix={prefix}
|
prefix={prefix}
|
||||||
|
readonly={readonly}
|
||||||
options={filter.options}
|
options={filter.options}
|
||||||
multiple={filter.multiple}
|
multiple={filter.multiple}
|
||||||
searchable={filter.searchable}
|
searchable={filter.searchable}
|
||||||
@@ -139,6 +145,7 @@ export const DataTableFilter = ({ filters, prefix }: DataTableFilterProps) => {
|
|||||||
key={filter.key}
|
key={filter.key}
|
||||||
filter={filter}
|
filter={filter}
|
||||||
prefix={prefix}
|
prefix={prefix}
|
||||||
|
readonly={readonly}
|
||||||
openOnMount={filter.openOnMount}
|
openOnMount={filter.openOnMount}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
@@ -148,6 +155,7 @@ export const DataTableFilter = ({ filters, prefix }: DataTableFilterProps) => {
|
|||||||
key={filter.key}
|
key={filter.key}
|
||||||
filter={filter}
|
filter={filter}
|
||||||
prefix={prefix}
|
prefix={prefix}
|
||||||
|
readonly={readonly}
|
||||||
openOnMount={filter.openOnMount}
|
openOnMount={filter.openOnMount}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
@@ -157,6 +165,7 @@ export const DataTableFilter = ({ filters, prefix }: DataTableFilterProps) => {
|
|||||||
key={filter.key}
|
key={filter.key}
|
||||||
filter={filter}
|
filter={filter}
|
||||||
prefix={prefix}
|
prefix={prefix}
|
||||||
|
readonly={readonly}
|
||||||
openOnMount={filter.openOnMount}
|
openOnMount={filter.openOnMount}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
@@ -164,7 +173,7 @@ export const DataTableFilter = ({ filters, prefix }: DataTableFilterProps) => {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
})}
|
})}
|
||||||
{availableFilters.length > 0 && (
|
{!readonly && availableFilters.length > 0 && (
|
||||||
<Popover.Root modal open={open} onOpenChange={setOpen}>
|
<Popover.Root modal open={open} onOpenChange={setOpen}>
|
||||||
<Popover.Trigger asChild id="filters_menu_trigger">
|
<Popover.Trigger asChild id="filters_menu_trigger">
|
||||||
<Button size="small" variant="secondary">
|
<Button size="small" variant="secondary">
|
||||||
@@ -208,7 +217,7 @@ export const DataTableFilter = ({ filters, prefix }: DataTableFilterProps) => {
|
|||||||
</Popover.Portal>
|
</Popover.Portal>
|
||||||
</Popover.Root>
|
</Popover.Root>
|
||||||
)}
|
)}
|
||||||
{activeFilters.length > 0 && (
|
{!readonly && activeFilters.length > 0 && (
|
||||||
<ClearAllFilters filters={filters} prefix={prefix} />
|
<ClearAllFilters filters={filters} prefix={prefix} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+21
-5
@@ -35,6 +35,7 @@ type DateComparisonOperator = {
|
|||||||
export const DateFilter = ({
|
export const DateFilter = ({
|
||||||
filter,
|
filter,
|
||||||
prefix,
|
prefix,
|
||||||
|
readonly,
|
||||||
openOnMount,
|
openOnMount,
|
||||||
}: DateFilterProps) => {
|
}: DateFilterProps) => {
|
||||||
const [open, setOpen] = useState(openOnMount)
|
const [open, setOpen] = useState(openOnMount)
|
||||||
@@ -118,7 +119,13 @@ export const DateFilter = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover.Root modal open={open} onOpenChange={handleOpenChange}>
|
<Popover.Root modal open={open} onOpenChange={handleOpenChange}>
|
||||||
<DateDisplay label={label} value={displayValue} onRemove={handleRemove} />
|
<DateDisplay
|
||||||
|
label={label}
|
||||||
|
value={displayValue}
|
||||||
|
onRemove={handleRemove}
|
||||||
|
readonly={readonly}
|
||||||
|
/>
|
||||||
|
{!readonly && (
|
||||||
<Popover.Portal>
|
<Popover.Portal>
|
||||||
<Popover.Content
|
<Popover.Content
|
||||||
data-name="date_filter_content"
|
data-name="date_filter_content"
|
||||||
@@ -224,6 +231,7 @@ export const DateFilter = ({
|
|||||||
)}
|
)}
|
||||||
</Popover.Content>
|
</Popover.Content>
|
||||||
</Popover.Portal>
|
</Popover.Portal>
|
||||||
|
)}
|
||||||
</Popover.Root>
|
</Popover.Root>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -231,10 +239,16 @@ export const DateFilter = ({
|
|||||||
type DateDisplayProps = {
|
type DateDisplayProps = {
|
||||||
label: string
|
label: string
|
||||||
value?: string
|
value?: string
|
||||||
|
readonly?: boolean
|
||||||
onRemove: () => void
|
onRemove: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const DateDisplay = ({ label, value, onRemove }: DateDisplayProps) => {
|
const DateDisplay = ({
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
readonly,
|
||||||
|
onRemove,
|
||||||
|
}: DateDisplayProps) => {
|
||||||
const handleRemove = (e: MouseEvent<HTMLButtonElement>) => {
|
const handleRemove = (e: MouseEvent<HTMLButtonElement>) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
onRemove()
|
onRemove()
|
||||||
@@ -245,8 +259,10 @@ const DateDisplay = ({ label, value, onRemove }: DateDisplayProps) => {
|
|||||||
asChild
|
asChild
|
||||||
className={clx(
|
className={clx(
|
||||||
"bg-ui-bg-field transition-fg shadow-borders-base text-ui-fg-subtle flex cursor-pointer select-none items-center rounded-md",
|
"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,
|
||||||
|
}
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
@@ -268,7 +284,7 @@ const DateDisplay = ({ label, value, onRemove }: DateDisplayProps) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{value && (
|
{!readonly && value && (
|
||||||
<div>
|
<div>
|
||||||
<button
|
<button
|
||||||
onClick={handleRemove}
|
onClick={handleRemove}
|
||||||
|
|||||||
+11
-3
@@ -24,6 +24,7 @@ type Operator = "lt" | "gt" | "eq"
|
|||||||
export const NumberFilter = ({
|
export const NumberFilter = ({
|
||||||
filter,
|
filter,
|
||||||
prefix,
|
prefix,
|
||||||
|
readonly,
|
||||||
openOnMount,
|
openOnMount,
|
||||||
}: NumberFilterProps) => {
|
}: NumberFilterProps) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
@@ -136,7 +137,9 @@ export const NumberFilter = ({
|
|||||||
label={label}
|
label={label}
|
||||||
value={currentValue}
|
value={currentValue}
|
||||||
onRemove={handleRemove}
|
onRemove={handleRemove}
|
||||||
|
readonly={readonly}
|
||||||
/>
|
/>
|
||||||
|
{!readonly && (
|
||||||
<Popover.Portal>
|
<Popover.Portal>
|
||||||
<Popover.Content
|
<Popover.Content
|
||||||
data-name="number_filter_content"
|
data-name="number_filter_content"
|
||||||
@@ -234,6 +237,7 @@ export const NumberFilter = ({
|
|||||||
</div>
|
</div>
|
||||||
</Popover.Content>
|
</Popover.Content>
|
||||||
</Popover.Portal>
|
</Popover.Portal>
|
||||||
|
)}
|
||||||
</Popover.Root>
|
</Popover.Root>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -241,10 +245,12 @@ export const NumberFilter = ({
|
|||||||
const NumberDisplay = ({
|
const NumberDisplay = ({
|
||||||
label,
|
label,
|
||||||
value,
|
value,
|
||||||
|
readonly,
|
||||||
onRemove,
|
onRemove,
|
||||||
}: {
|
}: {
|
||||||
label: string
|
label: string
|
||||||
value?: string[]
|
value?: string[]
|
||||||
|
readonly?: boolean
|
||||||
onRemove: () => void
|
onRemove: () => void
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
@@ -282,8 +288,10 @@ const NumberDisplay = ({
|
|||||||
asChild
|
asChild
|
||||||
className={clx(
|
className={clx(
|
||||||
"bg-ui-bg-field transition-fg shadow-borders-base text-ui-fg-subtle flex cursor-pointer select-none items-center rounded-md",
|
"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,
|
||||||
|
}
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
@@ -317,7 +325,7 @@ const NumberDisplay = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{value && (
|
{!readonly && value && (
|
||||||
<div>
|
<div>
|
||||||
<button
|
<button
|
||||||
onClick={handleRemove}
|
onClick={handleRemove}
|
||||||
|
|||||||
+12
-3
@@ -11,6 +11,7 @@ import { IFilter } from "./types"
|
|||||||
|
|
||||||
interface SelectFilterProps extends IFilter {
|
interface SelectFilterProps extends IFilter {
|
||||||
options: { label: string; value: unknown }[]
|
options: { label: string; value: unknown }[]
|
||||||
|
readonly?: boolean
|
||||||
multiple?: boolean
|
multiple?: boolean
|
||||||
searchable?: boolean
|
searchable?: boolean
|
||||||
}
|
}
|
||||||
@@ -18,6 +19,7 @@ interface SelectFilterProps extends IFilter {
|
|||||||
export const SelectFilter = ({
|
export const SelectFilter = ({
|
||||||
filter,
|
filter,
|
||||||
prefix,
|
prefix,
|
||||||
|
readonly,
|
||||||
multiple,
|
multiple,
|
||||||
searchable,
|
searchable,
|
||||||
options,
|
options,
|
||||||
@@ -80,10 +82,12 @@ export const SelectFilter = ({
|
|||||||
return (
|
return (
|
||||||
<Popover.Root modal open={open} onOpenChange={handleOpenChange}>
|
<Popover.Root modal open={open} onOpenChange={handleOpenChange}>
|
||||||
<SelectDisplay
|
<SelectDisplay
|
||||||
|
readonly={readonly}
|
||||||
label={label}
|
label={label}
|
||||||
value={labelValues}
|
value={labelValues}
|
||||||
onRemove={handleRemove}
|
onRemove={handleRemove}
|
||||||
/>
|
/>
|
||||||
|
{!readonly && (
|
||||||
<Popover.Portal>
|
<Popover.Portal>
|
||||||
<Popover.Content
|
<Popover.Content
|
||||||
hideWhenDetached
|
hideWhenDetached
|
||||||
@@ -171,12 +175,14 @@ export const SelectFilter = ({
|
|||||||
</Command>
|
</Command>
|
||||||
</Popover.Content>
|
</Popover.Content>
|
||||||
</Popover.Portal>
|
</Popover.Portal>
|
||||||
|
)}
|
||||||
</Popover.Root>
|
</Popover.Root>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
type SelectDisplayProps = {
|
type SelectDisplayProps = {
|
||||||
label: string
|
label: string
|
||||||
|
readonly?: boolean
|
||||||
value?: string | string[]
|
value?: string | string[]
|
||||||
onRemove: () => void
|
onRemove: () => void
|
||||||
}
|
}
|
||||||
@@ -185,6 +191,7 @@ export const SelectDisplay = ({
|
|||||||
label,
|
label,
|
||||||
value,
|
value,
|
||||||
onRemove,
|
onRemove,
|
||||||
|
readonly,
|
||||||
}: SelectDisplayProps) => {
|
}: SelectDisplayProps) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const v = value ? (Array.isArray(value) ? value : [value]) : null
|
const v = value ? (Array.isArray(value) ? value : [value]) : null
|
||||||
@@ -200,8 +207,10 @@ export const SelectDisplay = ({
|
|||||||
<div
|
<div
|
||||||
className={clx(
|
className={clx(
|
||||||
"bg-ui-bg-field transition-fg shadow-borders-base text-ui-fg-subtle flex cursor-pointer select-none items-center overflow-hidden rounded-md",
|
"bg-ui-bg-field transition-fg shadow-borders-base text-ui-fg-subtle flex cursor-pointer select-none items-center overflow-hidden 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,
|
||||||
|
}
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@@ -242,7 +251,7 @@ export const SelectDisplay = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{v && v.length > 0 && (
|
{!readonly && v && v.length > 0 && (
|
||||||
<div>
|
<div>
|
||||||
<button
|
<button
|
||||||
onClick={handleRemove}
|
onClick={handleRemove}
|
||||||
|
|||||||
+16
-4
@@ -13,6 +13,7 @@ type StringFilterProps = IFilter
|
|||||||
export const StringFilter = ({
|
export const StringFilter = ({
|
||||||
filter,
|
filter,
|
||||||
prefix,
|
prefix,
|
||||||
|
readonly,
|
||||||
openOnMount,
|
openOnMount,
|
||||||
}: StringFilterProps) => {
|
}: StringFilterProps) => {
|
||||||
const [open, setOpen] = useState(openOnMount)
|
const [open, setOpen] = useState(openOnMount)
|
||||||
@@ -67,7 +68,13 @@ export const StringFilter = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover.Root modal open={open} onOpenChange={handleOpenChange}>
|
<Popover.Root modal open={open} onOpenChange={handleOpenChange}>
|
||||||
<StringDisplay label={label} value={query?.[0]} onRemove={handleRemove} />
|
<StringDisplay
|
||||||
|
label={label}
|
||||||
|
value={query?.[0]}
|
||||||
|
onRemove={handleRemove}
|
||||||
|
readonly={readonly}
|
||||||
|
/>
|
||||||
|
{!readonly && (
|
||||||
<Popover.Portal>
|
<Popover.Portal>
|
||||||
<Popover.Content
|
<Popover.Content
|
||||||
hideWhenDetached
|
hideWhenDetached
|
||||||
@@ -106,6 +113,7 @@ export const StringFilter = ({
|
|||||||
</div>
|
</div>
|
||||||
</Popover.Content>
|
</Popover.Content>
|
||||||
</Popover.Portal>
|
</Popover.Portal>
|
||||||
|
)}
|
||||||
</Popover.Root>
|
</Popover.Root>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -113,10 +121,12 @@ export const StringFilter = ({
|
|||||||
const StringDisplay = ({
|
const StringDisplay = ({
|
||||||
label,
|
label,
|
||||||
value,
|
value,
|
||||||
|
readonly,
|
||||||
onRemove,
|
onRemove,
|
||||||
}: {
|
}: {
|
||||||
label: string
|
label: string
|
||||||
value?: string
|
value?: string
|
||||||
|
readonly?: boolean
|
||||||
onRemove: () => void
|
onRemove: () => void
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
@@ -126,8 +136,10 @@ const StringDisplay = ({
|
|||||||
<div
|
<div
|
||||||
className={clx(
|
className={clx(
|
||||||
"bg-ui-bg-field transition-fg shadow-borders-base text-ui-fg-subtle flex cursor-pointer select-none items-center overflow-hidden rounded-md",
|
"bg-ui-bg-field transition-fg shadow-borders-base text-ui-fg-subtle flex cursor-pointer select-none items-center overflow-hidden 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,
|
||||||
|
}
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@@ -168,7 +180,7 @@ const StringDisplay = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{!!value && (
|
{!readonly && !!value && (
|
||||||
<div>
|
<div>
|
||||||
<button
|
<button
|
||||||
onClick={onRemove}
|
onClick={onRemove}
|
||||||
|
|||||||
+1
@@ -3,6 +3,7 @@ export interface IFilter {
|
|||||||
key: string
|
key: string
|
||||||
label: string
|
label: string
|
||||||
}
|
}
|
||||||
|
readonly?: boolean
|
||||||
openOnMount?: boolean
|
openOnMount?: boolean
|
||||||
prefix?: string
|
prefix?: string
|
||||||
}
|
}
|
||||||
@@ -326,3 +326,20 @@ export const useDeleteProduct = (
|
|||||||
...options,
|
...options,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const useExportProducts = (
|
||||||
|
query?: HttpTypes.AdminProductListParams,
|
||||||
|
options?: UseMutationOptions<
|
||||||
|
HttpTypes.AdminExportProductResponse,
|
||||||
|
FetchError,
|
||||||
|
HttpTypes.AdminExportProductRequest
|
||||||
|
>
|
||||||
|
) => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (payload) => sdk.admin.product.export(payload, query),
|
||||||
|
onSuccess: (data, variables, context) => {
|
||||||
|
options?.onSuccess?.(data, variables, context)
|
||||||
|
},
|
||||||
|
...options,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -121,7 +121,9 @@
|
|||||||
"select": "Select",
|
"select": "Select",
|
||||||
"browse": "Browse",
|
"browse": "Browse",
|
||||||
"logout": "Logout",
|
"logout": "Logout",
|
||||||
"hide": "Hide"
|
"hide": "Hide",
|
||||||
|
"export": "Export",
|
||||||
|
"import": "Import"
|
||||||
},
|
},
|
||||||
"operators": {
|
"operators": {
|
||||||
"in": "In"
|
"in": "In"
|
||||||
@@ -330,6 +332,26 @@
|
|||||||
},
|
},
|
||||||
"successToast": "Product {{title}} was successfully created."
|
"successToast": "Product {{title}} was successfully created."
|
||||||
},
|
},
|
||||||
|
"export": {
|
||||||
|
"header": "Export Product List",
|
||||||
|
"description": "Export the product list to a CSV file.",
|
||||||
|
"success": {
|
||||||
|
"title": "We are processing your export",
|
||||||
|
"description": "Exporting data may take a few minutes. We will notify you when we are done."
|
||||||
|
},
|
||||||
|
"filters": {
|
||||||
|
"title": "Filters",
|
||||||
|
"description": "Apply filters in the table overview to adjust this view"
|
||||||
|
},
|
||||||
|
"columns": {
|
||||||
|
"title": "Columns",
|
||||||
|
"description": "Customize the exported data to meet specific needs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"import": {
|
||||||
|
"header": "Import Product List",
|
||||||
|
"description": "Import products by providing a CSV file in a pre-defined format"
|
||||||
|
},
|
||||||
"deleteWarning": "You are about to delete the product {{title}}. This action cannot be undone.",
|
"deleteWarning": "You are about to delete the product {{title}}. This action cannot be undone.",
|
||||||
"variants": "Variants",
|
"variants": "Variants",
|
||||||
"attributes": "Attributes",
|
"attributes": "Attributes",
|
||||||
|
|||||||
@@ -62,6 +62,14 @@ export const RouteMap: RouteObject[] = [
|
|||||||
path: "create",
|
path: "create",
|
||||||
lazy: () => import("../../routes/products/product-create"),
|
lazy: () => import("../../routes/products/product-create"),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "import",
|
||||||
|
lazy: () => import("../../routes/products/product-import"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "export",
|
||||||
|
lazy: () => import("../../routes/products/product-export"),
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
+22
@@ -0,0 +1,22 @@
|
|||||||
|
import { Heading, Text } from "@medusajs/ui"
|
||||||
|
import { DataTableFilter } from "../../../../components/table/data-table/data-table-filter"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
import { useProductTableFilters } from "../../../../hooks/table/filters"
|
||||||
|
|
||||||
|
export const ExportFilters = () => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const filters = useProductTableFilters()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Heading level="h2">{t("products.export.filters.title")}</Heading>
|
||||||
|
<Text size="small" className="text-ui-fg-subtle">
|
||||||
|
{t("products.export.filters.description")}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<div className="mt-4">
|
||||||
|
<DataTableFilter filters={filters} readonly />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { ProductExport as Component } from "./product-export"
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import { Button, Heading, toast } from "@medusajs/ui"
|
||||||
|
import { RouteDrawer, useRouteModal } from "../../../components/modals"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
import { ExportFilters } from "./components/export-filters"
|
||||||
|
import { useExportProducts } from "../../../hooks/api"
|
||||||
|
import { useProductTableQuery } from "../../../hooks/table/query"
|
||||||
|
|
||||||
|
export const ProductExport = () => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RouteDrawer>
|
||||||
|
<RouteDrawer.Header>
|
||||||
|
<RouteDrawer.Title asChild>
|
||||||
|
<Heading>{t("products.export.header")}</Heading>
|
||||||
|
</RouteDrawer.Title>
|
||||||
|
<RouteDrawer.Description className="sr-only">
|
||||||
|
{t("products.export.description")}
|
||||||
|
</RouteDrawer.Description>
|
||||||
|
</RouteDrawer.Header>
|
||||||
|
<ProductExportContent />
|
||||||
|
</RouteDrawer>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const ProductExportContent = () => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const { searchParams } = useProductTableQuery({})
|
||||||
|
const { mutateAsync } = useExportProducts(searchParams)
|
||||||
|
const { handleSuccess } = useRouteModal()
|
||||||
|
|
||||||
|
const handleExportRequest = async () => {
|
||||||
|
await mutateAsync(
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.info(t("products.export.success.title"), {
|
||||||
|
description: t("products.export.success.description"),
|
||||||
|
})
|
||||||
|
handleSuccess()
|
||||||
|
},
|
||||||
|
onError: (err) => {
|
||||||
|
toast.error(err.message)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<RouteDrawer.Body>
|
||||||
|
<ExportFilters />
|
||||||
|
{/* <Divider className="mt-4" variant="dashed" /> */}
|
||||||
|
</RouteDrawer.Body>
|
||||||
|
<RouteDrawer.Footer>
|
||||||
|
<div className="flex items-center gap-x-2">
|
||||||
|
<RouteDrawer.Close asChild>
|
||||||
|
<Button size="small" variant="secondary">
|
||||||
|
{t("actions.cancel")}
|
||||||
|
</Button>
|
||||||
|
</RouteDrawer.Close>
|
||||||
|
<Button onClick={handleExportRequest} size="small">
|
||||||
|
{t("actions.export")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</RouteDrawer.Footer>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { ProductImport as Component } from "./product-import"
|
||||||
@@ -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 (
|
||||||
|
<RouteDrawer>
|
||||||
|
<RouteDrawer.Header>
|
||||||
|
<RouteDrawer.Title asChild>
|
||||||
|
<Heading>{t("products.import.header")}</Heading>
|
||||||
|
</RouteDrawer.Title>
|
||||||
|
<RouteDrawer.Description className="sr-only">
|
||||||
|
{t("products.import.description")}
|
||||||
|
</RouteDrawer.Description>
|
||||||
|
</RouteDrawer.Header>
|
||||||
|
<RouteDrawer.Body></RouteDrawer.Body>
|
||||||
|
</RouteDrawer>
|
||||||
|
)
|
||||||
|
}
|
||||||
+10
-1
@@ -4,7 +4,7 @@ import { keepPreviousData } from "@tanstack/react-query"
|
|||||||
import { createColumnHelper } from "@tanstack/react-table"
|
import { createColumnHelper } from "@tanstack/react-table"
|
||||||
import { useMemo } from "react"
|
import { useMemo } from "react"
|
||||||
import { useTranslation } from "react-i18next"
|
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 { HttpTypes } from "@medusajs/types"
|
||||||
import { ActionMenu } from "../../../../../components/common/action-menu"
|
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||||
@@ -23,6 +23,7 @@ const PAGE_SIZE = 20
|
|||||||
|
|
||||||
export const ProductListTable = () => {
|
export const ProductListTable = () => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
const location = useLocation()
|
||||||
|
|
||||||
const initialData = useLoaderData() as Awaited<
|
const initialData = useLoaderData() as Awaited<
|
||||||
ReturnType<ReturnType<typeof productsLoader>>
|
ReturnType<ReturnType<typeof productsLoader>>
|
||||||
@@ -59,10 +60,18 @@ export const ProductListTable = () => {
|
|||||||
<Container className="divide-y p-0">
|
<Container className="divide-y p-0">
|
||||||
<div className="flex items-center justify-between px-6 py-4">
|
<div className="flex items-center justify-between px-6 py-4">
|
||||||
<Heading level="h2">{t("products.domain")}</Heading>
|
<Heading level="h2">{t("products.domain")}</Heading>
|
||||||
|
<div className="flex items-center justify-center gap-x-2">
|
||||||
|
<Button size="small" variant="secondary" asChild>
|
||||||
|
<Link to={`export${location.search}`}>{t("actions.export")}</Link>
|
||||||
|
</Button>
|
||||||
|
{/* <Button size="small" variant="secondary" asChild>
|
||||||
|
<Link to="import">{t("actions.import")}</Link>
|
||||||
|
</Button> */}
|
||||||
<Button size="small" variant="secondary" asChild>
|
<Button size="small" variant="secondary" asChild>
|
||||||
<Link to="create">{t("actions.create")}</Link>
|
<Link to="create">{t("actions.create")}</Link>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<DataTable
|
<DataTable
|
||||||
table={table}
|
table={table}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export class Product {
|
|||||||
|
|
||||||
async export(
|
async export(
|
||||||
body: HttpTypes.AdminExportProductRequest,
|
body: HttpTypes.AdminExportProductRequest,
|
||||||
query?: SelectParams,
|
query?: HttpTypes.AdminProductListParams,
|
||||||
headers?: ClientHeaders
|
headers?: ClientHeaders
|
||||||
) {
|
) {
|
||||||
return await this.client.fetch<HttpTypes.AdminExportProductResponse>(
|
return await this.client.fetch<HttpTypes.AdminExportProductResponse>(
|
||||||
|
|||||||
@@ -77,7 +77,6 @@ export class LocalFileService extends AbstractFileProviderService {
|
|||||||
return
|
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).
|
// The local file provider doesn't support presigned URLs for private files (i.e files not placed in /static).
|
||||||
async getPresignedDownloadUrl(
|
async getPresignedDownloadUrl(
|
||||||
file: FileTypes.ProviderGetFileDTO
|
file: FileTypes.ProviderGetFileDTO
|
||||||
@@ -96,10 +95,6 @@ export class LocalFileService extends AbstractFileProviderService {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isPrivate) {
|
|
||||||
return filePath
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.getUploadFileUrl(file.fileKey)
|
return this.getUploadFileUrl(file.fileKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user