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
+564
-343
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>
|
||||||
|
|||||||
+120
-104
@@ -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,112 +119,119 @@ 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
|
||||||
<Popover.Portal>
|
label={label}
|
||||||
<Popover.Content
|
value={displayValue}
|
||||||
data-name="date_filter_content"
|
onRemove={handleRemove}
|
||||||
align="start"
|
readonly={readonly}
|
||||||
sideOffset={8}
|
/>
|
||||||
collisionPadding={24}
|
{!readonly && (
|
||||||
className={clx(
|
<Popover.Portal>
|
||||||
"bg-ui-bg-base text-ui-fg-base shadow-elevation-flyout h-full max-h-[var(--radix-popper-available-height)] w-[300px] overflow-auto rounded-lg"
|
<Popover.Content
|
||||||
)}
|
data-name="date_filter_content"
|
||||||
onInteractOutside={(e) => {
|
align="start"
|
||||||
if (e.target instanceof HTMLElement) {
|
sideOffset={8}
|
||||||
if (
|
collisionPadding={24}
|
||||||
e.target.attributes.getNamedItem("data-name")?.value ===
|
className={clx(
|
||||||
"filters_menu_content"
|
"bg-ui-bg-base text-ui-fg-base shadow-elevation-flyout h-full max-h-[var(--radix-popper-available-height)] w-[300px] overflow-auto rounded-lg"
|
||||||
) {
|
)}
|
||||||
e.preventDefault()
|
onInteractOutside={(e) => {
|
||||||
|
if (e.target instanceof HTMLElement) {
|
||||||
|
if (
|
||||||
|
e.target.attributes.getNamedItem("data-name")?.value ===
|
||||||
|
"filters_menu_content"
|
||||||
|
) {
|
||||||
|
e.preventDefault()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}}
|
||||||
}}
|
>
|
||||||
>
|
<ul className="w-full p-1">
|
||||||
<ul className="w-full p-1">
|
{presets.map((preset) => {
|
||||||
{presets.map((preset) => {
|
const isSelected = selectedParams
|
||||||
const isSelected = selectedParams
|
.get()
|
||||||
.get()
|
.includes(JSON.stringify(preset.value))
|
||||||
.includes(JSON.stringify(preset.value))
|
return (
|
||||||
return (
|
<li key={preset.label}>
|
||||||
<li key={preset.label}>
|
<button
|
||||||
<button
|
className="bg-ui-bg-base hover:bg-ui-bg-base-hover focus-visible:bg-ui-bg-base-pressed text-ui-fg-base data-[disabled]:text-ui-fg-disabled txt-compact-small relative flex w-full cursor-pointer select-none items-center rounded-md px-2 py-1.5 outline-none transition-colors data-[disabled]:pointer-events-none"
|
||||||
className="bg-ui-bg-base hover:bg-ui-bg-base-hover focus-visible:bg-ui-bg-base-pressed text-ui-fg-base data-[disabled]:text-ui-fg-disabled txt-compact-small relative flex w-full cursor-pointer select-none items-center rounded-md px-2 py-1.5 outline-none transition-colors data-[disabled]:pointer-events-none"
|
type="button"
|
||||||
type="button"
|
onClick={() => {
|
||||||
onClick={() => {
|
handleSelectPreset(preset.value)
|
||||||
handleSelectPreset(preset.value)
|
}}
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={clx(
|
|
||||||
"transition-fg flex h-5 w-5 items-center justify-center",
|
|
||||||
{
|
|
||||||
"[&_svg]:invisible": !isSelected,
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
<EllipseMiniSolid />
|
<div
|
||||||
</div>
|
className={clx(
|
||||||
{preset.label}
|
"transition-fg flex h-5 w-5 items-center justify-center",
|
||||||
</button>
|
{
|
||||||
</li>
|
"[&_svg]:invisible": !isSelected,
|
||||||
)
|
}
|
||||||
})}
|
)}
|
||||||
<li>
|
>
|
||||||
<button
|
<EllipseMiniSolid />
|
||||||
className="bg-ui-bg-base hover:bg-ui-bg-base-hover focus-visible:bg-ui-bg-base-pressed text-ui-fg-base data-[disabled]:text-ui-fg-disabled txt-compact-small relative flex w-full cursor-pointer select-none items-center rounded-md px-2 py-1.5 outline-none transition-colors data-[disabled]:pointer-events-none"
|
</div>
|
||||||
type="button"
|
{preset.label}
|
||||||
onClick={handleSelectCustom}
|
</button>
|
||||||
>
|
</li>
|
||||||
<div
|
)
|
||||||
className={clx(
|
})}
|
||||||
"transition-fg flex h-5 w-5 items-center justify-center",
|
<li>
|
||||||
{
|
<button
|
||||||
"[&_svg]:invisible": !showCustom,
|
className="bg-ui-bg-base hover:bg-ui-bg-base-hover focus-visible:bg-ui-bg-base-pressed text-ui-fg-base data-[disabled]:text-ui-fg-disabled txt-compact-small relative flex w-full cursor-pointer select-none items-center rounded-md px-2 py-1.5 outline-none transition-colors data-[disabled]:pointer-events-none"
|
||||||
}
|
type="button"
|
||||||
)}
|
onClick={handleSelectCustom}
|
||||||
>
|
>
|
||||||
<EllipseMiniSolid />
|
<div
|
||||||
|
className={clx(
|
||||||
|
"transition-fg flex h-5 w-5 items-center justify-center",
|
||||||
|
{
|
||||||
|
"[&_svg]:invisible": !showCustom,
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<EllipseMiniSolid />
|
||||||
|
</div>
|
||||||
|
{t("filters.date.custom")}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
{showCustom && (
|
||||||
|
<div className="border-t px-1 pb-3 pt-1">
|
||||||
|
<div>
|
||||||
|
<div className="px-2 py-1">
|
||||||
|
<Text size="xsmall" leading="compact" weight="plus">
|
||||||
|
{t("filters.date.from")}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<div className="px-2 py-1">
|
||||||
|
<DatePicker
|
||||||
|
maxValue={customEndValue}
|
||||||
|
value={customStartValue}
|
||||||
|
onChange={(d) => handleCustomDateChange(d, "start")}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{t("filters.date.custom")}
|
<div>
|
||||||
</button>
|
<div className="px-2 py-1">
|
||||||
</li>
|
<Text size="xsmall" leading="compact" weight="plus">
|
||||||
</ul>
|
{t("filters.date.to")}
|
||||||
{showCustom && (
|
</Text>
|
||||||
<div className="border-t px-1 pb-3 pt-1">
|
</div>
|
||||||
<div>
|
<div className="px-2 py-1">
|
||||||
<div className="px-2 py-1">
|
<DatePicker
|
||||||
<Text size="xsmall" leading="compact" weight="plus">
|
minValue={customStartValue}
|
||||||
{t("filters.date.from")}
|
value={customEndValue || undefined}
|
||||||
</Text>
|
onChange={(d) => {
|
||||||
</div>
|
handleCustomDateChange(d, "end")
|
||||||
<div className="px-2 py-1">
|
}}
|
||||||
<DatePicker
|
/>
|
||||||
maxValue={customEndValue}
|
</div>
|
||||||
value={customStartValue}
|
|
||||||
onChange={(d) => handleCustomDateChange(d, "start")}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
)}
|
||||||
<div className="px-2 py-1">
|
</Popover.Content>
|
||||||
<Text size="xsmall" leading="compact" weight="plus">
|
</Popover.Portal>
|
||||||
{t("filters.date.to")}
|
)}
|
||||||
</Text>
|
|
||||||
</div>
|
|
||||||
<div className="px-2 py-1">
|
|
||||||
<DatePicker
|
|
||||||
minValue={customStartValue}
|
|
||||||
value={customEndValue || undefined}
|
|
||||||
onChange={(d) => {
|
|
||||||
handleCustomDateChange(d, "end")
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Popover.Content>
|
|
||||||
</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}
|
||||||
|
|||||||
+107
-99
@@ -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,104 +137,107 @@ export const NumberFilter = ({
|
|||||||
label={label}
|
label={label}
|
||||||
value={currentValue}
|
value={currentValue}
|
||||||
onRemove={handleRemove}
|
onRemove={handleRemove}
|
||||||
|
readonly={readonly}
|
||||||
/>
|
/>
|
||||||
<Popover.Portal>
|
{!readonly && (
|
||||||
<Popover.Content
|
<Popover.Portal>
|
||||||
data-name="number_filter_content"
|
<Popover.Content
|
||||||
align="start"
|
data-name="number_filter_content"
|
||||||
sideOffset={8}
|
align="start"
|
||||||
collisionPadding={24}
|
sideOffset={8}
|
||||||
className={clx(
|
collisionPadding={24}
|
||||||
"bg-ui-bg-base text-ui-fg-base shadow-elevation-flyout max-h-[var(--radix-popper-available-height)] w-[300px] divide-y overflow-y-auto rounded-lg outline-none"
|
className={clx(
|
||||||
)}
|
"bg-ui-bg-base text-ui-fg-base shadow-elevation-flyout max-h-[var(--radix-popper-available-height)] w-[300px] divide-y overflow-y-auto rounded-lg outline-none"
|
||||||
onInteractOutside={(e) => {
|
|
||||||
if (e.target instanceof HTMLElement) {
|
|
||||||
if (
|
|
||||||
e.target.attributes.getNamedItem("data-name")?.value ===
|
|
||||||
"filters_menu_content"
|
|
||||||
) {
|
|
||||||
e.preventDefault()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="p-1">
|
|
||||||
<RadioGroup.Root
|
|
||||||
value={operator}
|
|
||||||
onValueChange={(val) => setOperator(val as Comparison)}
|
|
||||||
className="flex flex-col items-start"
|
|
||||||
orientation="vertical"
|
|
||||||
autoFocus
|
|
||||||
>
|
|
||||||
{operators.map((o) => (
|
|
||||||
<RadioGroup.Item
|
|
||||||
key={o.operator}
|
|
||||||
value={o.operator}
|
|
||||||
className="txt-compact-small hover:bg-ui-bg-base-hover focus-visible:bg-ui-bg-base-hover active:bg-ui-bg-base-pressed transition-fg grid w-full grid-cols-[20px_1fr] gap-2 rounded-[4px] px-2 py-1.5 text-left outline-none"
|
|
||||||
>
|
|
||||||
<div className="size-5">
|
|
||||||
<RadioGroup.Indicator>
|
|
||||||
<EllipseMiniSolid />
|
|
||||||
</RadioGroup.Indicator>
|
|
||||||
</div>
|
|
||||||
<span className="w-full">{o.label}</span>
|
|
||||||
</RadioGroup.Item>
|
|
||||||
))}
|
|
||||||
</RadioGroup.Root>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
{operator === "range" ? (
|
|
||||||
<div className="px-1 pb-3 pt-1" key="range">
|
|
||||||
<div className="px-2 py-1.5">
|
|
||||||
<Label size="xsmall" weight="plus" htmlFor={GT_KEY}>
|
|
||||||
{t("filters.compare.greaterThan")}
|
|
||||||
</Label>
|
|
||||||
</div>
|
|
||||||
<div className="px-2 py-0.5">
|
|
||||||
<Input
|
|
||||||
name={GT_KEY}
|
|
||||||
size="small"
|
|
||||||
type="number"
|
|
||||||
defaultValue={getValue(currentValue, "gt")}
|
|
||||||
onChange={(e) => debouncedOnChange(e, "gt")}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="px-2 py-1.5">
|
|
||||||
<Label size="xsmall" weight="plus" htmlFor={LT_KEY}>
|
|
||||||
{t("filters.compare.lessThan")}
|
|
||||||
</Label>
|
|
||||||
</div>
|
|
||||||
<div className="px-2 py-0.5">
|
|
||||||
<Input
|
|
||||||
name={LT_KEY}
|
|
||||||
size="small"
|
|
||||||
type="number"
|
|
||||||
defaultValue={getValue(currentValue, "lt")}
|
|
||||||
onChange={(e) => debouncedOnChange(e, "lt")}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="px-1 pb-3 pt-1" key="exact">
|
|
||||||
<div className="px-2 py-1.5">
|
|
||||||
<Label size="xsmall" weight="plus" htmlFor={EQ_KEY}>
|
|
||||||
{label}
|
|
||||||
</Label>
|
|
||||||
</div>
|
|
||||||
<div className="px-2 py-0.5">
|
|
||||||
<Input
|
|
||||||
name={EQ_KEY}
|
|
||||||
size="small"
|
|
||||||
type="number"
|
|
||||||
defaultValue={getValue(currentValue, "eq")}
|
|
||||||
onChange={(e) => debouncedOnChange(e, "eq")}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
onInteractOutside={(e) => {
|
||||||
</Popover.Content>
|
if (e.target instanceof HTMLElement) {
|
||||||
</Popover.Portal>
|
if (
|
||||||
|
e.target.attributes.getNamedItem("data-name")?.value ===
|
||||||
|
"filters_menu_content"
|
||||||
|
) {
|
||||||
|
e.preventDefault()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="p-1">
|
||||||
|
<RadioGroup.Root
|
||||||
|
value={operator}
|
||||||
|
onValueChange={(val) => setOperator(val as Comparison)}
|
||||||
|
className="flex flex-col items-start"
|
||||||
|
orientation="vertical"
|
||||||
|
autoFocus
|
||||||
|
>
|
||||||
|
{operators.map((o) => (
|
||||||
|
<RadioGroup.Item
|
||||||
|
key={o.operator}
|
||||||
|
value={o.operator}
|
||||||
|
className="txt-compact-small hover:bg-ui-bg-base-hover focus-visible:bg-ui-bg-base-hover active:bg-ui-bg-base-pressed transition-fg grid w-full grid-cols-[20px_1fr] gap-2 rounded-[4px] px-2 py-1.5 text-left outline-none"
|
||||||
|
>
|
||||||
|
<div className="size-5">
|
||||||
|
<RadioGroup.Indicator>
|
||||||
|
<EllipseMiniSolid />
|
||||||
|
</RadioGroup.Indicator>
|
||||||
|
</div>
|
||||||
|
<span className="w-full">{o.label}</span>
|
||||||
|
</RadioGroup.Item>
|
||||||
|
))}
|
||||||
|
</RadioGroup.Root>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{operator === "range" ? (
|
||||||
|
<div className="px-1 pb-3 pt-1" key="range">
|
||||||
|
<div className="px-2 py-1.5">
|
||||||
|
<Label size="xsmall" weight="plus" htmlFor={GT_KEY}>
|
||||||
|
{t("filters.compare.greaterThan")}
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
<div className="px-2 py-0.5">
|
||||||
|
<Input
|
||||||
|
name={GT_KEY}
|
||||||
|
size="small"
|
||||||
|
type="number"
|
||||||
|
defaultValue={getValue(currentValue, "gt")}
|
||||||
|
onChange={(e) => debouncedOnChange(e, "gt")}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="px-2 py-1.5">
|
||||||
|
<Label size="xsmall" weight="plus" htmlFor={LT_KEY}>
|
||||||
|
{t("filters.compare.lessThan")}
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
<div className="px-2 py-0.5">
|
||||||
|
<Input
|
||||||
|
name={LT_KEY}
|
||||||
|
size="small"
|
||||||
|
type="number"
|
||||||
|
defaultValue={getValue(currentValue, "lt")}
|
||||||
|
onChange={(e) => debouncedOnChange(e, "lt")}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="px-1 pb-3 pt-1" key="exact">
|
||||||
|
<div className="px-2 py-1.5">
|
||||||
|
<Label size="xsmall" weight="plus" htmlFor={EQ_KEY}>
|
||||||
|
{label}
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
<div className="px-2 py-0.5">
|
||||||
|
<Input
|
||||||
|
name={EQ_KEY}
|
||||||
|
size="small"
|
||||||
|
type="number"
|
||||||
|
defaultValue={getValue(currentValue, "eq")}
|
||||||
|
onChange={(e) => debouncedOnChange(e, "eq")}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Popover.Content>
|
||||||
|
</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}
|
||||||
|
|||||||
+94
-85
@@ -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,103 +82,107 @@ 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}
|
||||||
/>
|
/>
|
||||||
<Popover.Portal>
|
{!readonly && (
|
||||||
<Popover.Content
|
<Popover.Portal>
|
||||||
hideWhenDetached
|
<Popover.Content
|
||||||
align="start"
|
hideWhenDetached
|
||||||
sideOffset={8}
|
align="start"
|
||||||
collisionPadding={8}
|
sideOffset={8}
|
||||||
className={clx(
|
collisionPadding={8}
|
||||||
"bg-ui-bg-base text-ui-fg-base shadow-elevation-flyout z-[1] h-full max-h-[200px] w-[300px] overflow-hidden rounded-lg outline-none"
|
className={clx(
|
||||||
)}
|
"bg-ui-bg-base text-ui-fg-base shadow-elevation-flyout z-[1] h-full max-h-[200px] w-[300px] overflow-hidden rounded-lg outline-none"
|
||||||
onInteractOutside={(e) => {
|
)}
|
||||||
if (e.target instanceof HTMLElement) {
|
onInteractOutside={(e) => {
|
||||||
if (
|
if (e.target instanceof HTMLElement) {
|
||||||
e.target.attributes.getNamedItem("data-name")?.value ===
|
if (
|
||||||
"filters_menu_content"
|
e.target.attributes.getNamedItem("data-name")?.value ===
|
||||||
) {
|
"filters_menu_content"
|
||||||
e.preventDefault()
|
) {
|
||||||
e.stopPropagation()
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}}
|
||||||
}}
|
>
|
||||||
>
|
<Command className="h-full">
|
||||||
<Command className="h-full">
|
{searchable && (
|
||||||
{searchable && (
|
<div className="border-b p-1">
|
||||||
<div className="border-b p-1">
|
<div className="grid grid-cols-[1fr_20px] gap-x-2 rounded-md px-2 py-1">
|
||||||
<div className="grid grid-cols-[1fr_20px] gap-x-2 rounded-md px-2 py-1">
|
<Command.Input
|
||||||
<Command.Input
|
ref={setSearchRef}
|
||||||
ref={setSearchRef}
|
value={search}
|
||||||
value={search}
|
onValueChange={setSearch}
|
||||||
onValueChange={setSearch}
|
className="txt-compact-small placeholder:text-ui-fg-muted bg-transparent outline-none"
|
||||||
className="txt-compact-small placeholder:text-ui-fg-muted bg-transparent outline-none"
|
placeholder="Search"
|
||||||
placeholder="Search"
|
/>
|
||||||
/>
|
<div className="flex h-5 w-5 items-center justify-center">
|
||||||
<div className="flex h-5 w-5 items-center justify-center">
|
<button
|
||||||
<button
|
disabled={!search}
|
||||||
disabled={!search}
|
onClick={handleClearSearch}
|
||||||
onClick={handleClearSearch}
|
className={clx(
|
||||||
className={clx(
|
"transition-fg text-ui-fg-muted focus-visible:bg-ui-bg-base-pressed rounded-md outline-none",
|
||||||
"transition-fg text-ui-fg-muted focus-visible:bg-ui-bg-base-pressed rounded-md outline-none",
|
{
|
||||||
{
|
invisible: !search,
|
||||||
invisible: !search,
|
}
|
||||||
}
|
)}
|
||||||
)}
|
>
|
||||||
>
|
<XMarkMini />
|
||||||
<XMarkMini />
|
</button>
|
||||||
</button>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
<Command.Empty className="txt-compact-small flex items-center justify-center p-1">
|
||||||
<Command.Empty className="txt-compact-small flex items-center justify-center p-1">
|
<span className="w-full px-2 py-1 text-center">
|
||||||
<span className="w-full px-2 py-1 text-center">
|
{t("general.noResultsTitle")}
|
||||||
{t("general.noResultsTitle")}
|
</span>
|
||||||
</span>
|
</Command.Empty>
|
||||||
</Command.Empty>
|
<Command.List className="h-full max-h-[163px] min-h-[0] overflow-auto p-1 outline-none">
|
||||||
<Command.List className="h-full max-h-[163px] min-h-[0] overflow-auto p-1 outline-none">
|
{options.map((option) => {
|
||||||
{options.map((option) => {
|
const isSelected = selectedParams
|
||||||
const isSelected = selectedParams
|
.get()
|
||||||
.get()
|
.includes(String(option.value))
|
||||||
.includes(String(option.value))
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Command.Item
|
<Command.Item
|
||||||
key={String(option.value)}
|
key={String(option.value)}
|
||||||
className="bg-ui-bg-base hover:bg-ui-bg-base-hover aria-selected:bg-ui-bg-base-pressed focus-visible:bg-ui-bg-base-pressed text-ui-fg-base data-[disabled]:text-ui-fg-disabled txt-compact-small relative flex cursor-pointer select-none items-center gap-x-2 rounded-md px-2 py-1.5 outline-none transition-colors data-[disabled]:pointer-events-none"
|
className="bg-ui-bg-base hover:bg-ui-bg-base-hover aria-selected:bg-ui-bg-base-pressed focus-visible:bg-ui-bg-base-pressed text-ui-fg-base data-[disabled]:text-ui-fg-disabled txt-compact-small relative flex cursor-pointer select-none items-center gap-x-2 rounded-md px-2 py-1.5 outline-none transition-colors data-[disabled]:pointer-events-none"
|
||||||
value={option.label}
|
value={option.label}
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
handleSelect(option.value)
|
handleSelect(option.value)
|
||||||
}}
|
}}
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={clx(
|
|
||||||
"transition-fg flex h-5 w-5 items-center justify-center",
|
|
||||||
{
|
|
||||||
"[&_svg]:invisible": !isSelected,
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
{multiple ? <CheckMini /> : <EllipseMiniSolid />}
|
<div
|
||||||
</div>
|
className={clx(
|
||||||
{option.label}
|
"transition-fg flex h-5 w-5 items-center justify-center",
|
||||||
</Command.Item>
|
{
|
||||||
)
|
"[&_svg]:invisible": !isSelected,
|
||||||
})}
|
}
|
||||||
</Command.List>
|
)}
|
||||||
</Command>
|
>
|
||||||
</Popover.Content>
|
{multiple ? <CheckMini /> : <EllipseMiniSolid />}
|
||||||
</Popover.Portal>
|
</div>
|
||||||
|
{option.label}
|
||||||
|
</Command.Item>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</Command.List>
|
||||||
|
</Command>
|
||||||
|
</Popover.Content>
|
||||||
|
</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}
|
||||||
|
|||||||
+52
-40
@@ -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,45 +68,52 @@ 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
|
||||||
<Popover.Portal>
|
label={label}
|
||||||
<Popover.Content
|
value={query?.[0]}
|
||||||
hideWhenDetached
|
onRemove={handleRemove}
|
||||||
align="start"
|
readonly={readonly}
|
||||||
sideOffset={8}
|
/>
|
||||||
collisionPadding={8}
|
{!readonly && (
|
||||||
className={clx(
|
<Popover.Portal>
|
||||||
"bg-ui-bg-base text-ui-fg-base shadow-elevation-flyout z-[1] h-full max-h-[200px] w-[300px] overflow-hidden rounded-lg outline-none"
|
<Popover.Content
|
||||||
)}
|
hideWhenDetached
|
||||||
onInteractOutside={(e) => {
|
align="start"
|
||||||
if (e.target instanceof HTMLElement) {
|
sideOffset={8}
|
||||||
if (
|
collisionPadding={8}
|
||||||
e.target.attributes.getNamedItem("data-name")?.value ===
|
className={clx(
|
||||||
"filters_menu_content"
|
"bg-ui-bg-base text-ui-fg-base shadow-elevation-flyout z-[1] h-full max-h-[200px] w-[300px] overflow-hidden rounded-lg outline-none"
|
||||||
) {
|
)}
|
||||||
e.preventDefault()
|
onInteractOutside={(e) => {
|
||||||
e.stopPropagation()
|
if (e.target instanceof HTMLElement) {
|
||||||
|
if (
|
||||||
|
e.target.attributes.getNamedItem("data-name")?.value ===
|
||||||
|
"filters_menu_content"
|
||||||
|
) {
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}}
|
||||||
}}
|
>
|
||||||
>
|
<div className="px-1 pb-3 pt-1">
|
||||||
<div className="px-1 pb-3 pt-1">
|
<div className="px-2 py-1.5">
|
||||||
<div className="px-2 py-1.5">
|
<Label size="xsmall" weight="plus" htmlFor={key}>
|
||||||
<Label size="xsmall" weight="plus" htmlFor={key}>
|
{label}
|
||||||
{label}
|
</Label>
|
||||||
</Label>
|
</div>
|
||||||
|
<div className="px-2 py-0.5">
|
||||||
|
<Input
|
||||||
|
name={key}
|
||||||
|
size="small"
|
||||||
|
defaultValue={query?.[0] || undefined}
|
||||||
|
onChange={debouncedOnChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="px-2 py-0.5">
|
</Popover.Content>
|
||||||
<Input
|
</Popover.Portal>
|
||||||
name={key}
|
)}
|
||||||
size="small"
|
|
||||||
defaultValue={query?.[0] || undefined}
|
|
||||||
onChange={debouncedOnChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Popover.Content>
|
|
||||||
</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>
|
||||||
|
)
|
||||||
|
}
|
||||||
+13
-4
@@ -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,9 +60,17 @@ 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>
|
||||||
<Button size="small" variant="secondary" asChild>
|
<div className="flex items-center justify-center gap-x-2">
|
||||||
<Link to="create">{t("actions.create")}</Link>
|
<Button size="small" variant="secondary" asChild>
|
||||||
</Button>
|
<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>
|
||||||
|
<Link to="create">{t("actions.create")}</Link>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<DataTable
|
<DataTable
|
||||||
table={table}
|
table={table}
|
||||||
|
|||||||
@@ -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