feat(dashboard): Initial pricing domain (#6996)
**What** - Sets up the initial work for Pricing domain - Fixes Store domain **Todo in follow up PR** - Translations - Add status when creating the Price List and allow updating it - Improve DataGrid component - Add missing functionality once backend support is added (customer_groups, region prices and update prices) CLOSES CORE-1931
This commit is contained in:
1 parent
6cc9a5e469
commit
0f05790ae4
81 files changed
+2628
-745
No files matched your search
@@ -38,12 +38,12 @@
|
|||||||
"qs": "^6.12.0",
|
"qs": "^6.12.0",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-country-flag": "^3.1.0",
|
"react-country-flag": "^3.1.0",
|
||||||
|
"react-currency-input-field": "^3.6.11",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
"react-focus-lock": "^2.11.1",
|
|
||||||
"react-hook-form": "7.49.1",
|
"react-hook-form": "7.49.1",
|
||||||
"react-i18next": "13.5.0",
|
"react-i18next": "13.5.0",
|
||||||
"react-jwt": "^1.2.0",
|
"react-jwt": "^1.2.0",
|
||||||
"react-resizable-panels": "^2.0.9",
|
"react-resizable-panels": "^2.0.16",
|
||||||
"react-router-dom": "6.20.1",
|
"react-router-dom": "6.20.1",
|
||||||
"zod": "3.22.4"
|
"zod": "3.22.4"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
export enum GridCellType {
|
||||||
|
VOID = "void",
|
||||||
|
READONLY = "readonly",
|
||||||
|
EDITABLE = "editable",
|
||||||
|
OVERLAY = "overlay",
|
||||||
|
}
|
||||||
|
|
||||||
|
export const NON_INTERACTIVE_CELL_TYPES = [
|
||||||
|
GridCellType.VOID,
|
||||||
|
GridCellType.READONLY,
|
||||||
|
]
|
||||||
@@ -1,203 +0,0 @@
|
|||||||
import { zodResolver } from "@hookform/resolvers/zod"
|
|
||||||
import { Product, ProductVariant } from "@medusajs/medusa"
|
|
||||||
import { Button, Container } from "@medusajs/ui"
|
|
||||||
import { ColumnDef, createColumnHelper } from "@tanstack/react-table"
|
|
||||||
import { useAdminProducts } from "medusa-react"
|
|
||||||
import { useEffect, useMemo } from "react"
|
|
||||||
import { useForm } from "react-hook-form"
|
|
||||||
import { useTranslation } from "react-i18next"
|
|
||||||
import * as zod from "zod"
|
|
||||||
|
|
||||||
import { Thumbnail } from "../common/thumbnail"
|
|
||||||
import { DataGrid } from "./data-grid"
|
|
||||||
import { TextField } from "./grid-fields/common/text-field"
|
|
||||||
import { DisplayField } from "./grid-fields/non-interactive/display-field"
|
|
||||||
import { DataGridMeta } from "./types"
|
|
||||||
|
|
||||||
const ProductEditorSchema = zod.object({
|
|
||||||
products: zod.record(
|
|
||||||
zod.object({
|
|
||||||
variants: zod.record(
|
|
||||||
zod.object({
|
|
||||||
title: zod.string(),
|
|
||||||
sku: zod.string(),
|
|
||||||
ean: zod.string().optional(),
|
|
||||||
upc: zod.string().optional(),
|
|
||||||
})
|
|
||||||
),
|
|
||||||
})
|
|
||||||
),
|
|
||||||
})
|
|
||||||
|
|
||||||
type ProductEditorSchemaType = zod.infer<typeof ProductEditorSchema>
|
|
||||||
type VariantObject = ProductEditorSchemaType["products"]["id"]["variants"]
|
|
||||||
|
|
||||||
const getVariantRows = (row: Product | ProductVariant) => {
|
|
||||||
if ("variants" in row) {
|
|
||||||
return row.variants
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Demo component to test the data grid.
|
|
||||||
*
|
|
||||||
* To be deleted when the feature is implemented.
|
|
||||||
*/
|
|
||||||
export const DataGridDemo = () => {
|
|
||||||
const form = useForm<ProductEditorSchemaType>({
|
|
||||||
resolver: zodResolver(ProductEditorSchema),
|
|
||||||
})
|
|
||||||
|
|
||||||
const { setValue } = form
|
|
||||||
|
|
||||||
const { products, isLoading } = useAdminProducts(
|
|
||||||
{
|
|
||||||
expand: "variants,variants.prices",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
keepPreviousData: true,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isLoading && products) {
|
|
||||||
products.forEach((product) => {
|
|
||||||
setValue(`products.${product.id}.variants`, {
|
|
||||||
...product.variants.reduce((variants, variant) => {
|
|
||||||
variants[variant.id!] = {
|
|
||||||
title: variant.title || "",
|
|
||||||
sku: variant.sku || "",
|
|
||||||
ean: variant.ean || "",
|
|
||||||
upc: variant.upc || "",
|
|
||||||
}
|
|
||||||
return variants
|
|
||||||
}, {} as VariantObject),
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}, [products, isLoading, setValue])
|
|
||||||
|
|
||||||
const columns = useColumns()
|
|
||||||
|
|
||||||
const initializing = isLoading || !products
|
|
||||||
|
|
||||||
const handleSubmit = form.handleSubmit((data) => {
|
|
||||||
console.log("submitting", data)
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Container className="overflow-hidden p-0">
|
|
||||||
<DataGrid
|
|
||||||
isLoading={initializing}
|
|
||||||
data={products as Product[]}
|
|
||||||
columns={columns}
|
|
||||||
state={form}
|
|
||||||
getSubRows={getVariantRows}
|
|
||||||
/>
|
|
||||||
<div className="flex items-center justify-end gap-x-2 border-t p-4">
|
|
||||||
<Button size="small" onClick={handleSubmit}>
|
|
||||||
Submit
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</Container>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper function to determine if a row is a product or a variant.
|
|
||||||
*/
|
|
||||||
const isProduct = (row: Product | ProductVariant): row is Product => {
|
|
||||||
return "variants" in row
|
|
||||||
}
|
|
||||||
|
|
||||||
const columnHelper = createColumnHelper<Product | ProductVariant>()
|
|
||||||
|
|
||||||
const useColumns = () => {
|
|
||||||
const { t } = useTranslation()
|
|
||||||
|
|
||||||
const colDefs: ColumnDef<Product | ProductVariant>[] = useMemo(() => {
|
|
||||||
return [
|
|
||||||
columnHelper.display({
|
|
||||||
id: t("fields.title"),
|
|
||||||
header: "Title",
|
|
||||||
cell: ({ row, table }) => {
|
|
||||||
const entity = row.original
|
|
||||||
|
|
||||||
if (isProduct(entity)) {
|
|
||||||
return (
|
|
||||||
<DisplayField>
|
|
||||||
<div className="flex h-full w-full items-center gap-x-2 overflow-hidden">
|
|
||||||
<Thumbnail src={entity.thumbnail} />
|
|
||||||
<span className="truncate">{entity.title}</span>
|
|
||||||
</div>
|
|
||||||
</DisplayField>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TextField
|
|
||||||
meta={table.options.meta as DataGridMeta<ProductEditorSchemaType>}
|
|
||||||
field={`products.${entity.product_id}.variants.${entity.id}.title`}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
size: 350,
|
|
||||||
}),
|
|
||||||
columnHelper.accessor("sku", {
|
|
||||||
header: t("fields.sku"),
|
|
||||||
cell: ({ row, table }) => {
|
|
||||||
const entity = row.original
|
|
||||||
|
|
||||||
if (isProduct(entity)) {
|
|
||||||
return <DisplayField />
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TextField
|
|
||||||
meta={table.options.meta as DataGridMeta<ProductEditorSchemaType>}
|
|
||||||
field={`products.${entity.product_id}.variants.${entity.id}.sku`}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
columnHelper.accessor("ean", {
|
|
||||||
header: "EAN",
|
|
||||||
cell: ({ row, table }) => {
|
|
||||||
const entity = row.original
|
|
||||||
|
|
||||||
if (isProduct(entity)) {
|
|
||||||
return <DisplayField />
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TextField
|
|
||||||
meta={table.options.meta as DataGridMeta<ProductEditorSchemaType>}
|
|
||||||
field={`products.${entity.product_id}.variants.${entity.id}.ean`}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
columnHelper.accessor("upc", {
|
|
||||||
header: "UPC",
|
|
||||||
cell: ({ row, table }) => {
|
|
||||||
const entity = row.original
|
|
||||||
|
|
||||||
if (isProduct(entity)) {
|
|
||||||
return <DisplayField />
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TextField
|
|
||||||
meta={table.options.meta as DataGridMeta<ProductEditorSchemaType>}
|
|
||||||
field={`products.${entity.product_id}.variants.${entity.id}.upc`}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
]
|
|
||||||
}, [t])
|
|
||||||
|
|
||||||
return colDefs
|
|
||||||
}
|
|
||||||
+35
-15
@@ -20,6 +20,7 @@ import {
|
|||||||
Command,
|
Command,
|
||||||
useCommandHistory,
|
useCommandHistory,
|
||||||
} from "../../../../hooks/use-command-history"
|
} from "../../../../hooks/use-command-history"
|
||||||
|
import { GridCellType, NON_INTERACTIVE_CELL_TYPES } from "../../constants"
|
||||||
|
|
||||||
type FieldCoordinates = {
|
type FieldCoordinates = {
|
||||||
column: number
|
column: number
|
||||||
@@ -28,9 +29,9 @@ type FieldCoordinates = {
|
|||||||
|
|
||||||
export interface DataGridRootProps<
|
export interface DataGridRootProps<
|
||||||
TData,
|
TData,
|
||||||
TFieldValues extends FieldValues = FieldValues,
|
TFieldValues extends FieldValues = FieldValues
|
||||||
> {
|
> {
|
||||||
data: TData[]
|
data?: TData[]
|
||||||
columns: ColumnDef<TData>[]
|
columns: ColumnDef<TData>[]
|
||||||
state: UseFormReturn<TFieldValues>
|
state: UseFormReturn<TFieldValues>
|
||||||
getSubRows: (row: TData) => TData[] | undefined
|
getSubRows: (row: TData) => TData[] | undefined
|
||||||
@@ -40,9 +41,9 @@ const ROW_HEIGHT = 40
|
|||||||
|
|
||||||
export const DataGridRoot = <
|
export const DataGridRoot = <
|
||||||
TData,
|
TData,
|
||||||
TFieldValues extends FieldValues = FieldValues,
|
TFieldValues extends FieldValues = FieldValues
|
||||||
>({
|
>({
|
||||||
data,
|
data = [],
|
||||||
columns,
|
columns,
|
||||||
state,
|
state,
|
||||||
getSubRows,
|
getSubRows,
|
||||||
@@ -99,17 +100,35 @@ export const DataGridRoot = <
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleBlurAnchor = () => {
|
||||||
|
const activeElement = document.activeElement
|
||||||
|
|
||||||
|
if (anchor && activeElement instanceof HTMLElement) {
|
||||||
|
activeElement.blur()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const isNonInteractive = (element: HTMLElement) => {
|
||||||
|
const type = element.getAttribute("data-cell-type")
|
||||||
|
|
||||||
|
if (!type) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return NON_INTERACTIVE_CELL_TYPES.includes(type as GridCellType)
|
||||||
|
}
|
||||||
|
|
||||||
const handleMouseDown = (e: ReactMouseEvent<HTMLTableCellElement>) => {
|
const handleMouseDown = (e: ReactMouseEvent<HTMLTableCellElement>) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
const target = e.target
|
const target = e.target
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if the click was on a presentation element.
|
* Check if the click was on a presentation element.
|
||||||
* If so, we don't want to set the anchor.
|
* If so, we don't want to set the anchor.
|
||||||
*/
|
*/
|
||||||
if (
|
if (target instanceof HTMLElement && isNonInteractive(target)) {
|
||||||
target instanceof HTMLElement &&
|
|
||||||
target.querySelector("[data-role=presentation]")
|
|
||||||
) {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,6 +140,9 @@ export const DataGridRoot = <
|
|||||||
if (e.detail === 2 || isAnchor) {
|
if (e.detail === 2 || isAnchor) {
|
||||||
handleFocusInner(e.currentTarget)
|
handleFocusInner(e.currentTarget)
|
||||||
return
|
return
|
||||||
|
} else {
|
||||||
|
// reset focus so the previous cell doesn't keep the focus
|
||||||
|
handleBlurAnchor()
|
||||||
}
|
}
|
||||||
|
|
||||||
const coordinates: FieldCoordinates = {
|
const coordinates: FieldCoordinates = {
|
||||||
@@ -157,10 +179,7 @@ export const DataGridRoot = <
|
|||||||
* Check if the click was on a presentation element.
|
* Check if the click was on a presentation element.
|
||||||
* If so, we don't want to add it to the selection.
|
* If so, we don't want to add it to the selection.
|
||||||
*/
|
*/
|
||||||
if (
|
if (target instanceof HTMLElement && isNonInteractive(target)) {
|
||||||
target instanceof HTMLElement &&
|
|
||||||
target.querySelector("[data-role=presentation]")
|
|
||||||
) {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -487,7 +506,7 @@ export const DataGridRoot = <
|
|||||||
}, [handleMouseUp, handleCopy, handlePaste, handleCommandHistory])
|
}, [handleMouseUp, handleCopy, handlePaste, handleCommandHistory])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="size-full overflow-hidden">
|
<div className="bg-ui-bg-subtle size-full overflow-hidden">
|
||||||
<div
|
<div
|
||||||
ref={tableContainerRef}
|
ref={tableContainerRef}
|
||||||
style={{
|
style={{
|
||||||
@@ -559,10 +578,11 @@ export const DataGridRoot = <
|
|||||||
data-column-index={index}
|
data-column-index={index}
|
||||||
className={clx(
|
className={clx(
|
||||||
"bg-ui-bg-base has-[[data-role='presentation']]:bg-ui-bg-subtle relative flex items-center border-b border-r p-0 outline-none",
|
"bg-ui-bg-base has-[[data-role='presentation']]:bg-ui-bg-subtle relative flex items-center border-b border-r p-0 outline-none",
|
||||||
"after:transition-fg after:border-ui-fg-interactive after:invisible after:absolute after:-bottom-px after:-left-px after:-right-px after:-top-px after:box-border after:border-[2px] after:content-['']",
|
"after:transition-fg after:border-ui-fg-interactive after:pointer-events-none after:invisible after:absolute after:-bottom-px after:-left-px after:-right-px after:-top-px after:box-border after:border-[2px] after:content-['']",
|
||||||
{
|
{
|
||||||
"after:visible": isAnchor,
|
"after:visible": isAnchor,
|
||||||
"bg-ui-bg-highlight": isSelected,
|
"bg-ui-bg-highlight focus-within:bg-ui-bg-base":
|
||||||
|
isSelected || isAnchor,
|
||||||
"bg-ui-bg-base-hover": isDragTarget,
|
"bg-ui-bg-base-hover": isDragTarget,
|
||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
|
|||||||
+5
-5
@@ -1,14 +1,14 @@
|
|||||||
import { Select } from "@medusajs/ui"
|
import { Select } from "@medusajs/ui"
|
||||||
import { Controller, FieldValues } from "react-hook-form"
|
import { Controller, FieldValues } from "react-hook-form"
|
||||||
import { FieldProps } from "../../../types"
|
import { CellProps } from "../../../types"
|
||||||
|
|
||||||
interface BooleanFieldProps<TFieldValues extends FieldValues = any>
|
interface BooleanCellProps<TFieldValues extends FieldValues = any>
|
||||||
extends FieldProps<TFieldValues> {}
|
extends CellProps<TFieldValues> {}
|
||||||
|
|
||||||
export const BooleanField = <TFieldValues extends FieldValues = any>({
|
export const BooleanCell = <TFieldValues extends FieldValues = any>({
|
||||||
field,
|
field,
|
||||||
meta,
|
meta,
|
||||||
}: BooleanFieldProps<TFieldValues>) => {
|
}: BooleanCellProps<TFieldValues>) => {
|
||||||
const { control } = meta
|
const { control } = meta
|
||||||
|
|
||||||
return (
|
return (
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export * from "./boolean-cell"
|
||||||
+57
@@ -0,0 +1,57 @@
|
|||||||
|
import { CurrencyDTO } from "@medusajs/types"
|
||||||
|
import { useRef } from "react"
|
||||||
|
import Primitive from "react-currency-input-field"
|
||||||
|
import { Controller, FieldValues } from "react-hook-form"
|
||||||
|
|
||||||
|
import { GridCellType } from "../../../constants"
|
||||||
|
import { CellProps } from "../../../types"
|
||||||
|
|
||||||
|
interface CurrencyCellProps<TFieldValues extends FieldValues = any>
|
||||||
|
extends CellProps<TFieldValues> {
|
||||||
|
currency: CurrencyDTO
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CurrencyCell = ({ currency, field, meta }: CurrencyCellProps) => {
|
||||||
|
const symbolRef = useRef<HTMLSpanElement>(null)
|
||||||
|
// @ts-ignore - Type is wrong
|
||||||
|
const decimalScale = currency.decimal_digits
|
||||||
|
|
||||||
|
const { control } = meta
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name={field}
|
||||||
|
render={({ field: { onChange, ...rest } }) => {
|
||||||
|
return (
|
||||||
|
<div className="relative size-full">
|
||||||
|
<span
|
||||||
|
ref={symbolRef}
|
||||||
|
role="presentation"
|
||||||
|
className="text-ui-fg-muted txt-compact-small pointer-events-none absolute left-0 top-0 select-none py-2.5 pl-4"
|
||||||
|
>
|
||||||
|
{currency.symbol_native}
|
||||||
|
</span>
|
||||||
|
<Primitive
|
||||||
|
data-input-field="true"
|
||||||
|
data-field-id={field}
|
||||||
|
data-cell-type={GridCellType.EDITABLE}
|
||||||
|
className="size-full bg-transparent py-2.5 pr-4 text-right outline-none"
|
||||||
|
style={{
|
||||||
|
paddingLeft: symbolRef.current?.offsetWidth
|
||||||
|
? `${symbolRef.current.offsetWidth + 8}px`
|
||||||
|
: "16px",
|
||||||
|
}}
|
||||||
|
decimalScale={decimalScale}
|
||||||
|
allowDecimals={decimalScale > 0}
|
||||||
|
onValueChange={(_value, _name, values) => {
|
||||||
|
onChange(values?.value)
|
||||||
|
}}
|
||||||
|
{...rest}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export * from "./currency-cell"
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export * from "./readonly-cell"
|
||||||
+14
@@ -0,0 +1,14 @@
|
|||||||
|
import { PropsWithChildren } from "react"
|
||||||
|
import { GridCellType } from "../../../constants"
|
||||||
|
|
||||||
|
export const ReadonlyCell = ({ children }: PropsWithChildren) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
role="cell"
|
||||||
|
data-cell-type={GridCellType.READONLY}
|
||||||
|
className="bg-ui-bg-base size-full cursor-not-allowed px-4 py-2.5"
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from "./text-cell"
|
||||||
+5
-5
@@ -1,13 +1,13 @@
|
|||||||
import { FieldValues } from "react-hook-form"
|
import { FieldValues } from "react-hook-form"
|
||||||
import { FieldProps } from "../../../types"
|
import { CellProps } from "../../../types"
|
||||||
|
|
||||||
interface TextFieldProps<TFieldValues extends FieldValues = any>
|
interface TextCellProps<TFieldValues extends FieldValues = any>
|
||||||
extends FieldProps<TFieldValues> {}
|
extends CellProps<TFieldValues> {}
|
||||||
|
|
||||||
export const TextField = <TFieldValues extends FieldValues = any>({
|
export const TextCell = <TFieldValues extends FieldValues = any>({
|
||||||
field,
|
field,
|
||||||
meta,
|
meta,
|
||||||
}: TextFieldProps<TFieldValues>) => {
|
}: TextCellProps<TFieldValues>) => {
|
||||||
const { register } = meta
|
const { register } = meta
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from "./void-cell"
|
||||||
+14
@@ -0,0 +1,14 @@
|
|||||||
|
import { PropsWithChildren } from "react"
|
||||||
|
import { GridCellType } from "../../../constants"
|
||||||
|
|
||||||
|
export const VoidCell = ({ children }: PropsWithChildren) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
role="cell"
|
||||||
|
data-cell-type={GridCellType.VOID}
|
||||||
|
className="bg-ui-bg-subtle size-full cursor-not-allowed px-4 py-2.5"
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
-1
@@ -1 +0,0 @@
|
|||||||
export * from "./boolean-field"
|
|
||||||
-1
@@ -1 +0,0 @@
|
|||||||
export * from "./text-field"
|
|
||||||
-15
@@ -1,15 +0,0 @@
|
|||||||
import { PropsWithChildren } from "react"
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Field for displaying non-editable data in a grid.
|
|
||||||
*/
|
|
||||||
export const DisplayField = ({ children }: PropsWithChildren) => {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className="flex size-full cursor-not-allowed items-center justify-center px-4 py-2.5"
|
|
||||||
data-role="presentation"
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
-1
@@ -1 +0,0 @@
|
|||||||
export * from "./display-field"
|
|
||||||
@@ -5,7 +5,7 @@ export type DataGridMeta<TFieldValues extends FieldValues = FieldValues> = {
|
|||||||
control: Control<TFieldValues>
|
control: Control<TFieldValues>
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FieldProps<TFieldValues extends FieldValues = FieldValues> {
|
export interface CellProps<TFieldValues extends FieldValues = FieldValues> {
|
||||||
field: Path<TFieldValues>
|
field: Path<TFieldValues>
|
||||||
meta: DataGridMeta<TFieldValues>
|
meta: DataGridMeta<TFieldValues>
|
||||||
}
|
}
|
||||||
+6
-1
@@ -1,12 +1,17 @@
|
|||||||
import { Tooltip } from "@medusajs/ui"
|
import { Tooltip } from "@medusajs/ui"
|
||||||
import format from "date-fns/format"
|
import format from "date-fns/format"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
|
import { PlaceholderCell } from "../placeholder-cell"
|
||||||
|
|
||||||
type DateCellProps = {
|
type DateCellProps = {
|
||||||
date: Date
|
date: Date | string | undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CreatedAtCell = ({ date }: DateCellProps) => {
|
export const CreatedAtCell = ({ date }: DateCellProps) => {
|
||||||
|
if (!date) {
|
||||||
|
return <PlaceholderCell />
|
||||||
|
}
|
||||||
|
|
||||||
const value = new Date(date)
|
const value = new Date(date)
|
||||||
value.setMinutes(value.getMinutes() - value.getTimezoneOffset())
|
value.setMinutes(value.getMinutes() - value.getTimezoneOffset())
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -7,10 +7,10 @@ type StatusCellProps = PropsWithChildren<{
|
|||||||
|
|
||||||
export const StatusCell = ({ color, children }: StatusCellProps) => {
|
export const StatusCell = ({ color, children }: StatusCellProps) => {
|
||||||
return (
|
return (
|
||||||
<div className="txt-compact-small text-ui-fg-subtle flex h-full w-full items-center gap-x-0.5 overflow-hidden">
|
<div className="txt-compact-small text-ui-fg-subtle flex h-full w-full items-center gap-x-2 overflow-hidden">
|
||||||
<div
|
<div
|
||||||
role="presentation"
|
role="presentation"
|
||||||
className="flex size-5 items-center justify-center"
|
className="flex h-5 w-2 items-center justify-center"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={clx(
|
className={clx(
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
|
import {
|
||||||
|
AdminCustomerGroupListResponse,
|
||||||
|
AdminCustomerGroupResponse,
|
||||||
|
} from "@medusajs/types"
|
||||||
import { QueryKey, UseQueryOptions, useQuery } from "@tanstack/react-query"
|
import { QueryKey, UseQueryOptions, useQuery } from "@tanstack/react-query"
|
||||||
import { client } from "../../lib/client"
|
import { client } from "../../lib/client"
|
||||||
import { queryKeysFactory } from "../../lib/query-key-factory"
|
import { queryKeysFactory } from "../../lib/query-key-factory"
|
||||||
import {
|
|
||||||
AdminCustomerGroupResponse,
|
|
||||||
AdminCustomerGroupListResponse,
|
|
||||||
} from "@medusajs/types"
|
|
||||||
|
|
||||||
const CUSTOMER_GROUPS_QUERY_KEY = "customer_groups" as const
|
const CUSTOMER_GROUPS_QUERY_KEY = "customer_groups" as const
|
||||||
const customerGroupsQueryKeys = queryKeysFactory(CUSTOMER_GROUPS_QUERY_KEY)
|
const customerGroupsQueryKeys = queryKeysFactory(CUSTOMER_GROUPS_QUERY_KEY)
|
||||||
|
|||||||
@@ -0,0 +1,140 @@
|
|||||||
|
import {
|
||||||
|
QueryKey,
|
||||||
|
UseMutationOptions,
|
||||||
|
UseQueryOptions,
|
||||||
|
useMutation,
|
||||||
|
useQuery,
|
||||||
|
} from "@tanstack/react-query"
|
||||||
|
import { client } from "../../lib/client"
|
||||||
|
import { queryClient } from "../../lib/medusa"
|
||||||
|
import { queryKeysFactory } from "../../lib/query-key-factory"
|
||||||
|
import {
|
||||||
|
AddPriceListPricesReq,
|
||||||
|
CreatePriceListReq,
|
||||||
|
DeletePriceListPricesReq,
|
||||||
|
UpdatePriceListReq,
|
||||||
|
} from "../../types/api-payloads"
|
||||||
|
import {
|
||||||
|
PriceListDeleteRes,
|
||||||
|
PriceListListRes,
|
||||||
|
PriceListRes,
|
||||||
|
} from "../../types/api-responses"
|
||||||
|
|
||||||
|
const PRICE_LISTS_QUERY_KEY = "price-lists" as const
|
||||||
|
export const priceListsQueryKeys = queryKeysFactory(PRICE_LISTS_QUERY_KEY)
|
||||||
|
|
||||||
|
export const usePriceList = (
|
||||||
|
id: string,
|
||||||
|
query?: Record<string, any>,
|
||||||
|
options?: Omit<
|
||||||
|
UseQueryOptions<PriceListRes, Error, PriceListRes, QueryKey>,
|
||||||
|
"queryKey" | "queryFn"
|
||||||
|
>
|
||||||
|
) => {
|
||||||
|
const { data, ...rest } = useQuery({
|
||||||
|
queryFn: () => client.priceLists.retrieve(id, query),
|
||||||
|
queryKey: priceListsQueryKeys.detail(id),
|
||||||
|
...options,
|
||||||
|
})
|
||||||
|
|
||||||
|
return { ...data, ...rest }
|
||||||
|
}
|
||||||
|
|
||||||
|
export const usePriceLists = (
|
||||||
|
query?: Record<string, any>,
|
||||||
|
options?: Omit<
|
||||||
|
UseQueryOptions<PriceListListRes, Error, PriceListListRes, QueryKey>,
|
||||||
|
"queryKey" | "queryFn"
|
||||||
|
>
|
||||||
|
) => {
|
||||||
|
const { data, ...rest } = useQuery({
|
||||||
|
queryFn: () => client.priceLists.list(query),
|
||||||
|
queryKey: priceListsQueryKeys.list(query),
|
||||||
|
...options,
|
||||||
|
})
|
||||||
|
|
||||||
|
return { ...data, ...rest }
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useCreatePriceList = (
|
||||||
|
options?: UseMutationOptions<PriceListRes, Error, CreatePriceListReq>
|
||||||
|
) => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (payload) => client.priceLists.create(payload),
|
||||||
|
onSuccess: (data, variables, context) => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: priceListsQueryKeys.list() })
|
||||||
|
|
||||||
|
options?.onSuccess?.(data, variables, context)
|
||||||
|
},
|
||||||
|
...options,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useUpdatePriceList = (
|
||||||
|
id: string,
|
||||||
|
options?: UseMutationOptions<PriceListRes, Error, UpdatePriceListReq>
|
||||||
|
) => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (payload) => client.priceLists.update(id, payload),
|
||||||
|
onSuccess: (data, variables, context) => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: priceListsQueryKeys.list() })
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: priceListsQueryKeys.detail(id),
|
||||||
|
})
|
||||||
|
|
||||||
|
options?.onSuccess?.(data, variables, context)
|
||||||
|
},
|
||||||
|
...options,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useDeletePriceList = (
|
||||||
|
id: string,
|
||||||
|
options?: UseMutationOptions<PriceListDeleteRes, Error, void>
|
||||||
|
) => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: () => client.priceLists.delete(id),
|
||||||
|
onSuccess: (data, variables, context) => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: priceListsQueryKeys.list() })
|
||||||
|
|
||||||
|
options?.onSuccess?.(data, variables, context)
|
||||||
|
},
|
||||||
|
...options,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const usePriceListAddPrices = (
|
||||||
|
id: string,
|
||||||
|
options?: UseMutationOptions<PriceListRes, Error, AddPriceListPricesReq>
|
||||||
|
) => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (payload) => client.priceLists.addPrices(id, payload),
|
||||||
|
onSuccess: (data, variables, context) => {
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: priceListsQueryKeys.detail(id),
|
||||||
|
})
|
||||||
|
queryClient.invalidateQueries({ queryKey: priceListsQueryKeys.lists() })
|
||||||
|
|
||||||
|
options?.onSuccess?.(data, variables, context)
|
||||||
|
},
|
||||||
|
...options,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const usePriceListRemovePrices = (
|
||||||
|
id: string,
|
||||||
|
options?: UseMutationOptions<PriceListRes, Error, DeletePriceListPricesReq>
|
||||||
|
) => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (payload) => client.priceLists.removePrices(id, payload),
|
||||||
|
onSuccess: (data, variables, context) => {
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: priceListsQueryKeys.detail(id),
|
||||||
|
})
|
||||||
|
queryClient.invalidateQueries({ queryKey: priceListsQueryKeys.lists() })
|
||||||
|
|
||||||
|
options?.onSuccess?.(data, variables, context)
|
||||||
|
},
|
||||||
|
...options,
|
||||||
|
})
|
||||||
|
}
|
||||||
+5
-5
@@ -1,15 +1,15 @@
|
|||||||
import { CustomerGroup } from "@medusajs/medusa"
|
import { Text } from "@medusajs/ui"
|
||||||
import { createColumnHelper } from "@tanstack/react-table"
|
import { createColumnHelper } from "@tanstack/react-table"
|
||||||
import { useMemo } from "react"
|
import { useMemo } from "react"
|
||||||
import { Text } from "@medusajs/ui"
|
|
||||||
|
|
||||||
import { NameHeader } from "../../../components/table/table-cells/common/name-cell"
|
import { CustomerGroupDTO } from "@medusajs/types"
|
||||||
import {
|
import {
|
||||||
CreatedAtHeader,
|
|
||||||
CreatedAtCell,
|
CreatedAtCell,
|
||||||
|
CreatedAtHeader,
|
||||||
} from "../../../components/table/table-cells/common/created-at-cell"
|
} from "../../../components/table/table-cells/common/created-at-cell"
|
||||||
|
import { NameHeader } from "../../../components/table/table-cells/common/name-cell"
|
||||||
|
|
||||||
const columnHelper = createColumnHelper<CustomerGroup>()
|
const columnHelper = createColumnHelper<CustomerGroupDTO>()
|
||||||
|
|
||||||
export const useCustomerGroupTableColumns = () => {
|
export const useCustomerGroupTableColumns = () => {
|
||||||
return useMemo(
|
return useMemo(
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { customerGroups } from "./customer-groups"
|
|||||||
import { customers } from "./customers"
|
import { customers } from "./customers"
|
||||||
import { invites } from "./invites"
|
import { invites } from "./invites"
|
||||||
import { payments } from "./payments"
|
import { payments } from "./payments"
|
||||||
|
import { priceLists } from "./price-lists"
|
||||||
import { productTypes } from "./product-types"
|
import { productTypes } from "./product-types"
|
||||||
import { products } from "./products"
|
import { products } from "./products"
|
||||||
import { promotions } from "./promotions"
|
import { promotions } from "./promotions"
|
||||||
@@ -40,6 +41,7 @@ export const client = {
|
|||||||
invites: invites,
|
invites: invites,
|
||||||
products: products,
|
products: products,
|
||||||
productTypes: productTypes,
|
productTypes: productTypes,
|
||||||
|
priceLists: priceLists,
|
||||||
stockLocations: stockLocations,
|
stockLocations: stockLocations,
|
||||||
workflowExecutions: workflowExecutions,
|
workflowExecutions: workflowExecutions,
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import {
|
||||||
|
AddPriceListPricesReq,
|
||||||
|
CreatePriceListReq,
|
||||||
|
DeletePriceListPricesReq,
|
||||||
|
UpdatePriceListReq,
|
||||||
|
} from "../../types/api-payloads"
|
||||||
|
import {
|
||||||
|
PriceListDeleteRes,
|
||||||
|
PriceListListRes,
|
||||||
|
PriceListRes,
|
||||||
|
} from "../../types/api-responses"
|
||||||
|
import { getRequest, postRequest } from "./common"
|
||||||
|
|
||||||
|
async function retrievePriceLists(id: string, query?: Record<string, any>) {
|
||||||
|
return getRequest<PriceListRes>(`/admin/price-lists/${id}`, query)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function listPriceLists(query?: Record<string, any>) {
|
||||||
|
return getRequest<PriceListListRes>(`/admin/price-lists`, query)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createPriceList(payload: CreatePriceListReq) {
|
||||||
|
return postRequest<PriceListRes>(`/admin/price-lists`, payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updatePriceList(id: string, payload: UpdatePriceListReq) {
|
||||||
|
return postRequest<PriceListRes>(`/admin/price-lists/${id}`, payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deletePriceList(id: string) {
|
||||||
|
return postRequest<PriceListDeleteRes>(`/admin/price-lists/${id}/delete`)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function addPriceListPrices(id: string, payload: AddPriceListPricesReq) {
|
||||||
|
return postRequest<PriceListRes>(
|
||||||
|
`/admin/price-lists/${id}/prices/batch/add`,
|
||||||
|
payload
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function removePriceListPrices(
|
||||||
|
id: string,
|
||||||
|
payload: DeletePriceListPricesReq
|
||||||
|
) {
|
||||||
|
return postRequest<PriceListRes>(
|
||||||
|
`/admin/price-lists/${id}/prices/batch/remove`,
|
||||||
|
payload
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const priceLists = {
|
||||||
|
retrieve: retrievePriceLists,
|
||||||
|
list: listPriceLists,
|
||||||
|
create: createPriceList,
|
||||||
|
update: updatePriceList,
|
||||||
|
delete: deletePriceList,
|
||||||
|
addPrices: addPriceListPrices,
|
||||||
|
removePrices: removePriceListPrices,
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { currencies } from "./currencies"
|
import { currencies } from "./currencies"
|
||||||
|
|
||||||
const getDecimalDigits = (currency: string) => {
|
export const getDecimalDigits = (currency: string) => {
|
||||||
return currencies[currency.toUpperCase()]?.decimal_digits
|
return currencies[currency.toUpperCase()]?.decimal_digits ?? 0
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -18,7 +18,7 @@ const getDecimalDigits = (currency: string) => {
|
|||||||
export const getPresentationalAmount = (amount: number, currency: string) => {
|
export const getPresentationalAmount = (amount: number, currency: string) => {
|
||||||
const decimalDigits = getDecimalDigits(currency)
|
const decimalDigits = getDecimalDigits(currency)
|
||||||
|
|
||||||
if (!decimalDigits) {
|
if (decimalDigits === undefined) {
|
||||||
throw new Error("Currency has no decimal digits")
|
throw new Error("Currency has no decimal digits")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,9 +36,9 @@ export const getPresentationalAmount = (amount: number, currency: string) => {
|
|||||||
* getDbAmount(10, "jpy") // 10
|
* getDbAmount(10, "jpy") // 10
|
||||||
*/
|
*/
|
||||||
export const getDbAmount = (amount: number, currency: string) => {
|
export const getDbAmount = (amount: number, currency: string) => {
|
||||||
const decimalDigits = currencies[currency.toUpperCase()].decimal_digits
|
const decimalDigits = getDecimalDigits(currency)
|
||||||
|
|
||||||
if (!decimalDigits) {
|
if (decimalDigits === undefined) {
|
||||||
throw new Error("Currency has no decimal digits")
|
throw new Error("Currency has no decimal digits")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -310,44 +310,6 @@ export const v1Routes: RouteObject[] = [
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: "/pricing",
|
|
||||||
handle: {
|
|
||||||
crumb: () => "Pricing",
|
|
||||||
},
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
path: "",
|
|
||||||
lazy: () => import("../../routes/pricing/pricing-list"),
|
|
||||||
children: [
|
|
||||||
// {
|
|
||||||
// path: "create",
|
|
||||||
// lazy: () => import("../../routes/pricing/pricing-create"),
|
|
||||||
// },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: ":id",
|
|
||||||
lazy: () => import("../../routes/pricing/pricing-detail"),
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
path: "edit",
|
|
||||||
lazy: () => import("../../routes/pricing/pricing-edit"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "products/add",
|
|
||||||
lazy: () =>
|
|
||||||
import("../../routes/pricing/pricing-products-add"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "products/edit",
|
|
||||||
lazy: () =>
|
|
||||||
import("../../routes/pricing/pricing-products-edit"),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
...routeExtensions,
|
...routeExtensions,
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import { ErrorBoundary } from "../../components/error/error-boundary"
|
|||||||
import { MainLayout } from "../../components/layout-v2/main-layout"
|
import { MainLayout } from "../../components/layout-v2/main-layout"
|
||||||
import { SettingsLayout } from "../../components/layout/settings-layout"
|
import { SettingsLayout } from "../../components/layout/settings-layout"
|
||||||
import { useMe } from "../../hooks/api/users"
|
import { useMe } from "../../hooks/api/users"
|
||||||
|
import { PriceListRes } from "../../types/api-responses"
|
||||||
import { SearchProvider } from "../search-provider"
|
import { SearchProvider } from "../search-provider"
|
||||||
import { SidebarProvider } from "../sidebar-provider"
|
import { SidebarProvider } from "../sidebar-provider"
|
||||||
|
|
||||||
@@ -262,6 +263,53 @@ export const v2Routes: RouteObject[] = [
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/pricing",
|
||||||
|
handle: {
|
||||||
|
crumb: () => "Pricing",
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: "",
|
||||||
|
lazy: () => import("../../v2-routes/pricing/pricing-list"),
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: "create",
|
||||||
|
lazy: () =>
|
||||||
|
import("../../v2-routes/pricing/pricing-create"),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: ":id",
|
||||||
|
lazy: () => import("../../v2-routes/pricing/pricing-detail"),
|
||||||
|
handle: {
|
||||||
|
crumb: (data: PriceListRes) => data.price_list.title,
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: "edit",
|
||||||
|
lazy: () => import("../../v2-routes/pricing/pricing-edit"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "configuration",
|
||||||
|
lazy: () =>
|
||||||
|
import("../../v2-routes/pricing/pricing-configuration"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "products/add",
|
||||||
|
lazy: () =>
|
||||||
|
import("../../v2-routes/pricing/pricing-products-add"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "products/edit",
|
||||||
|
lazy: () =>
|
||||||
|
import("../../v2-routes/pricing/pricing-products-prices"),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "/customers",
|
path: "/customers",
|
||||||
handle: {
|
handle: {
|
||||||
|
|||||||
-202
@@ -1,202 +0,0 @@
|
|||||||
import { Currency, Product, ProductVariant, Region } from "@medusajs/medusa"
|
|
||||||
import { createColumnHelper } from "@tanstack/react-table"
|
|
||||||
import { useAdminPriceListProducts } from "medusa-react"
|
|
||||||
import { useMemo } from "react"
|
|
||||||
import { useForm } from "react-hook-form"
|
|
||||||
import { useParams } from "react-router-dom"
|
|
||||||
import { z } from "zod"
|
|
||||||
|
|
||||||
import { useTranslation } from "react-i18next"
|
|
||||||
import { Thumbnail } from "../../../../../components/common/thumbnail"
|
|
||||||
import { DataGrid } from "../../../../../components/grid/data-grid"
|
|
||||||
import { TextField } from "../../../../../components/grid/grid-fields/common/text-field"
|
|
||||||
import { DisplayField } from "../../../../../components/grid/grid-fields/non-interactive/display-field"
|
|
||||||
import { DataGridMeta } from "../../../../../components/grid/types"
|
|
||||||
import { RouteFocusModal } from "../../../../../components/route-modal"
|
|
||||||
|
|
||||||
type EditProductPricesFormProps = {
|
|
||||||
regions: Region[]
|
|
||||||
currencies: Currency[]
|
|
||||||
ids: string | null
|
|
||||||
}
|
|
||||||
|
|
||||||
const ProductEditorSchema = z.object({
|
|
||||||
products: z.record(
|
|
||||||
z.object({
|
|
||||||
variants: z.record(
|
|
||||||
z.object({
|
|
||||||
prices: z.object({
|
|
||||||
regions: z.record(
|
|
||||||
z.object({
|
|
||||||
amount: z.number(),
|
|
||||||
})
|
|
||||||
),
|
|
||||||
currencies: z.record(
|
|
||||||
z.object({
|
|
||||||
amount: z.number(),
|
|
||||||
})
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
),
|
|
||||||
})
|
|
||||||
),
|
|
||||||
})
|
|
||||||
|
|
||||||
type ProductEditorSchemaType = z.infer<typeof ProductEditorSchema>
|
|
||||||
|
|
||||||
export const EditProductPricesForm = ({
|
|
||||||
ids,
|
|
||||||
regions,
|
|
||||||
currencies,
|
|
||||||
}: EditProductPricesFormProps) => {
|
|
||||||
const { id } = useParams()
|
|
||||||
|
|
||||||
const form = useForm()
|
|
||||||
|
|
||||||
const { products, count, isLoading, isError, error } =
|
|
||||||
useAdminPriceListProducts(
|
|
||||||
id!,
|
|
||||||
{
|
|
||||||
id: ids?.split(",") || undefined,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
keepPreviousData: true,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const columns = useColumns({ regions, currencies })
|
|
||||||
|
|
||||||
if (isError) {
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<RouteFocusModal.Form form={form}>
|
|
||||||
<form className="flex size-full flex-col">
|
|
||||||
<RouteFocusModal.Header></RouteFocusModal.Header>
|
|
||||||
<RouteFocusModal.Body className="flex flex-col overflow-hidden">
|
|
||||||
<DataGrid
|
|
||||||
isLoading={isLoading}
|
|
||||||
data={products as Product[]}
|
|
||||||
columns={columns}
|
|
||||||
state={form}
|
|
||||||
getSubRows={getVariantRows}
|
|
||||||
/>
|
|
||||||
</RouteFocusModal.Body>
|
|
||||||
</form>
|
|
||||||
</RouteFocusModal.Form>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper function to determine if a row is a product or a variant.
|
|
||||||
*/
|
|
||||||
const isProduct = (row: Product | ProductVariant): row is Product => {
|
|
||||||
return "variants" in row
|
|
||||||
}
|
|
||||||
|
|
||||||
const getVariantRows = (row: Product | ProductVariant) => {
|
|
||||||
if ("variants" in row) {
|
|
||||||
return row.variants
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
const columnHelper = createColumnHelper<Product | ProductVariant>()
|
|
||||||
|
|
||||||
const createRegionColum = (region: Region) => {
|
|
||||||
return columnHelper.display({
|
|
||||||
id: region.id,
|
|
||||||
header: region.name,
|
|
||||||
cell: ({ row, table }) => {
|
|
||||||
const entity = row.original
|
|
||||||
|
|
||||||
if (isProduct(entity)) {
|
|
||||||
return <DisplayField />
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TextField
|
|
||||||
field={`products.${entity.id}.variants.${entity.id}.prices.regions.${region.id}.amount`}
|
|
||||||
meta={table.options.meta as DataGridMeta<ProductEditorSchemaType>}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const createCurrencyColumn = (currency: Currency) => {
|
|
||||||
return columnHelper.display({
|
|
||||||
id: currency.code,
|
|
||||||
header: currency.code.toUpperCase(),
|
|
||||||
cell: ({ row, table }) => {
|
|
||||||
const entity = row.original
|
|
||||||
|
|
||||||
if (isProduct(entity)) {
|
|
||||||
return <DisplayField />
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TextField
|
|
||||||
field={`products.${entity.id}.variants.${entity.id}.prices.currencies.${currency.code}.amount`}
|
|
||||||
meta={table.options.meta as DataGridMeta<ProductEditorSchemaType>}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const useColumns = ({
|
|
||||||
regions,
|
|
||||||
currencies,
|
|
||||||
}: {
|
|
||||||
regions: Region[]
|
|
||||||
currencies: Currency[]
|
|
||||||
}) => {
|
|
||||||
const { t } = useTranslation()
|
|
||||||
|
|
||||||
const regionColumns = useMemo(() => {
|
|
||||||
return regions.map(createRegionColum)
|
|
||||||
}, [regions])
|
|
||||||
|
|
||||||
const currencyColumns = useMemo(() => {
|
|
||||||
return currencies.map(createCurrencyColumn)
|
|
||||||
}, [currencies])
|
|
||||||
|
|
||||||
return useMemo(
|
|
||||||
() => [
|
|
||||||
columnHelper.display({
|
|
||||||
id: "product-display",
|
|
||||||
header: t("fields.product"),
|
|
||||||
cell: ({ row }) => {
|
|
||||||
const entity = row.original
|
|
||||||
|
|
||||||
if (isProduct(entity)) {
|
|
||||||
return (
|
|
||||||
<DisplayField>
|
|
||||||
<div className="flex h-full w-full items-center gap-x-2 overflow-hidden">
|
|
||||||
<Thumbnail src={entity.thumbnail} />
|
|
||||||
<span className="truncate">{entity.title}</span>
|
|
||||||
</div>
|
|
||||||
</DisplayField>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DisplayField>
|
|
||||||
<div className="flex h-full w-full items-center overflow-hidden">
|
|
||||||
<span className="truncate">{entity.title}</span>
|
|
||||||
</div>
|
|
||||||
</DisplayField>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
size: 350,
|
|
||||||
}),
|
|
||||||
...regionColumns,
|
|
||||||
...currencyColumns,
|
|
||||||
],
|
|
||||||
[t, regionColumns, currencyColumns]
|
|
||||||
)
|
|
||||||
}
|
|
||||||
-1
@@ -1 +0,0 @@
|
|||||||
export * from "./edit-product-prices-form"
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export { PricingProductsEdit as Component } from "./pricing-products-edit"
|
|
||||||
-45
@@ -1,45 +0,0 @@
|
|||||||
import { useAdminRegions, useAdminStore } from "medusa-react"
|
|
||||||
import { useSearchParams } from "react-router-dom"
|
|
||||||
import { RouteFocusModal } from "../../../components/route-modal"
|
|
||||||
import { EditProductPricesForm } from "./components/edit-product-prices-form"
|
|
||||||
|
|
||||||
export const PricingProductsEdit = () => {
|
|
||||||
const [searchParams] = useSearchParams()
|
|
||||||
|
|
||||||
const { regions, isLoading, isError, error } = useAdminRegions({
|
|
||||||
limit: 1000,
|
|
||||||
fields: "id,name,includes_tax,currency.code,currency.symbol_native",
|
|
||||||
expand: "currency",
|
|
||||||
})
|
|
||||||
|
|
||||||
const {
|
|
||||||
store,
|
|
||||||
isLoading: isLoadingStore,
|
|
||||||
isError: isStoreError,
|
|
||||||
error: storeError,
|
|
||||||
} = useAdminStore()
|
|
||||||
|
|
||||||
const ids = searchParams.get("ids[]")
|
|
||||||
const currencies = store?.currencies || []
|
|
||||||
const ready = !isLoading && regions && !isLoadingStore && store
|
|
||||||
|
|
||||||
if (isError) {
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isStoreError) {
|
|
||||||
throw storeError
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<RouteFocusModal>
|
|
||||||
{ready && (
|
|
||||||
<EditProductPricesForm
|
|
||||||
ids={ids}
|
|
||||||
regions={regions}
|
|
||||||
currencies={currencies}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</RouteFocusModal>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
CreateCampaignDTO,
|
CreateCampaignDTO,
|
||||||
CreateCustomerDTO,
|
CreateCustomerDTO,
|
||||||
CreateInviteDTO,
|
CreateInviteDTO,
|
||||||
|
CreatePriceListDTO,
|
||||||
CreateProductCollectionDTO,
|
CreateProductCollectionDTO,
|
||||||
CreatePromotionDTO,
|
CreatePromotionDTO,
|
||||||
CreatePromotionRuleDTO,
|
CreatePromotionRuleDTO,
|
||||||
@@ -16,6 +17,7 @@ import {
|
|||||||
UpdateApiKeyDTO,
|
UpdateApiKeyDTO,
|
||||||
UpdateCampaignDTO,
|
UpdateCampaignDTO,
|
||||||
UpdateCustomerDTO,
|
UpdateCustomerDTO,
|
||||||
|
UpdatePriceListDTO,
|
||||||
UpdateProductCollectionDTO,
|
UpdateProductCollectionDTO,
|
||||||
UpdatePromotionDTO,
|
UpdatePromotionDTO,
|
||||||
UpdatePromotionRuleDTO,
|
UpdatePromotionRuleDTO,
|
||||||
@@ -64,6 +66,18 @@ export type UpdateStockLocationReq = UpdateStockLocationInput
|
|||||||
export type CreateProductCollectionReq = CreateProductCollectionDTO
|
export type CreateProductCollectionReq = CreateProductCollectionDTO
|
||||||
export type UpdateProductCollectionReq = UpdateProductCollectionDTO
|
export type UpdateProductCollectionReq = UpdateProductCollectionDTO
|
||||||
|
|
||||||
|
// Price Lists
|
||||||
|
export type CreatePriceListReq = CreatePriceListDTO
|
||||||
|
export type UpdatePriceListReq = UpdatePriceListDTO
|
||||||
|
export type AddPriceListPricesReq = {
|
||||||
|
prices: {
|
||||||
|
currency_code: string
|
||||||
|
amount: number
|
||||||
|
variant_id: string
|
||||||
|
}[]
|
||||||
|
}
|
||||||
|
export type DeletePriceListPricesReq = { ids: string[] }
|
||||||
|
|
||||||
// Promotion
|
// Promotion
|
||||||
export type CreatePromotionReq = CreatePromotionDTO
|
export type CreatePromotionReq = CreatePromotionDTO
|
||||||
export type UpdatePromotionReq = UpdatePromotionDTO
|
export type UpdatePromotionReq = UpdatePromotionDTO
|
||||||
|
|||||||
@@ -5,8 +5,10 @@
|
|||||||
import {
|
import {
|
||||||
CampaignDTO,
|
CampaignDTO,
|
||||||
CurrencyDTO,
|
CurrencyDTO,
|
||||||
|
CustomerGroupDTO,
|
||||||
InviteDTO,
|
InviteDTO,
|
||||||
PaymentProviderDTO,
|
PaymentProviderDTO,
|
||||||
|
PriceListDTO,
|
||||||
ProductCategoryDTO,
|
ProductCategoryDTO,
|
||||||
ProductCollectionDTO,
|
ProductCollectionDTO,
|
||||||
ProductDTO,
|
ProductDTO,
|
||||||
@@ -137,3 +139,15 @@ export type ProductCollectionListRes = {
|
|||||||
collections: ProductCollectionDTO[]
|
collections: ProductCollectionDTO[]
|
||||||
} & ListRes
|
} & ListRes
|
||||||
export type ProductCollectionDeleteRes = DeleteRes
|
export type ProductCollectionDeleteRes = DeleteRes
|
||||||
|
|
||||||
|
// Price Lists
|
||||||
|
export type PriceListRes = { price_list: PriceListDTO }
|
||||||
|
export type PriceListListRes = { price_lists: PriceListDTO[] } & ListRes
|
||||||
|
export type PriceListDeleteRes = DeleteRes
|
||||||
|
|
||||||
|
// Customer Groups
|
||||||
|
export type CustomerGroupRes = { customer_group: CustomerGroupDTO }
|
||||||
|
export type CustomerGroupListRes = {
|
||||||
|
customer_groups: CustomerGroupDTO[]
|
||||||
|
} & ListRes
|
||||||
|
export type CustomerGroupDeleteRes = DeleteRes
|
||||||
File renamed without changes.
+77
@@ -0,0 +1,77 @@
|
|||||||
|
import { CurrencyDTO, ProductVariantDTO } from "@medusajs/types"
|
||||||
|
import { ColumnDef, createColumnHelper } from "@tanstack/react-table"
|
||||||
|
import { useMemo } from "react"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
import { Thumbnail } from "../../../../components/common/thumbnail"
|
||||||
|
import { CurrencyCell } from "../../../../components/grid/grid-cells/common/currency-cell"
|
||||||
|
import { ReadonlyCell } from "../../../../components/grid/grid-cells/common/readonly-cell"
|
||||||
|
import { VoidCell } from "../../../../components/grid/grid-cells/common/void-cell"
|
||||||
|
import { DataGridMeta } from "../../../../components/grid/types"
|
||||||
|
import { ExtendedProductDTO } from "../../../../types/api-responses"
|
||||||
|
import { isProductRow } from "../utils"
|
||||||
|
|
||||||
|
const columnHelper = createColumnHelper<
|
||||||
|
ExtendedProductDTO | ProductVariantDTO
|
||||||
|
>()
|
||||||
|
|
||||||
|
export const usePriceListGridColumns = ({
|
||||||
|
currencies = [],
|
||||||
|
}: {
|
||||||
|
currencies?: CurrencyDTO[]
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
const colDefs: ColumnDef<ExtendedProductDTO | ProductVariantDTO>[] =
|
||||||
|
useMemo(() => {
|
||||||
|
return [
|
||||||
|
columnHelper.display({
|
||||||
|
id: t("fields.title"),
|
||||||
|
header: t("fields.title"),
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const entity = row.original
|
||||||
|
|
||||||
|
if (isProductRow(entity)) {
|
||||||
|
return (
|
||||||
|
<VoidCell>
|
||||||
|
<div className="flex h-full w-full items-center gap-x-2 overflow-hidden">
|
||||||
|
<Thumbnail src={entity.thumbnail} />
|
||||||
|
<span className="truncate">{entity.title}</span>
|
||||||
|
</div>
|
||||||
|
</VoidCell>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ReadonlyCell>
|
||||||
|
<div className="flex h-full w-full items-center gap-x-2 overflow-hidden">
|
||||||
|
<span className="truncate">{entity.title}</span>
|
||||||
|
</div>
|
||||||
|
</ReadonlyCell>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
...currencies.map((currency) => {
|
||||||
|
return columnHelper.display({
|
||||||
|
header: `Price ${currency.code.toUpperCase()}`,
|
||||||
|
cell: ({ row, table }) => {
|
||||||
|
const entity = row.original
|
||||||
|
|
||||||
|
if (isProductRow(entity)) {
|
||||||
|
return <VoidCell />
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CurrencyCell
|
||||||
|
currency={currency}
|
||||||
|
meta={table.options.meta as DataGridMeta}
|
||||||
|
field={`products.${entity.product_id}.variants.${entity.id}.currency_prices.${currency.code}.amount`}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
}, [t, currencies])
|
||||||
|
|
||||||
|
return colDefs
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { z } from "zod"
|
||||||
|
|
||||||
|
const PricingVariantPricesRecordSchema = z.record(
|
||||||
|
z
|
||||||
|
.object({
|
||||||
|
amount: z.string().optional(),
|
||||||
|
id: z.string().nullable().optional(),
|
||||||
|
})
|
||||||
|
.optional()
|
||||||
|
)
|
||||||
|
|
||||||
|
const PricingVariantsRecordSchema = z.record(
|
||||||
|
z.object({
|
||||||
|
currency_prices: PricingVariantPricesRecordSchema,
|
||||||
|
region_prices: PricingVariantPricesRecordSchema,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
export type PricingVariantsRecordType = z.infer<
|
||||||
|
typeof PricingVariantsRecordSchema
|
||||||
|
>
|
||||||
|
|
||||||
|
export const PricingProductsRecordSchema = z.record(
|
||||||
|
z.object({
|
||||||
|
variants: PricingVariantsRecordSchema,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
export type PricingProductsRecordType = z.infer<
|
||||||
|
typeof PricingProductsRecordSchema
|
||||||
|
>
|
||||||
+10
-3
@@ -1,8 +1,9 @@
|
|||||||
import { PriceList } from "@medusajs/medusa"
|
import { PriceListDTO, ProductVariantDTO } from "@medusajs/types"
|
||||||
import { TFunction } from "i18next"
|
import { TFunction } from "i18next"
|
||||||
|
import { ExtendedProductDTO } from "../../../types/api-responses"
|
||||||
import { PriceListStatus } from "./constants"
|
import { PriceListStatus } from "./constants"
|
||||||
|
|
||||||
const getValues = (priceList: PriceList) => {
|
const getValues = (priceList: PriceListDTO) => {
|
||||||
const startsAt = priceList.starts_at
|
const startsAt = priceList.starts_at
|
||||||
const endsAt = priceList.ends_at
|
const endsAt = priceList.ends_at
|
||||||
|
|
||||||
@@ -19,7 +20,7 @@ const getValues = (priceList: PriceList) => {
|
|||||||
|
|
||||||
export const getPriceListStatus = (
|
export const getPriceListStatus = (
|
||||||
t: TFunction<"translation">,
|
t: TFunction<"translation">,
|
||||||
priceList: PriceList
|
priceList: PriceListDTO
|
||||||
) => {
|
) => {
|
||||||
const { isExpired, isScheduled, isDraft } = getValues(priceList)
|
const { isExpired, isScheduled, isDraft } = getValues(priceList)
|
||||||
|
|
||||||
@@ -46,3 +47,9 @@ export const getPriceListStatus = (
|
|||||||
text,
|
text,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const isProductRow = (
|
||||||
|
row: ExtendedProductDTO | ProductVariantDTO
|
||||||
|
): row is ExtendedProductDTO => {
|
||||||
|
return "variants" in row
|
||||||
|
}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export * from "./price-list-configuration-form"
|
||||||
+197
@@ -0,0 +1,197 @@
|
|||||||
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
|
import { PriceListDTO } from "@medusajs/types"
|
||||||
|
import { Button, DatePicker, Switch, Text } from "@medusajs/ui"
|
||||||
|
import * as Collapsible from "@radix-ui/react-collapsible"
|
||||||
|
import { useForm } from "react-hook-form"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
import { z } from "zod"
|
||||||
|
import { Divider } from "../../../../../components/common/divider"
|
||||||
|
import { Form } from "../../../../../components/common/form"
|
||||||
|
import {
|
||||||
|
RouteDrawer,
|
||||||
|
useRouteModal,
|
||||||
|
} from "../../../../../components/route-modal"
|
||||||
|
import { useUpdatePriceList } from "../../../../../hooks/api/price-lists"
|
||||||
|
|
||||||
|
type PriceListConfigurationFormProps = {
|
||||||
|
priceList: PriceListDTO
|
||||||
|
}
|
||||||
|
|
||||||
|
const PriceListConfigurationSchema = z.object({
|
||||||
|
ends_at: z.date().nullable(),
|
||||||
|
starts_at: z.date().nullable(),
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO: Add CustomerGroups and possibly change to a RouteFocusModal
|
||||||
|
*
|
||||||
|
* Customer group rules aren't supported out of the box atm, so we can't
|
||||||
|
* set them in the UI without throwing an error.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const PriceListConfigurationForm = ({
|
||||||
|
priceList,
|
||||||
|
}: PriceListConfigurationFormProps) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const { handleSuccess } = useRouteModal()
|
||||||
|
|
||||||
|
const form = useForm<z.infer<typeof PriceListConfigurationSchema>>({
|
||||||
|
defaultValues: {
|
||||||
|
ends_at: priceList.ends_at ? new Date(priceList.ends_at) : null,
|
||||||
|
starts_at: priceList.starts_at ? new Date(priceList.starts_at) : null,
|
||||||
|
},
|
||||||
|
resolver: zodResolver(PriceListConfigurationSchema),
|
||||||
|
})
|
||||||
|
|
||||||
|
const { mutateAsync } = useUpdatePriceList(priceList.id)
|
||||||
|
|
||||||
|
const handleSubmit = form.handleSubmit(async (values) => {
|
||||||
|
await mutateAsync(
|
||||||
|
// @ts-ignore - type is wrong and expects an ID.
|
||||||
|
{
|
||||||
|
starts_at: values.starts_at?.toISOString() || null,
|
||||||
|
ends_at: values.ends_at?.toISOString() || null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
handleSuccess()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RouteDrawer.Form form={form}>
|
||||||
|
<form
|
||||||
|
className="flex flex-1 flex-col overflow-hidden"
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
>
|
||||||
|
<RouteDrawer.Body className="flex flex-1 flex-col gap-y-8 overflow-auto">
|
||||||
|
<Form.Field
|
||||||
|
control={form.control}
|
||||||
|
name="starts_at"
|
||||||
|
render={({ field: { value, onChange, ...rest } }) => {
|
||||||
|
const handleSwitchChange = (checked: boolean) => {
|
||||||
|
if (!checked) {
|
||||||
|
onChange(null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = new Date()
|
||||||
|
|
||||||
|
onChange(now)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form.Item>
|
||||||
|
<Collapsible.Root
|
||||||
|
open={!!value}
|
||||||
|
onOpenChange={handleSwitchChange}
|
||||||
|
>
|
||||||
|
<div className="grid grid-cols-[1fr_32px] gap-4">
|
||||||
|
<div>
|
||||||
|
<Text size="small" leading="compact" weight="plus">
|
||||||
|
Price list has a start date?
|
||||||
|
</Text>
|
||||||
|
<Text size="small" className="text-ui-fg-subtle">
|
||||||
|
Schedule the price list to activate in the future.
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<Collapsible.Trigger asChild>
|
||||||
|
<Switch name={rest.name} checked={!!value} />
|
||||||
|
</Collapsible.Trigger>
|
||||||
|
</div>
|
||||||
|
<Collapsible.Content>
|
||||||
|
<div className="flex flex-col gap-y-2 pt-4">
|
||||||
|
<Form.Label className="!txt-small text-ui-fg-subtle">
|
||||||
|
{t("fields.startDate")}
|
||||||
|
</Form.Label>
|
||||||
|
<Form.Control>
|
||||||
|
<DatePicker
|
||||||
|
value={value || undefined}
|
||||||
|
onChange={onChange}
|
||||||
|
{...rest}
|
||||||
|
/>
|
||||||
|
</Form.Control>
|
||||||
|
</div>
|
||||||
|
</Collapsible.Content>
|
||||||
|
<Form.ErrorMessage />
|
||||||
|
</Collapsible.Root>
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Divider />
|
||||||
|
<Form.Field
|
||||||
|
control={form.control}
|
||||||
|
name="ends_at"
|
||||||
|
render={({ field: { value, onChange, ...rest } }) => {
|
||||||
|
const handleSwitchChange = (checked: boolean) => {
|
||||||
|
if (!checked) {
|
||||||
|
onChange(null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const inAWeek = new Date(
|
||||||
|
new Date().setDate(new Date().getDate() + 7)
|
||||||
|
)
|
||||||
|
|
||||||
|
onChange(inAWeek)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form.Item>
|
||||||
|
<Collapsible.Root
|
||||||
|
open={!!value}
|
||||||
|
onOpenChange={handleSwitchChange}
|
||||||
|
>
|
||||||
|
<div className="grid grid-cols-[1fr_32px] gap-4">
|
||||||
|
<div>
|
||||||
|
<Text size="small" leading="compact" weight="plus">
|
||||||
|
Price list has an end date?
|
||||||
|
</Text>
|
||||||
|
<Text size="small" className="text-ui-fg-subtle">
|
||||||
|
Schedule the price list to deactivate in the future.
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<Collapsible.Trigger asChild>
|
||||||
|
<Switch name={rest.name} checked={!!value} />
|
||||||
|
</Collapsible.Trigger>
|
||||||
|
</div>
|
||||||
|
<Collapsible.Content>
|
||||||
|
<div className="flex flex-col gap-y-2 pt-4">
|
||||||
|
<Form.Label className="!txt-small text-ui-fg-subtle">
|
||||||
|
{t("fields.endDate")}
|
||||||
|
</Form.Label>
|
||||||
|
<Form.Control>
|
||||||
|
<DatePicker
|
||||||
|
value={value || undefined}
|
||||||
|
onChange={onChange}
|
||||||
|
{...rest}
|
||||||
|
/>
|
||||||
|
</Form.Control>
|
||||||
|
</div>
|
||||||
|
</Collapsible.Content>
|
||||||
|
<Form.ErrorMessage />
|
||||||
|
</Collapsible.Root>
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</RouteDrawer.Body>
|
||||||
|
<RouteDrawer.Footer className="shrink-0">
|
||||||
|
<div className="flex items-center justify-end gap-x-2">
|
||||||
|
<RouteDrawer.Close asChild>
|
||||||
|
<Button size="small" variant="secondary">
|
||||||
|
{t("actions.cancel")}
|
||||||
|
</Button>
|
||||||
|
</RouteDrawer.Close>
|
||||||
|
<Button size="small" type="submit">
|
||||||
|
{t("actions.save")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</RouteDrawer.Footer>
|
||||||
|
</form>
|
||||||
|
</RouteDrawer.Form>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { PricingConfiguration as Component } from "./pricing-configuration"
|
||||||
+28
@@ -0,0 +1,28 @@
|
|||||||
|
import { Heading } from "@medusajs/ui"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
import { useParams } from "react-router-dom"
|
||||||
|
import { RouteDrawer } from "../../../components/route-modal"
|
||||||
|
import { usePriceList } from "../../../hooks/api/price-lists"
|
||||||
|
import { PriceListConfigurationForm } from "./components/price-list-configuration-form"
|
||||||
|
|
||||||
|
export const PricingConfiguration = () => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const { id } = useParams()
|
||||||
|
|
||||||
|
const { price_list, isLoading, isError, error } = usePriceList(id!)
|
||||||
|
|
||||||
|
const ready = !isLoading && price_list
|
||||||
|
|
||||||
|
if (isError) {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RouteDrawer>
|
||||||
|
<RouteDrawer.Header>
|
||||||
|
<Heading>{t("pricing.settings.editPriceListTitle")}</Heading>
|
||||||
|
</RouteDrawer.Header>
|
||||||
|
{ready && <PriceListConfigurationForm priceList={price_list} />}
|
||||||
|
</RouteDrawer>
|
||||||
|
)
|
||||||
|
}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export * from "./pricing-create-form"
|
||||||
+359
@@ -0,0 +1,359 @@
|
|||||||
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
|
import { Button, ProgressStatus, ProgressTabs } from "@medusajs/ui"
|
||||||
|
import { FieldPath, useForm } from "react-hook-form"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
|
||||||
|
import { CreatePriceListDTO, CreatePriceListPriceDTO } from "@medusajs/types"
|
||||||
|
import { useState } from "react"
|
||||||
|
import { z } from "zod"
|
||||||
|
import {
|
||||||
|
RouteFocusModal,
|
||||||
|
useRouteModal,
|
||||||
|
} from "../../../../../components/route-modal"
|
||||||
|
import { useCreatePriceList } from "../../../../../hooks/api/price-lists"
|
||||||
|
import { castNumber } from "../../../../../lib/cast-number"
|
||||||
|
import { getDbAmount } from "../../../../../lib/money-amount-helpers"
|
||||||
|
import { PricingDetailsForm } from "./pricing-details-form"
|
||||||
|
import { PricingPricesForm } from "./pricing-prices-form"
|
||||||
|
import { PricingProductsForm } from "./pricing-products-form"
|
||||||
|
import {
|
||||||
|
PricingCreateSchema,
|
||||||
|
PricingCreateSchemaType,
|
||||||
|
PricingDetailsFields,
|
||||||
|
PricingDetailsSchema,
|
||||||
|
PricingPricesFields,
|
||||||
|
PricingProductsFields,
|
||||||
|
PricingProductsSchema,
|
||||||
|
} from "./schema"
|
||||||
|
|
||||||
|
enum Tab {
|
||||||
|
DETAIL = "detail",
|
||||||
|
PRODUCT = "product",
|
||||||
|
PRICE = "price",
|
||||||
|
}
|
||||||
|
|
||||||
|
const tabOrder = [Tab.DETAIL, Tab.PRODUCT, Tab.PRICE] as const
|
||||||
|
|
||||||
|
type TabState = Record<Tab, ProgressStatus>
|
||||||
|
|
||||||
|
const initialTabState: TabState = {
|
||||||
|
[Tab.DETAIL]: "in-progress",
|
||||||
|
[Tab.PRODUCT]: "not-started",
|
||||||
|
[Tab.PRICE]: "not-started",
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PricingCreateForm = () => {
|
||||||
|
const [tab, setTab] = useState<Tab>(Tab.DETAIL)
|
||||||
|
const [tabState, setTabState] = useState<TabState>(initialTabState)
|
||||||
|
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const { handleSuccess } = useRouteModal()
|
||||||
|
|
||||||
|
const form = useForm<PricingCreateSchemaType>({
|
||||||
|
defaultValues: {
|
||||||
|
type: "sale",
|
||||||
|
title: "",
|
||||||
|
description: "",
|
||||||
|
starts_at: null,
|
||||||
|
ends_at: null,
|
||||||
|
customer_group_ids: [],
|
||||||
|
product_ids: [],
|
||||||
|
products: {},
|
||||||
|
},
|
||||||
|
resolver: zodResolver(PricingCreateSchema),
|
||||||
|
})
|
||||||
|
|
||||||
|
const { mutateAsync, isPending } = useCreatePriceList()
|
||||||
|
|
||||||
|
const handleSubmit = form.handleSubmit(
|
||||||
|
async (data) => {
|
||||||
|
const { customer_group_ids, products } = data
|
||||||
|
|
||||||
|
const rules = customer_group_ids?.length
|
||||||
|
? { customer_group_id: customer_group_ids.map((cg) => cg.id) }
|
||||||
|
: undefined
|
||||||
|
|
||||||
|
const prices: CreatePriceListPriceDTO[] = []
|
||||||
|
|
||||||
|
for (const [_, product] of Object.entries(products)) {
|
||||||
|
const { variants } = product
|
||||||
|
|
||||||
|
for (const [variantId, variant] of Object.entries(variants)) {
|
||||||
|
const { currency_prices } = variant
|
||||||
|
|
||||||
|
for (const [currencyCode, currencyPrice] of Object.entries(
|
||||||
|
currency_prices
|
||||||
|
)) {
|
||||||
|
if (!currencyPrice) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
prices.push({
|
||||||
|
amount: getDbAmount(
|
||||||
|
castNumber(currencyPrice.amount),
|
||||||
|
currencyCode
|
||||||
|
),
|
||||||
|
currency_code: currencyCode,
|
||||||
|
// @ts-expect-error type is wrong
|
||||||
|
variant_id: variantId,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await mutateAsync(
|
||||||
|
{
|
||||||
|
title: data.title,
|
||||||
|
type: data.type as CreatePriceListDTO["type"],
|
||||||
|
description: data.description,
|
||||||
|
starts_at: data.starts_at ? data.starts_at.toISOString() : null,
|
||||||
|
ends_at: data.ends_at ? data.ends_at.toISOString() : null,
|
||||||
|
rules,
|
||||||
|
prices: prices,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSuccess: ({ price_list }) => {
|
||||||
|
handleSuccess(`../${price_list.id}`)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
},
|
||||||
|
(error) => console.error(error)
|
||||||
|
)
|
||||||
|
|
||||||
|
const partialFormValidation = (
|
||||||
|
fields: FieldPath<PricingCreateSchemaType>[],
|
||||||
|
schema: z.ZodSchema<any>
|
||||||
|
) => {
|
||||||
|
form.clearErrors(fields)
|
||||||
|
|
||||||
|
const values = fields.reduce((acc, key) => {
|
||||||
|
acc[key] = form.getValues(key)
|
||||||
|
return acc
|
||||||
|
}, {} as Record<string, unknown>)
|
||||||
|
|
||||||
|
const validationResult = schema.safeParse(values)
|
||||||
|
|
||||||
|
if (!validationResult.success) {
|
||||||
|
validationResult.error.errors.forEach(({ path, message }) => {
|
||||||
|
form.setError(path.join(".") as keyof PricingCreateSchemaType, {
|
||||||
|
type: "manual",
|
||||||
|
message,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
const isTabDirty = (tab: Tab) => {
|
||||||
|
switch (tab) {
|
||||||
|
case Tab.DETAIL: {
|
||||||
|
const fields = PricingDetailsFields
|
||||||
|
|
||||||
|
return fields.some((field) => {
|
||||||
|
return form.getFieldState(field).isDirty
|
||||||
|
})
|
||||||
|
}
|
||||||
|
case Tab.PRODUCT: {
|
||||||
|
const fields = PricingProductsFields
|
||||||
|
|
||||||
|
return fields.some((field) => {
|
||||||
|
return form.getFieldState(field).isDirty
|
||||||
|
})
|
||||||
|
}
|
||||||
|
case Tab.PRICE: {
|
||||||
|
const fields = PricingPricesFields
|
||||||
|
|
||||||
|
return fields.some((field) => {
|
||||||
|
return form.getFieldState(field).isDirty
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleChangeTab = (update: Tab) => {
|
||||||
|
if (tab === update) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tabOrder.indexOf(update) < tabOrder.indexOf(tab)) {
|
||||||
|
const isCurrentTabDirty = isTabDirty(tab)
|
||||||
|
|
||||||
|
setTabState((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[tab]: isCurrentTabDirty ? prev[tab] : "not-started",
|
||||||
|
[update]: "in-progress",
|
||||||
|
}))
|
||||||
|
|
||||||
|
setTab(update)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// get the tabs from the current tab to the update tab including the current tab
|
||||||
|
const tabs = tabOrder.slice(0, tabOrder.indexOf(update))
|
||||||
|
|
||||||
|
// validate all the tabs from the current tab to the update tab if it fails on any of tabs then set that tab as current tab
|
||||||
|
for (const tab of tabs) {
|
||||||
|
if (tab === Tab.DETAIL) {
|
||||||
|
if (
|
||||||
|
!partialFormValidation(PricingDetailsFields, PricingDetailsSchema)
|
||||||
|
) {
|
||||||
|
setTabState((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[tab]: "in-progress",
|
||||||
|
}))
|
||||||
|
setTab(tab)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setTabState((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[tab]: "completed",
|
||||||
|
}))
|
||||||
|
} else if (tab === Tab.PRODUCT) {
|
||||||
|
if (
|
||||||
|
!partialFormValidation(PricingProductsFields, PricingProductsSchema)
|
||||||
|
) {
|
||||||
|
setTabState((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[tab]: "in-progress",
|
||||||
|
}))
|
||||||
|
setTab(tab)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setTabState((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[tab]: "completed",
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setTabState((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[tab]: "completed",
|
||||||
|
[update]: "in-progress",
|
||||||
|
}))
|
||||||
|
setTab(update)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleNextTab = (tab: Tab) => {
|
||||||
|
if (tabOrder.indexOf(tab) + 1 >= tabOrder.length) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextTab = tabOrder[tabOrder.indexOf(tab) + 1]
|
||||||
|
handleChangeTab(nextTab)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RouteFocusModal.Form form={form}>
|
||||||
|
<ProgressTabs
|
||||||
|
value={tab}
|
||||||
|
onValueChange={(tab) => handleChangeTab(tab as Tab)}
|
||||||
|
className="flex h-full flex-col overflow-hidden"
|
||||||
|
>
|
||||||
|
<form onSubmit={handleSubmit} className="flex h-full flex-col">
|
||||||
|
<RouteFocusModal.Header>
|
||||||
|
<div className="flex w-full items-center justify-between gap-x-4">
|
||||||
|
<div className="-my-2 w-full max-w-[400px] border-l">
|
||||||
|
<ProgressTabs.List className="grid w-full grid-cols-3">
|
||||||
|
<ProgressTabs.Trigger
|
||||||
|
status={tabState.detail}
|
||||||
|
value={Tab.DETAIL}
|
||||||
|
>
|
||||||
|
Details
|
||||||
|
</ProgressTabs.Trigger>
|
||||||
|
<ProgressTabs.Trigger
|
||||||
|
status={tabState.product}
|
||||||
|
value={Tab.PRODUCT}
|
||||||
|
>
|
||||||
|
Products
|
||||||
|
</ProgressTabs.Trigger>
|
||||||
|
<ProgressTabs.Trigger
|
||||||
|
status={tabState.price}
|
||||||
|
value={Tab.PRICE}
|
||||||
|
>
|
||||||
|
Prices
|
||||||
|
</ProgressTabs.Trigger>
|
||||||
|
</ProgressTabs.List>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-end gap-x-2">
|
||||||
|
<RouteFocusModal.Close asChild>
|
||||||
|
<Button variant="secondary" size="small">
|
||||||
|
{t("actions.cancel")}
|
||||||
|
</Button>
|
||||||
|
</RouteFocusModal.Close>
|
||||||
|
<PrimaryButton
|
||||||
|
tab={tab}
|
||||||
|
next={handleNextTab}
|
||||||
|
isLoading={isPending}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</RouteFocusModal.Header>
|
||||||
|
<RouteFocusModal.Body className="size-full overflow-hidden">
|
||||||
|
<ProgressTabs.Content
|
||||||
|
className="size-full overflow-y-auto"
|
||||||
|
value={Tab.DETAIL}
|
||||||
|
>
|
||||||
|
<PricingDetailsForm form={form} />
|
||||||
|
</ProgressTabs.Content>
|
||||||
|
<ProgressTabs.Content
|
||||||
|
className="size-full overflow-y-auto"
|
||||||
|
value={Tab.PRODUCT}
|
||||||
|
>
|
||||||
|
<PricingProductsForm form={form} />
|
||||||
|
</ProgressTabs.Content>
|
||||||
|
<ProgressTabs.Content
|
||||||
|
className="size-full overflow-hidden"
|
||||||
|
value={Tab.PRICE}
|
||||||
|
>
|
||||||
|
<PricingPricesForm form={form} />
|
||||||
|
</ProgressTabs.Content>
|
||||||
|
</RouteFocusModal.Body>
|
||||||
|
</form>
|
||||||
|
</ProgressTabs>
|
||||||
|
</RouteFocusModal.Form>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
type PrimaryButtonProps = {
|
||||||
|
tab: Tab
|
||||||
|
next: (tab: Tab) => void
|
||||||
|
isLoading?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const PrimaryButton = ({ tab, next, isLoading }: PrimaryButtonProps) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
if (tab === Tab.PRICE) {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
key="submit-button"
|
||||||
|
type="submit"
|
||||||
|
variant="primary"
|
||||||
|
size="small"
|
||||||
|
isLoading={isLoading}
|
||||||
|
>
|
||||||
|
{t("actions.save")}
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
key="next-button"
|
||||||
|
type="button"
|
||||||
|
variant="primary"
|
||||||
|
size="small"
|
||||||
|
onClick={() => next(tab)}
|
||||||
|
>
|
||||||
|
{t("actions.continue")}
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
+536
@@ -0,0 +1,536 @@
|
|||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Checkbox,
|
||||||
|
DatePicker,
|
||||||
|
Heading,
|
||||||
|
Input,
|
||||||
|
RadioGroup,
|
||||||
|
Switch,
|
||||||
|
Text,
|
||||||
|
Textarea,
|
||||||
|
} from "@medusajs/ui"
|
||||||
|
import * as Collapsible from "@radix-ui/react-collapsible"
|
||||||
|
import { useFieldArray, type UseFormReturn } from "react-hook-form"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
|
||||||
|
import { XMarkMini } from "@medusajs/icons"
|
||||||
|
import { CustomerGroupDTO } from "@medusajs/types"
|
||||||
|
import { keepPreviousData } from "@tanstack/react-query"
|
||||||
|
import {
|
||||||
|
OnChangeFn,
|
||||||
|
RowSelectionState,
|
||||||
|
createColumnHelper,
|
||||||
|
} from "@tanstack/react-table"
|
||||||
|
import { t } from "i18next"
|
||||||
|
import { useEffect, useMemo, useState } from "react"
|
||||||
|
import { Divider } from "../../../../../components/common/divider"
|
||||||
|
import { Form } from "../../../../../components/common/form"
|
||||||
|
import { SplitView } from "../../../../../components/layout/split-view"
|
||||||
|
import { DataTable } from "../../../../../components/table/data-table"
|
||||||
|
import { useCustomerGroups } from "../../../../../hooks/api/customer-groups"
|
||||||
|
import { useCustomerGroupTableColumns } from "../../../../../hooks/table/columns/use-customer-group-table-columns"
|
||||||
|
import { useCustomerGroupTableQuery } from "../../../../../hooks/table/query/use-customer-group-table-query"
|
||||||
|
import { useDataTable } from "../../../../../hooks/use-data-table"
|
||||||
|
import type {
|
||||||
|
PricingCreateSchemaType,
|
||||||
|
PricingCustomerGroupsArrayType,
|
||||||
|
} from "./schema"
|
||||||
|
|
||||||
|
type PricingDetailsFormProps = {
|
||||||
|
form: UseFormReturn<PricingCreateSchemaType>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PricingDetailsForm = ({ form }: PricingDetailsFormProps) => {
|
||||||
|
const [open, setOpen] = useState(false)
|
||||||
|
const [showCustomerGroups, setShowCustomerGroups] = useState(
|
||||||
|
!!form.getValues("customer_group_ids")?.length
|
||||||
|
)
|
||||||
|
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
const { fields, remove, append } = useFieldArray({
|
||||||
|
control: form.control,
|
||||||
|
name: "customer_group_ids",
|
||||||
|
keyName: "cg_id",
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleAddCustomerGroup = (groups: PricingCustomerGroupsArrayType) => {
|
||||||
|
const newIds = groups.map((group) => group.id)
|
||||||
|
|
||||||
|
const fieldsToAdd = groups.filter(
|
||||||
|
(group) => !fields.some((field) => field.id === group.id)
|
||||||
|
)
|
||||||
|
|
||||||
|
for (const field of fields) {
|
||||||
|
if (!newIds.includes(field.id)) {
|
||||||
|
remove(fields.indexOf(field))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
append(fieldsToAdd)
|
||||||
|
setOpen(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleOpenDrawer = () => {
|
||||||
|
setOpen(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleShowCustomerGroups = (open: boolean) => {
|
||||||
|
if (!open) {
|
||||||
|
form.setValue("customer_group_ids", [])
|
||||||
|
}
|
||||||
|
|
||||||
|
setShowCustomerGroups(open)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SplitView open={open} onOpenChange={setOpen}>
|
||||||
|
<SplitView.Content>
|
||||||
|
<div className="flex flex-1 flex-col items-center overflow-y-auto">
|
||||||
|
<div className="flex w-full max-w-[720px] flex-col gap-y-8 px-2 py-16">
|
||||||
|
<div>
|
||||||
|
<Heading>Create Price List</Heading>
|
||||||
|
<Text size="small" className="text-ui-fg-subtle">
|
||||||
|
Create a new price list to manage the prices of your products.
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<Form.Field
|
||||||
|
control={form.control}
|
||||||
|
name="type"
|
||||||
|
render={({ field: { onChange, ...rest } }) => {
|
||||||
|
return (
|
||||||
|
<Form.Item>
|
||||||
|
<div className="flex flex-col gap-y-4">
|
||||||
|
<div>
|
||||||
|
<Form.Label>{t("fields.type")}</Form.Label>
|
||||||
|
<Form.Hint>
|
||||||
|
Choose the type of price list you want to create.
|
||||||
|
</Form.Hint>
|
||||||
|
</div>
|
||||||
|
<Form.Control>
|
||||||
|
<RadioGroup
|
||||||
|
onValueChange={onChange}
|
||||||
|
{...rest}
|
||||||
|
className="grid grid-cols-1 gap-4 md:grid-cols-2"
|
||||||
|
>
|
||||||
|
<RadioGroup.ChoiceBox
|
||||||
|
value={"sale"}
|
||||||
|
label="Sale"
|
||||||
|
description="Choose this if you are creating a sale"
|
||||||
|
/>
|
||||||
|
<RadioGroup.ChoiceBox
|
||||||
|
value={"override"}
|
||||||
|
label="Override"
|
||||||
|
description="Choose this if you are creating an override"
|
||||||
|
/>
|
||||||
|
</RadioGroup>
|
||||||
|
</Form.Control>
|
||||||
|
</div>
|
||||||
|
<Form.ErrorMessage />
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="flex flex-col gap-y-4">
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<Form.Field
|
||||||
|
control={form.control}
|
||||||
|
name="title"
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<Form.Item>
|
||||||
|
<Form.Label>{t("fields.title")}</Form.Label>
|
||||||
|
<Form.Control>
|
||||||
|
<Input {...field} />
|
||||||
|
</Form.Control>
|
||||||
|
<Form.ErrorMessage />
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Form.Field
|
||||||
|
control={form.control}
|
||||||
|
name="description"
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<Form.Item>
|
||||||
|
<Form.Label>{t("fields.description")}</Form.Label>
|
||||||
|
<Form.Control>
|
||||||
|
<Textarea {...field} />
|
||||||
|
</Form.Control>
|
||||||
|
<Form.ErrorMessage />
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Divider />
|
||||||
|
<Form.Field
|
||||||
|
control={form.control}
|
||||||
|
name="starts_at"
|
||||||
|
render={({ field: { value, onChange, ...rest } }) => {
|
||||||
|
const handleSwitchChange = (checked: boolean) => {
|
||||||
|
if (!checked) {
|
||||||
|
onChange(null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = new Date()
|
||||||
|
|
||||||
|
onChange(now)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form.Item>
|
||||||
|
<Collapsible.Root
|
||||||
|
open={!!value}
|
||||||
|
onOpenChange={handleSwitchChange}
|
||||||
|
>
|
||||||
|
<div className="grid grid-cols-[1fr_32px] gap-4">
|
||||||
|
<div>
|
||||||
|
<Text size="small" leading="compact" weight="plus">
|
||||||
|
Price list has a start date?
|
||||||
|
</Text>
|
||||||
|
<Text size="small" className="text-ui-fg-subtle">
|
||||||
|
Schedule the price list to activate in the future.
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<Collapsible.Trigger asChild>
|
||||||
|
<Switch name={rest.name} checked={!!value} />
|
||||||
|
</Collapsible.Trigger>
|
||||||
|
</div>
|
||||||
|
<Collapsible.Content>
|
||||||
|
<div className="flex flex-col gap-y-2 pt-4">
|
||||||
|
<Form.Label className="!txt-small text-ui-fg-subtle">
|
||||||
|
{t("fields.startDate")}
|
||||||
|
</Form.Label>
|
||||||
|
<Form.Control>
|
||||||
|
<DatePicker
|
||||||
|
value={value || undefined}
|
||||||
|
onChange={onChange}
|
||||||
|
{...rest}
|
||||||
|
/>
|
||||||
|
</Form.Control>
|
||||||
|
</div>
|
||||||
|
</Collapsible.Content>
|
||||||
|
<Form.ErrorMessage />
|
||||||
|
</Collapsible.Root>
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Divider />
|
||||||
|
<Form.Field
|
||||||
|
control={form.control}
|
||||||
|
name="ends_at"
|
||||||
|
render={({ field: { value, onChange, ...rest } }) => {
|
||||||
|
const handleSwitchChange = (checked: boolean) => {
|
||||||
|
if (!checked) {
|
||||||
|
onChange(null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const inAWeek = new Date(
|
||||||
|
new Date().setDate(new Date().getDate() + 7)
|
||||||
|
)
|
||||||
|
|
||||||
|
onChange(inAWeek)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form.Item>
|
||||||
|
<Collapsible.Root
|
||||||
|
open={!!value}
|
||||||
|
onOpenChange={handleSwitchChange}
|
||||||
|
>
|
||||||
|
<div className="grid grid-cols-[1fr_32px] gap-4">
|
||||||
|
<div>
|
||||||
|
<Text size="small" leading="compact" weight="plus">
|
||||||
|
Price list has an end date?
|
||||||
|
</Text>
|
||||||
|
<Text size="small" className="text-ui-fg-subtle">
|
||||||
|
Schedule the price list to deactivate in the future.
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<Collapsible.Trigger asChild>
|
||||||
|
<Switch name={rest.name} checked={!!value} />
|
||||||
|
</Collapsible.Trigger>
|
||||||
|
</div>
|
||||||
|
<Collapsible.Content>
|
||||||
|
<div className="flex flex-col gap-y-2 pt-4">
|
||||||
|
<Form.Label className="!txt-small text-ui-fg-subtle">
|
||||||
|
{t("fields.endDate")}
|
||||||
|
</Form.Label>
|
||||||
|
<Form.Control>
|
||||||
|
<DatePicker
|
||||||
|
value={value || undefined}
|
||||||
|
onChange={onChange}
|
||||||
|
{...rest}
|
||||||
|
/>
|
||||||
|
</Form.Control>
|
||||||
|
</div>
|
||||||
|
</Collapsible.Content>
|
||||||
|
<Form.ErrorMessage />
|
||||||
|
</Collapsible.Root>
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Divider />
|
||||||
|
<div>
|
||||||
|
<Collapsible.Root
|
||||||
|
open={showCustomerGroups}
|
||||||
|
onOpenChange={handleShowCustomerGroups}
|
||||||
|
>
|
||||||
|
<Form.Field
|
||||||
|
control={form.control}
|
||||||
|
name="customer_group_ids"
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<Form.Item>
|
||||||
|
<div className="grid grid-cols-[1fr_32px] items-start gap-4">
|
||||||
|
<div>
|
||||||
|
<Form.Label optional>
|
||||||
|
Customer availability
|
||||||
|
</Form.Label>
|
||||||
|
<Form.Hint>
|
||||||
|
Specify which customer groups the price overrides
|
||||||
|
should apply for.
|
||||||
|
</Form.Hint>
|
||||||
|
</div>
|
||||||
|
<Form.Control>
|
||||||
|
<Collapsible.Trigger asChild>
|
||||||
|
<Switch
|
||||||
|
name={field.name}
|
||||||
|
checked={showCustomerGroups}
|
||||||
|
/>
|
||||||
|
</Collapsible.Trigger>
|
||||||
|
</Form.Control>
|
||||||
|
</div>
|
||||||
|
<Form.ErrorMessage />
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Collapsible.Content>
|
||||||
|
<div className="flex flex-col pt-4">
|
||||||
|
{fields.length > 0 ? (
|
||||||
|
fields.map((field, index) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={field.cg_id}
|
||||||
|
className="bg-ui-bg-field shadow-borders-base transition-fg hover:bg-ui-bg-field-hover flex h-7 w-fit items-center overflow-hidden rounded-md"
|
||||||
|
>
|
||||||
|
<div className="txt-compact-small-plus flex h-full select-none items-center justify-center px-2 py-0.5">
|
||||||
|
{field.name}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => remove(index)}
|
||||||
|
className="focus-visible:bg-ui-bg-field-hover transition-fg hover:bg-ui-bg-field-hover flex h-full w-7 items-center justify-center border-l outline-none"
|
||||||
|
>
|
||||||
|
<XMarkMini className="text-ui-fg-muted" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center justify-center px-2 py-3">
|
||||||
|
<Text
|
||||||
|
size="small"
|
||||||
|
leading="compact"
|
||||||
|
className="text-ui-fg-muted"
|
||||||
|
>
|
||||||
|
No customer groups selected.
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="flex items-center justify-end">
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
variant="secondary"
|
||||||
|
type="button"
|
||||||
|
onClick={handleOpenDrawer}
|
||||||
|
>
|
||||||
|
Add customer groups
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Collapsible.Content>
|
||||||
|
</Collapsible.Root>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</SplitView.Content>
|
||||||
|
<CustomerGroupDrawer
|
||||||
|
selectedCustomerGroups={fields}
|
||||||
|
saveCustomerGroups={handleAddCustomerGroup}
|
||||||
|
/>
|
||||||
|
</SplitView>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const PAGE_SIZE = 50
|
||||||
|
const PREFIX = "cg"
|
||||||
|
|
||||||
|
const initRowSelection = (
|
||||||
|
selectedCustomerGroups: PricingCustomerGroupsArrayType
|
||||||
|
) => {
|
||||||
|
return selectedCustomerGroups.reduce((acc, group) => {
|
||||||
|
acc[group.id] = true
|
||||||
|
return acc
|
||||||
|
}, {} as RowSelectionState)
|
||||||
|
}
|
||||||
|
|
||||||
|
const CustomerGroupDrawer = ({
|
||||||
|
selectedCustomerGroups,
|
||||||
|
saveCustomerGroups,
|
||||||
|
}: {
|
||||||
|
selectedCustomerGroups: PricingCustomerGroupsArrayType
|
||||||
|
saveCustomerGroups: (arr: PricingCustomerGroupsArrayType) => void
|
||||||
|
}) => {
|
||||||
|
const [rowSelection, setRowSelection] = useState<RowSelectionState>(
|
||||||
|
initRowSelection(selectedCustomerGroups)
|
||||||
|
)
|
||||||
|
const [intermediate, setIntermediate] =
|
||||||
|
useState<PricingCustomerGroupsArrayType>(selectedCustomerGroups)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// If the selected customer groups change outside of the drawer,
|
||||||
|
// update the row selection state and intermediate state
|
||||||
|
setRowSelection(initRowSelection(selectedCustomerGroups))
|
||||||
|
setIntermediate(selectedCustomerGroups)
|
||||||
|
}, [selectedCustomerGroups])
|
||||||
|
|
||||||
|
const { searchParams, raw } = useCustomerGroupTableQuery({
|
||||||
|
pageSize: PAGE_SIZE,
|
||||||
|
prefix: PREFIX,
|
||||||
|
})
|
||||||
|
const { customer_groups, count, isLoading, isError, error } =
|
||||||
|
useCustomerGroups(searchParams, {
|
||||||
|
placeholderData: keepPreviousData,
|
||||||
|
})
|
||||||
|
|
||||||
|
const updater: OnChangeFn<RowSelectionState> = (value) => {
|
||||||
|
const state = typeof value === "function" ? value(rowSelection) : value
|
||||||
|
const currentIds = Object.keys(rowSelection)
|
||||||
|
|
||||||
|
const ids = Object.keys(state)
|
||||||
|
|
||||||
|
const newIds = ids.filter((id) => !currentIds.includes(id))
|
||||||
|
const removedIds = currentIds.filter((id) => !ids.includes(id))
|
||||||
|
|
||||||
|
const newCustomerGroups =
|
||||||
|
customer_groups
|
||||||
|
?.filter((cg) => newIds.includes(cg.id))
|
||||||
|
.map((cg) => ({ id: cg.id, name: cg.name })) || []
|
||||||
|
|
||||||
|
const filteredIntermediate = intermediate.filter(
|
||||||
|
(cg) => !removedIds.includes(cg.id)
|
||||||
|
)
|
||||||
|
|
||||||
|
setIntermediate([...filteredIntermediate, ...newCustomerGroups])
|
||||||
|
setRowSelection(state)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSave = () => {
|
||||||
|
saveCustomerGroups(intermediate)
|
||||||
|
}
|
||||||
|
|
||||||
|
const columns = useColumns()
|
||||||
|
|
||||||
|
const { table } = useDataTable({
|
||||||
|
data: customer_groups || [],
|
||||||
|
columns,
|
||||||
|
count,
|
||||||
|
enablePagination: true,
|
||||||
|
enableRowSelection: true,
|
||||||
|
getRowId: (row) => row.id,
|
||||||
|
rowSelection: {
|
||||||
|
state: rowSelection,
|
||||||
|
updater,
|
||||||
|
},
|
||||||
|
pageSize: PAGE_SIZE,
|
||||||
|
prefix: PREFIX,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (isError) {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SplitView.Drawer>
|
||||||
|
<div className="flex size-full flex-col overflow-hidden">
|
||||||
|
<DataTable
|
||||||
|
table={table}
|
||||||
|
columns={columns}
|
||||||
|
pageSize={PAGE_SIZE}
|
||||||
|
count={count}
|
||||||
|
isLoading={isLoading}
|
||||||
|
layout="fill"
|
||||||
|
pagination
|
||||||
|
search
|
||||||
|
prefix={PREFIX}
|
||||||
|
queryObject={raw}
|
||||||
|
/>
|
||||||
|
<div className="flex items-center justify-end gap-x-2 border-t p-4">
|
||||||
|
<SplitView.Close type="button" asChild>
|
||||||
|
<Button variant="secondary" size="small">
|
||||||
|
{t("actions.cancel")}
|
||||||
|
</Button>
|
||||||
|
</SplitView.Close>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="primary"
|
||||||
|
size="small"
|
||||||
|
onClick={handleSave}
|
||||||
|
>
|
||||||
|
{t("actions.add")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</SplitView.Drawer>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const columnHelper = createColumnHelper<CustomerGroupDTO>()
|
||||||
|
|
||||||
|
const useColumns = () => {
|
||||||
|
const base = useCustomerGroupTableColumns()
|
||||||
|
|
||||||
|
return useMemo(
|
||||||
|
() => [
|
||||||
|
columnHelper.display({
|
||||||
|
id: "select",
|
||||||
|
header: ({ table }) => {
|
||||||
|
return (
|
||||||
|
<Checkbox
|
||||||
|
checked={
|
||||||
|
table.getIsSomePageRowsSelected()
|
||||||
|
? "indeterminate"
|
||||||
|
: table.getIsAllPageRowsSelected()
|
||||||
|
}
|
||||||
|
onCheckedChange={(value) =>
|
||||||
|
table.toggleAllPageRowsSelected(!!value)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
cell: ({ row }) => {
|
||||||
|
return (
|
||||||
|
<Checkbox
|
||||||
|
checked={row.getIsSelected()}
|
||||||
|
onCheckedChange={(value) => row.toggleSelected(!!value)}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
...base,
|
||||||
|
],
|
||||||
|
[base]
|
||||||
|
)
|
||||||
|
}
|
||||||
+119
@@ -0,0 +1,119 @@
|
|||||||
|
import { useEffect } from "react"
|
||||||
|
import { UseFormReturn, useWatch } from "react-hook-form"
|
||||||
|
import { DataGrid } from "../../../../../components/grid/data-grid"
|
||||||
|
import { useCurrencies } from "../../../../../hooks/api/currencies"
|
||||||
|
import { useProducts } from "../../../../../hooks/api/products"
|
||||||
|
import { useStore } from "../../../../../hooks/api/store"
|
||||||
|
import { usePriceListGridColumns } from "../../../common/hooks/use-price-list-grid-columns"
|
||||||
|
import { PricingVariantsRecordType } from "../../../common/schemas"
|
||||||
|
import { isProductRow } from "../../../common/utils"
|
||||||
|
import { PricingCreateSchemaType } from "./schema"
|
||||||
|
|
||||||
|
type PricingPricesFormProps = {
|
||||||
|
form: UseFormReturn<PricingCreateSchemaType>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PricingPricesForm = ({ form }: PricingPricesFormProps) => {
|
||||||
|
const {
|
||||||
|
store,
|
||||||
|
isLoading: isStoreLoading,
|
||||||
|
isError: isStoreError,
|
||||||
|
error: storeError,
|
||||||
|
} = useStore()
|
||||||
|
|
||||||
|
const {
|
||||||
|
currencies,
|
||||||
|
isLoading: isCurrenciesLoading,
|
||||||
|
isError: isCurrencyError,
|
||||||
|
error: currencyError,
|
||||||
|
} = useCurrencies(
|
||||||
|
{
|
||||||
|
code: store?.supported_currency_codes,
|
||||||
|
limit: store?.supported_currency_codes?.length,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: !!store,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const ids = useWatch({
|
||||||
|
control: form.control,
|
||||||
|
name: "product_ids",
|
||||||
|
})
|
||||||
|
|
||||||
|
const existingProducts = useWatch({
|
||||||
|
control: form.control,
|
||||||
|
name: "products",
|
||||||
|
})
|
||||||
|
|
||||||
|
const { products, isLoading, isError, error } = useProducts({
|
||||||
|
id: ids.map((id) => id.id),
|
||||||
|
limit: ids.length,
|
||||||
|
fields: "title,thumbnail,*variants",
|
||||||
|
})
|
||||||
|
|
||||||
|
const { setValue } = form
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isLoading && products) {
|
||||||
|
products.forEach((product) => {
|
||||||
|
/**
|
||||||
|
* If the product already exists in the form, we don't want to overwrite it.
|
||||||
|
*/
|
||||||
|
if (existingProducts[product.id] || !product.variants) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setValue(`products.${product.id}.variants`, {
|
||||||
|
...product.variants.reduce((variants, variant) => {
|
||||||
|
variants[variant.id] = {
|
||||||
|
currency_prices: {},
|
||||||
|
region_prices: {},
|
||||||
|
}
|
||||||
|
return variants
|
||||||
|
}, {} as PricingVariantsRecordType),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, [products, existingProducts, isLoading, setValue])
|
||||||
|
|
||||||
|
const columns = usePriceListGridColumns({
|
||||||
|
currencies,
|
||||||
|
})
|
||||||
|
|
||||||
|
const initializing =
|
||||||
|
isLoading ||
|
||||||
|
isStoreLoading ||
|
||||||
|
isCurrenciesLoading ||
|
||||||
|
!products ||
|
||||||
|
!store ||
|
||||||
|
!currencies
|
||||||
|
|
||||||
|
if (isError) {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isStoreError) {
|
||||||
|
throw storeError
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isCurrencyError) {
|
||||||
|
throw currencyError
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex size-full flex-col divide-y overflow-hidden">
|
||||||
|
<DataGrid
|
||||||
|
columns={columns}
|
||||||
|
data={products}
|
||||||
|
getSubRows={(row) => {
|
||||||
|
if (isProductRow(row)) {
|
||||||
|
return row.variants
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
isLoading={initializing}
|
||||||
|
state={form}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
+174
@@ -0,0 +1,174 @@
|
|||||||
|
import { Checkbox } from "@medusajs/ui"
|
||||||
|
import { keepPreviousData } from "@tanstack/react-query"
|
||||||
|
import {
|
||||||
|
OnChangeFn,
|
||||||
|
RowSelectionState,
|
||||||
|
createColumnHelper,
|
||||||
|
} from "@tanstack/react-table"
|
||||||
|
import { useMemo, useState } from "react"
|
||||||
|
import { UseFormReturn, useWatch } from "react-hook-form"
|
||||||
|
import { DataTable } from "../../../../../components/table/data-table"
|
||||||
|
import { useProducts } from "../../../../../hooks/api/products"
|
||||||
|
import { useProductTableColumns } from "../../../../../hooks/table/columns/use-product-table-columns"
|
||||||
|
import { useProductTableFilters } from "../../../../../hooks/table/filters/use-product-table-filters"
|
||||||
|
import { useProductTableQuery } from "../../../../../hooks/table/query/use-product-table-query"
|
||||||
|
import { useDataTable } from "../../../../../hooks/use-data-table"
|
||||||
|
import { ExtendedProductDTO } from "../../../../../types/api-responses"
|
||||||
|
import { PricingCreateSchemaType, PricingProductsRecordType } from "./schema"
|
||||||
|
|
||||||
|
type PricingProductsFormProps = {
|
||||||
|
form: UseFormReturn<PricingCreateSchemaType>
|
||||||
|
}
|
||||||
|
|
||||||
|
const PAGE_SIZE = 50
|
||||||
|
const PREFIX = "p"
|
||||||
|
|
||||||
|
function getInitialSelection(products: { id: string }[]) {
|
||||||
|
return products.reduce((acc, curr) => {
|
||||||
|
acc[curr.id] = true
|
||||||
|
return acc
|
||||||
|
}, {} as RowSelectionState)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PricingProductsForm = ({ form }: PricingProductsFormProps) => {
|
||||||
|
const { control, setValue } = form
|
||||||
|
|
||||||
|
const selectedIds = useWatch({
|
||||||
|
control,
|
||||||
|
name: "product_ids",
|
||||||
|
})
|
||||||
|
|
||||||
|
const productRecords = useWatch({
|
||||||
|
control,
|
||||||
|
name: "products",
|
||||||
|
})
|
||||||
|
|
||||||
|
const [rowSelection, setRowSelection] = useState<RowSelectionState>(
|
||||||
|
getInitialSelection(selectedIds)
|
||||||
|
)
|
||||||
|
|
||||||
|
const { searchParams, raw } = useProductTableQuery({
|
||||||
|
pageSize: PAGE_SIZE,
|
||||||
|
prefix: PREFIX,
|
||||||
|
})
|
||||||
|
const { products, count, isLoading, isError, error } = useProducts(
|
||||||
|
searchParams,
|
||||||
|
{
|
||||||
|
placeholderData: keepPreviousData,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const updater: OnChangeFn<RowSelectionState> = (fn) => {
|
||||||
|
const state = typeof fn === "function" ? fn(rowSelection) : fn
|
||||||
|
|
||||||
|
const ids = Object.keys(state)
|
||||||
|
const productRecordKeys = Object.keys(productRecords)
|
||||||
|
|
||||||
|
const updatedRecords = productRecordKeys.reduce((acc, key) => {
|
||||||
|
if (ids.includes(key)) {
|
||||||
|
acc[key] = productRecords[key]
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc
|
||||||
|
}, {} as PricingProductsRecordType)
|
||||||
|
|
||||||
|
const update = ids.map((id) => ({ id }))
|
||||||
|
|
||||||
|
setValue("product_ids", update, { shouldDirty: true, shouldTouch: true })
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the product records to ensure that all unselected products
|
||||||
|
* are removed from the form state.
|
||||||
|
*/
|
||||||
|
setValue("products", updatedRecords, {
|
||||||
|
shouldDirty: true,
|
||||||
|
shouldTouch: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
setRowSelection(state)
|
||||||
|
}
|
||||||
|
|
||||||
|
const columns = useColumns()
|
||||||
|
const filters = useProductTableFilters()
|
||||||
|
|
||||||
|
const { table } = useDataTable({
|
||||||
|
data: products || [],
|
||||||
|
columns,
|
||||||
|
count,
|
||||||
|
enablePagination: true,
|
||||||
|
enableRowSelection: (row) => {
|
||||||
|
return row.original.variants.length > 0
|
||||||
|
},
|
||||||
|
getRowId: (row) => row.id,
|
||||||
|
rowSelection: {
|
||||||
|
state: rowSelection,
|
||||||
|
updater,
|
||||||
|
},
|
||||||
|
pageSize: PAGE_SIZE,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (isError) {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex size-full flex-col">
|
||||||
|
<DataTable
|
||||||
|
table={table}
|
||||||
|
columns={columns}
|
||||||
|
filters={filters}
|
||||||
|
pageSize={PAGE_SIZE}
|
||||||
|
prefix={PREFIX}
|
||||||
|
count={count}
|
||||||
|
isLoading={isLoading}
|
||||||
|
layout="fill"
|
||||||
|
orderBy={["title", "status", "created_at", "updated_at"]}
|
||||||
|
pagination
|
||||||
|
search
|
||||||
|
queryObject={raw}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const columnHelper = createColumnHelper<ExtendedProductDTO>()
|
||||||
|
|
||||||
|
const useColumns = () => {
|
||||||
|
const base = useProductTableColumns()
|
||||||
|
|
||||||
|
return useMemo(
|
||||||
|
() => [
|
||||||
|
columnHelper.display({
|
||||||
|
id: "select",
|
||||||
|
header: ({ table }) => {
|
||||||
|
return (
|
||||||
|
<Checkbox
|
||||||
|
checked={
|
||||||
|
table.getIsSomePageRowsSelected()
|
||||||
|
? "indeterminate"
|
||||||
|
: table.getIsAllPageRowsSelected()
|
||||||
|
}
|
||||||
|
onCheckedChange={(value) =>
|
||||||
|
table.toggleAllPageRowsSelected(!!value)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
cell: ({ row }) => {
|
||||||
|
return (
|
||||||
|
<Checkbox
|
||||||
|
checked={row.getIsSelected()}
|
||||||
|
disabled={!row.getCanSelect()}
|
||||||
|
onCheckedChange={(value) => row.toggleSelected(!!value)}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
...base,
|
||||||
|
],
|
||||||
|
[base]
|
||||||
|
)
|
||||||
|
}
|
||||||
+54
@@ -0,0 +1,54 @@
|
|||||||
|
import { z } from "zod"
|
||||||
|
import { PricingProductsRecordSchema } from "../../../common/schemas"
|
||||||
|
|
||||||
|
const PricingCustomerGroupsArray = z.array(
|
||||||
|
z.object({
|
||||||
|
id: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
export type PricingCustomerGroupsArrayType = z.infer<
|
||||||
|
typeof PricingCustomerGroupsArray
|
||||||
|
>
|
||||||
|
|
||||||
|
export const PricingCreateSchema = z.object({
|
||||||
|
type: z.enum(["sale", "override"]),
|
||||||
|
title: z.string().min(1),
|
||||||
|
description: z.string().min(1),
|
||||||
|
starts_at: z.date().nullable(),
|
||||||
|
ends_at: z.date().nullable(),
|
||||||
|
customer_group_ids: PricingCustomerGroupsArray.optional(),
|
||||||
|
product_ids: z.array(z.object({ id: z.string() })).min(1),
|
||||||
|
products: PricingProductsRecordSchema,
|
||||||
|
})
|
||||||
|
|
||||||
|
export type PricingCreateSchemaType = z.infer<typeof PricingCreateSchema>
|
||||||
|
|
||||||
|
export const PricingDetailsSchema = PricingCreateSchema.pick({
|
||||||
|
type: true,
|
||||||
|
title: true,
|
||||||
|
description: true,
|
||||||
|
starts_at: true,
|
||||||
|
ends_at: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
export const PricingDetailsFields = Object.keys(
|
||||||
|
PricingDetailsSchema.shape
|
||||||
|
) as (keyof typeof PricingDetailsSchema.shape)[]
|
||||||
|
|
||||||
|
export const PricingProductsSchema = PricingCreateSchema.pick({
|
||||||
|
product_ids: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
export const PricingProductsFields = Object.keys(
|
||||||
|
PricingProductsSchema.shape
|
||||||
|
) as (keyof typeof PricingProductsSchema.shape)[]
|
||||||
|
|
||||||
|
export const PricingPricesSchema = PricingCreateSchema.pick({
|
||||||
|
products: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
export const PricingPricesFields = Object.keys(
|
||||||
|
PricingPricesSchema.shape
|
||||||
|
) as (keyof typeof PricingPricesSchema.shape)[]
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { PricingCreate as Component } from "./pricing-create"
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { RouteFocusModal } from "../../../components/route-modal"
|
||||||
|
import { PricingCreateForm } from "./components/pricing-create-form"
|
||||||
|
|
||||||
|
export const PricingCreate = () => {
|
||||||
|
return (
|
||||||
|
<RouteFocusModal>
|
||||||
|
<PricingCreateForm />
|
||||||
|
</RouteFocusModal>
|
||||||
|
)
|
||||||
|
}
|
||||||
File renamed without changes.
+13
-13
@@ -1,21 +1,23 @@
|
|||||||
import { PencilSquare } from "@medusajs/icons"
|
import { PencilSquare } from "@medusajs/icons"
|
||||||
import { PriceList } from "@medusajs/medusa"
|
import { PriceListDTO } from "@medusajs/types"
|
||||||
import { Container, Heading, Text, Tooltip } from "@medusajs/ui"
|
import { Container, Heading, Text } from "@medusajs/ui"
|
||||||
import { format } from "date-fns"
|
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
import { ActionMenu } from "../../../../../components/common/action-menu"
|
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||||
|
import { useDate } from "../../../../../hooks/use-date"
|
||||||
|
|
||||||
type PricingConfigurationSectionProps = {
|
type PricingConfigurationSectionProps = {
|
||||||
priceList: PriceList
|
priceList: PriceListDTO
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PricingConfigurationSection = ({
|
export const PricingConfigurationSection = ({
|
||||||
priceList,
|
priceList,
|
||||||
}: PricingConfigurationSectionProps) => {
|
}: PricingConfigurationSectionProps) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
const { getFullDate } = useDate()
|
||||||
|
|
||||||
const firstCustomerGroups = priceList.customer_groups?.slice(0, 3)
|
// TODO: Customer groups are not available in the price list schema out-of-the-box atm.
|
||||||
const remainingCustomerGroups = priceList.customer_groups?.slice(3)
|
// const firstCustomerGroups = priceList.customer_groups?.slice(0, 3) || []
|
||||||
|
// const remainingCustomerGroups = priceList.customer_groups?.slice(3) || []
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container className="divide-y p-0">
|
<Container className="divide-y p-0">
|
||||||
@@ -27,7 +29,7 @@ export const PricingConfigurationSection = ({
|
|||||||
actions: [
|
actions: [
|
||||||
{
|
{
|
||||||
label: t("actions.edit"),
|
label: t("actions.edit"),
|
||||||
to: "configurations/edit",
|
to: "configuration",
|
||||||
icon: <PencilSquare />,
|
icon: <PencilSquare />,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -41,7 +43,7 @@ export const PricingConfigurationSection = ({
|
|||||||
</Text>
|
</Text>
|
||||||
<Text size="small" className="text-pretty">
|
<Text size="small" className="text-pretty">
|
||||||
{priceList.starts_at
|
{priceList.starts_at
|
||||||
? format(new Date(priceList.starts_at), "dd MMM yyyy")
|
? getFullDate({ date: priceList.starts_at })
|
||||||
: "-"}
|
: "-"}
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
@@ -50,12 +52,10 @@ export const PricingConfigurationSection = ({
|
|||||||
{t("fields.endDate")}
|
{t("fields.endDate")}
|
||||||
</Text>
|
</Text>
|
||||||
<Text size="small" className="text-pretty">
|
<Text size="small" className="text-pretty">
|
||||||
{priceList.ends_at
|
{priceList.ends_at ? getFullDate({ date: priceList.ends_at }) : "-"}
|
||||||
? format(new Date(priceList.ends_at), "dd MMM yyyy")
|
|
||||||
: "-"}
|
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-ui-fg-subtle grid grid-cols-2 items-center px-6 py-4">
|
{/* <div className="text-ui-fg-subtle grid grid-cols-2 items-center px-6 py-4">
|
||||||
<Text leading="compact" size="small" weight="plus">
|
<Text leading="compact" size="small" weight="plus">
|
||||||
{t("pricing.settings.customerGroupsLabel")}
|
{t("pricing.settings.customerGroupsLabel")}
|
||||||
</Text>
|
</Text>
|
||||||
@@ -84,7 +84,7 @@ export const PricingConfigurationSection = ({
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div> */}
|
||||||
</Container>
|
</Container>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
File renamed without changes.
+6
-6
@@ -1,14 +1,14 @@
|
|||||||
import { PencilSquare, Trash } from "@medusajs/icons"
|
import { PencilSquare, Trash } from "@medusajs/icons"
|
||||||
import { PriceList } from "@medusajs/medusa"
|
import { PriceListDTO } from "@medusajs/types"
|
||||||
import { Container, Heading, StatusBadge, Text, usePrompt } from "@medusajs/ui"
|
import { Container, Heading, StatusBadge, Text, usePrompt } from "@medusajs/ui"
|
||||||
import { useAdminDeletePriceList } from "medusa-react"
|
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
import { useNavigate } from "react-router-dom"
|
import { useNavigate } from "react-router-dom"
|
||||||
import { ActionMenu } from "../../../../../components/common/action-menu"
|
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||||
|
import { useDeletePriceList } from "../../../../../hooks/api/price-lists"
|
||||||
import { getPriceListStatus } from "../../../common/utils"
|
import { getPriceListStatus } from "../../../common/utils"
|
||||||
|
|
||||||
type PricingGeneralSectionProps = {
|
type PricingGeneralSectionProps = {
|
||||||
priceList: PriceList
|
priceList: PriceListDTO
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PricingGeneralSection = ({
|
export const PricingGeneralSection = ({
|
||||||
@@ -18,7 +18,7 @@ export const PricingGeneralSection = ({
|
|||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const prompt = usePrompt()
|
const prompt = usePrompt()
|
||||||
|
|
||||||
const { mutateAsync } = useAdminDeletePriceList(priceList.id)
|
const { mutateAsync } = useDeletePriceList(priceList.id)
|
||||||
|
|
||||||
const overrideCount = priceList.prices?.length || 0
|
const overrideCount = priceList.prices?.length || 0
|
||||||
|
|
||||||
@@ -28,7 +28,7 @@ export const PricingGeneralSection = ({
|
|||||||
const res = await prompt({
|
const res = await prompt({
|
||||||
title: t("general.areYouSure"),
|
title: t("general.areYouSure"),
|
||||||
description: t("pricing.deletePriceListWarning", {
|
description: t("pricing.deletePriceListWarning", {
|
||||||
name: priceList.name,
|
name: priceList.title,
|
||||||
}),
|
}),
|
||||||
confirmText: t("actions.delete"),
|
confirmText: t("actions.delete"),
|
||||||
cancelText: t("actions.cancel"),
|
cancelText: t("actions.cancel"),
|
||||||
@@ -53,7 +53,7 @@ export const PricingGeneralSection = ({
|
|||||||
return (
|
return (
|
||||||
<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>{priceList.name}</Heading>
|
<Heading>{priceList.title}</Heading>
|
||||||
<div className="flex items-center gap-x-4">
|
<div className="flex items-center gap-x-4">
|
||||||
<StatusBadge color={color}>{text}</StatusBadge>
|
<StatusBadge color={color}>{text}</StatusBadge>
|
||||||
<ActionMenu
|
<ActionMenu
|
||||||
File renamed without changes.
+48
-24
@@ -1,23 +1,22 @@
|
|||||||
import { PencilSquare, Plus } from "@medusajs/icons"
|
import { PencilSquare, Plus, Trash } from "@medusajs/icons"
|
||||||
import { PriceList, Product } from "@medusajs/medusa"
|
import { PriceListDTO } from "@medusajs/types"
|
||||||
import { Checkbox, Container, Heading, usePrompt } from "@medusajs/ui"
|
import { Checkbox, Container, Heading, usePrompt } from "@medusajs/ui"
|
||||||
|
import { keepPreviousData } from "@tanstack/react-query"
|
||||||
import { RowSelectionState, createColumnHelper } from "@tanstack/react-table"
|
import { RowSelectionState, createColumnHelper } from "@tanstack/react-table"
|
||||||
import {
|
|
||||||
useAdminDeletePriceListProductsPrices,
|
|
||||||
useAdminPriceListProducts,
|
|
||||||
} from "medusa-react"
|
|
||||||
import { useMemo, useState } from "react"
|
import { useMemo, useState } from "react"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
import { useNavigate } from "react-router-dom"
|
import { useNavigate } from "react-router-dom"
|
||||||
import { ActionMenu } from "../../../../../components/common/action-menu"
|
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||||
import { DataTable } from "../../../../../components/table/data-table"
|
import { DataTable } from "../../../../../components/table/data-table"
|
||||||
|
import { useProducts } from "../../../../../hooks/api/products"
|
||||||
import { useProductTableColumns } from "../../../../../hooks/table/columns/use-product-table-columns"
|
import { useProductTableColumns } from "../../../../../hooks/table/columns/use-product-table-columns"
|
||||||
import { useProductTableFilters } from "../../../../../hooks/table/filters/use-product-table-filters"
|
import { useProductTableFilters } from "../../../../../hooks/table/filters/use-product-table-filters"
|
||||||
import { useProductTableQuery } from "../../../../../hooks/table/query/use-product-table-query"
|
import { useProductTableQuery } from "../../../../../hooks/table/query/use-product-table-query"
|
||||||
import { useDataTable } from "../../../../../hooks/use-data-table"
|
import { useDataTable } from "../../../../../hooks/use-data-table"
|
||||||
|
import { ExtendedProductDTO } from "../../../../../types/api-responses"
|
||||||
|
|
||||||
type PricingProductSectionProps = {
|
type PricingProductSectionProps = {
|
||||||
priceList: PriceList
|
priceList: PriceListDTO
|
||||||
}
|
}
|
||||||
|
|
||||||
const PAGE_SIZE = 10
|
const PAGE_SIZE = 10
|
||||||
@@ -36,23 +35,21 @@ export const PricingProductSection = ({
|
|||||||
pageSize: PAGE_SIZE,
|
pageSize: PAGE_SIZE,
|
||||||
prefix: PREFIX,
|
prefix: PREFIX,
|
||||||
})
|
})
|
||||||
const { products, count, isLoading, isError, error } =
|
const { products, count, isLoading, isError, error } = useProducts(
|
||||||
useAdminPriceListProducts(
|
{
|
||||||
priceList.id,
|
...searchParams,
|
||||||
{
|
price_list_id: [priceList.id],
|
||||||
...searchParams,
|
},
|
||||||
expand: "variants,sales_channels,collection",
|
{
|
||||||
},
|
placeholderData: keepPreviousData,
|
||||||
{
|
}
|
||||||
keepPreviousData: true,
|
)
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const filters = useProductTableFilters()
|
const filters = useProductTableFilters()
|
||||||
const columns = useColumns()
|
const columns = useColumns()
|
||||||
|
|
||||||
const { table } = useDataTable({
|
const { table } = useDataTable({
|
||||||
data: (products || []) as Product[],
|
data: products || [],
|
||||||
count,
|
count,
|
||||||
columns,
|
columns,
|
||||||
enablePagination: true,
|
enablePagination: true,
|
||||||
@@ -66,7 +63,6 @@ export const PricingProductSection = ({
|
|||||||
prefix: PREFIX,
|
prefix: PREFIX,
|
||||||
})
|
})
|
||||||
|
|
||||||
const { mutateAsync } = useAdminDeletePriceListProductsPrices(priceList.id)
|
|
||||||
const handleDelete = async () => {
|
const handleDelete = async () => {
|
||||||
const res = await prompt({
|
const res = await prompt({
|
||||||
title: t("general.areYouSure"),
|
title: t("general.areYouSure"),
|
||||||
@@ -81,9 +77,9 @@ export const PricingProductSection = ({
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
await mutateAsync({
|
// The endpoint to batch remove prices by product ids is not implemented in
|
||||||
product_ids: Object.keys(rowSelection),
|
// v2. We either need to implement it or remove the feature.
|
||||||
})
|
console.log("Not implemented yet.")
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleEdit = async () => {
|
const handleEdit = async () => {
|
||||||
@@ -149,7 +145,31 @@ export const PricingProductSection = ({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const columnHelper = createColumnHelper<Product>()
|
const ProductRowAction = ({ product }: { product: ExtendedProductDTO }) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
// TODO: The endpoint to remove prices by product id is not implemented in v2.
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ActionMenu
|
||||||
|
groups={[
|
||||||
|
{
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
icon: <Trash />,
|
||||||
|
label: t("actions.remove"),
|
||||||
|
onClick: () => {
|
||||||
|
console.log("Not implemented yet.")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const columnHelper = createColumnHelper<ExtendedProductDTO>()
|
||||||
|
|
||||||
const useColumns = () => {
|
const useColumns = () => {
|
||||||
const base = useProductTableColumns()
|
const base = useProductTableColumns()
|
||||||
@@ -185,6 +205,10 @@ const useColumns = () => {
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
...base,
|
...base,
|
||||||
|
columnHelper.display({
|
||||||
|
id: "actions",
|
||||||
|
cell: ({ row }) => <ProductRowAction product={row.original} />,
|
||||||
|
}),
|
||||||
],
|
],
|
||||||
[base]
|
[base]
|
||||||
)
|
)
|
||||||
+1
@@ -1 +1,2 @@
|
|||||||
|
export { pricingLoader as loader } from "./loader"
|
||||||
export { PricingDetail as Component } from "./pricing-detail"
|
export { PricingDetail as Component } from "./pricing-detail"
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { LoaderFunctionArgs } from "react-router-dom"
|
||||||
|
import { priceListsQueryKeys } from "../../../hooks/api/price-lists"
|
||||||
|
import { client } from "../../../lib/client"
|
||||||
|
import { queryClient } from "../../../lib/medusa"
|
||||||
|
import { PriceListRes } from "../../../types/api-responses"
|
||||||
|
|
||||||
|
const pricingDetailQuery = (id: string) => ({
|
||||||
|
queryKey: priceListsQueryKeys.detail(id),
|
||||||
|
queryFn: async () => client.priceLists.retrieve(id),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const pricingLoader = async ({ params }: LoaderFunctionArgs) => {
|
||||||
|
const id = params.id
|
||||||
|
const query = pricingDetailQuery(id!)
|
||||||
|
|
||||||
|
return (
|
||||||
|
queryClient.getQueryData<PriceListRes>(query.queryKey) ??
|
||||||
|
(await queryClient.fetchQuery(query))
|
||||||
|
)
|
||||||
|
}
|
||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
import { useAdminPriceList } from "medusa-react"
|
|
||||||
import { Outlet, useParams } from "react-router-dom"
|
import { Outlet, useParams } from "react-router-dom"
|
||||||
import { JsonViewSection } from "../../../components/common/json-view-section"
|
import { JsonViewSection } from "../../../components/common/json-view-section"
|
||||||
|
import { usePriceList } from "../../../hooks/api/price-lists"
|
||||||
import { PricingConfigurationSection } from "./components/pricing-configuration-section"
|
import { PricingConfigurationSection } from "./components/pricing-configuration-section"
|
||||||
import { PricingGeneralSection } from "./components/pricing-general-section"
|
import { PricingGeneralSection } from "./components/pricing-general-section"
|
||||||
import { PricingProductSection } from "./components/pricing-product-section"
|
import { PricingProductSection } from "./components/pricing-product-section"
|
||||||
@@ -8,7 +8,7 @@ import { PricingProductSection } from "./components/pricing-product-section"
|
|||||||
export const PricingDetail = () => {
|
export const PricingDetail = () => {
|
||||||
const { id } = useParams()
|
const { id } = useParams()
|
||||||
|
|
||||||
const { price_list, isLoading, isError, error } = useAdminPriceList(id!)
|
const { price_list, isLoading, isError, error } = usePriceList(id!)
|
||||||
|
|
||||||
if (isLoading || !price_list) {
|
if (isLoading || !price_list) {
|
||||||
return <div>Loading...</div>
|
return <div>Loading...</div>
|
||||||
+9
-37
@@ -1,7 +1,6 @@
|
|||||||
import { zodResolver } from "@hookform/resolvers/zod"
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
import { PriceList } from "@medusajs/medusa"
|
import { PriceListDTO } from "@medusajs/types"
|
||||||
import { Button, Input, RadioGroup, Switch, Textarea } from "@medusajs/ui"
|
import { Button, Input, RadioGroup, Textarea } from "@medusajs/ui"
|
||||||
import { useAdminUpdatePriceList } from "medusa-react"
|
|
||||||
import { useForm } from "react-hook-form"
|
import { useForm } from "react-hook-form"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
@@ -10,17 +9,17 @@ import {
|
|||||||
RouteDrawer,
|
RouteDrawer,
|
||||||
useRouteModal,
|
useRouteModal,
|
||||||
} from "../../../../../components/route-modal"
|
} from "../../../../../components/route-modal"
|
||||||
|
import { useUpdatePriceList } from "../../../../../hooks/api/price-lists"
|
||||||
import { PriceListType } from "../../../common/constants"
|
import { PriceListType } from "../../../common/constants"
|
||||||
|
|
||||||
type EditPriceListFormProps = {
|
type EditPriceListFormProps = {
|
||||||
priceList: PriceList
|
priceList: PriceListDTO
|
||||||
}
|
}
|
||||||
|
|
||||||
const EditPriceListFormSchema = z.object({
|
const EditPriceListFormSchema = z.object({
|
||||||
type: z.nativeEnum(PriceListType),
|
type: z.nativeEnum(PriceListType),
|
||||||
name: z.string().min(1),
|
title: z.string().min(1),
|
||||||
description: z.string().min(1),
|
description: z.string().min(1),
|
||||||
includes_tax: z.boolean(),
|
|
||||||
})
|
})
|
||||||
|
|
||||||
export const EditPriceListForm = ({ priceList }: EditPriceListFormProps) => {
|
export const EditPriceListForm = ({ priceList }: EditPriceListFormProps) => {
|
||||||
@@ -30,14 +29,13 @@ export const EditPriceListForm = ({ priceList }: EditPriceListFormProps) => {
|
|||||||
const form = useForm<z.infer<typeof EditPriceListFormSchema>>({
|
const form = useForm<z.infer<typeof EditPriceListFormSchema>>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
type: priceList.type,
|
type: priceList.type,
|
||||||
name: priceList.name,
|
title: priceList.title,
|
||||||
description: priceList.description,
|
description: priceList.description,
|
||||||
includes_tax: priceList.includes_tax,
|
|
||||||
},
|
},
|
||||||
resolver: zodResolver(EditPriceListFormSchema),
|
resolver: zodResolver(EditPriceListFormSchema),
|
||||||
})
|
})
|
||||||
|
|
||||||
const { mutateAsync } = useAdminUpdatePriceList(priceList.id)
|
const { mutateAsync } = useUpdatePriceList(priceList.id)
|
||||||
|
|
||||||
const handleSubmit = form.handleSubmit(async (values) => {
|
const handleSubmit = form.handleSubmit(async (values) => {
|
||||||
await mutateAsync(values, {
|
await mutateAsync(values, {
|
||||||
@@ -85,11 +83,11 @@ export const EditPriceListForm = ({ priceList }: EditPriceListFormProps) => {
|
|||||||
<div className="flex flex-col gap-y-4">
|
<div className="flex flex-col gap-y-4">
|
||||||
<Form.Field
|
<Form.Field
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="name"
|
name="title"
|
||||||
render={({ field }) => {
|
render={({ field }) => {
|
||||||
return (
|
return (
|
||||||
<Form.Item>
|
<Form.Item>
|
||||||
<Form.Label>{t("fields.name")}</Form.Label>
|
<Form.Label>{t("fields.title")}</Form.Label>
|
||||||
<Form.Control>
|
<Form.Control>
|
||||||
<Input {...field} />
|
<Input {...field} />
|
||||||
</Form.Control>
|
</Form.Control>
|
||||||
@@ -114,32 +112,6 @@ export const EditPriceListForm = ({ priceList }: EditPriceListFormProps) => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Form.Field
|
|
||||||
control={form.control}
|
|
||||||
name="includes_tax"
|
|
||||||
render={({ field: { value, onChange, ...field } }) => {
|
|
||||||
return (
|
|
||||||
<Form.Item>
|
|
||||||
<div>
|
|
||||||
<div className="flex items-start justify-between">
|
|
||||||
<Form.Label>{t("fields.taxInclusivePricing")}</Form.Label>
|
|
||||||
<Form.Control>
|
|
||||||
<Switch
|
|
||||||
{...field}
|
|
||||||
checked={value}
|
|
||||||
onCheckedChange={onChange}
|
|
||||||
/>
|
|
||||||
</Form.Control>
|
|
||||||
</div>
|
|
||||||
<Form.Hint>
|
|
||||||
{t("pricing.settings.taxInclusivePricingHint")}
|
|
||||||
</Form.Hint>
|
|
||||||
<Form.ErrorMessage />
|
|
||||||
</div>
|
|
||||||
</Form.Item>
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</RouteDrawer.Body>
|
</RouteDrawer.Body>
|
||||||
<RouteDrawer.Footer className="shrink-0">
|
<RouteDrawer.Footer className="shrink-0">
|
||||||
<div className="flex items-center justify-end gap-x-2">
|
<div className="flex items-center justify-end gap-x-2">
|
||||||
File renamed without changes.
File renamed without changes.
+2
-2
@@ -1,15 +1,15 @@
|
|||||||
import { Heading } from "@medusajs/ui"
|
import { Heading } from "@medusajs/ui"
|
||||||
import { useAdminPriceList } from "medusa-react"
|
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
import { useParams } from "react-router-dom"
|
import { useParams } from "react-router-dom"
|
||||||
import { RouteDrawer } from "../../../components/route-modal"
|
import { RouteDrawer } from "../../../components/route-modal"
|
||||||
|
import { usePriceList } from "../../../hooks/api/price-lists"
|
||||||
import { EditPriceListForm } from "./components/edit-price-list-form"
|
import { EditPriceListForm } from "./components/edit-price-list-form"
|
||||||
|
|
||||||
export const PricingEdit = () => {
|
export const PricingEdit = () => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { id } = useParams()
|
const { id } = useParams()
|
||||||
|
|
||||||
const { price_list, isLoading, isError, error } = useAdminPriceList(id!)
|
const { price_list, isLoading, isError, error } = usePriceList(id!)
|
||||||
|
|
||||||
const ready = !isLoading && price_list
|
const ready = !isLoading && price_list
|
||||||
|
|
||||||
File renamed without changes.
+8
-6
@@ -1,8 +1,9 @@
|
|||||||
import { Button, Container, Heading } from "@medusajs/ui"
|
import { Button, Container, Heading } from "@medusajs/ui"
|
||||||
import { useAdminPriceLists } from "medusa-react"
|
import { keepPreviousData } from "@tanstack/react-query"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
import { Link } from "react-router-dom"
|
import { Link } from "react-router-dom"
|
||||||
import { DataTable } from "../../../../../components/table/data-table"
|
import { DataTable } from "../../../../../components/table/data-table"
|
||||||
|
import { usePriceLists } from "../../../../../hooks/api/price-lists"
|
||||||
import { useDataTable } from "../../../../../hooks/use-data-table"
|
import { useDataTable } from "../../../../../hooks/use-data-table"
|
||||||
import { usePricingTableColumns } from "./use-pricing-table-columns"
|
import { usePricingTableColumns } from "./use-pricing-table-columns"
|
||||||
import { usePricingTableQuery } from "./use-pricing-table-query"
|
import { usePricingTableQuery } from "./use-pricing-table-query"
|
||||||
@@ -15,12 +16,13 @@ export const PricingListTable = () => {
|
|||||||
const { searchParams, raw } = usePricingTableQuery({
|
const { searchParams, raw } = usePricingTableQuery({
|
||||||
pageSize: PAGE_SIZE,
|
pageSize: PAGE_SIZE,
|
||||||
})
|
})
|
||||||
const { price_lists, count, isLoading, isError, error } = useAdminPriceLists(
|
const { price_lists, count, isLoading, isError, error } = usePriceLists(
|
||||||
|
// {
|
||||||
|
// ...searchParams, // The query params are not implemented, and any search params other than expand and fields will throw an error
|
||||||
|
// },
|
||||||
|
undefined,
|
||||||
{
|
{
|
||||||
...searchParams,
|
placeholderData: keepPreviousData,
|
||||||
},
|
|
||||||
{
|
|
||||||
keepPreviousData: true,
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
+5
-5
@@ -1,12 +1,12 @@
|
|||||||
import { PencilSquare, Trash } from "@medusajs/icons"
|
import { PencilSquare, Trash } from "@medusajs/icons"
|
||||||
import { PriceList } from "@medusajs/medusa"
|
import { PriceListDTO } from "@medusajs/types"
|
||||||
import { usePrompt } from "@medusajs/ui"
|
import { usePrompt } from "@medusajs/ui"
|
||||||
import { useAdminDeletePriceList } from "medusa-react"
|
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
import { ActionMenu } from "../../../../../components/common/action-menu"
|
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||||
|
import { useDeletePriceList } from "../../../../../hooks/api/price-lists"
|
||||||
|
|
||||||
type PricingTableActionsProps = {
|
type PricingTableActionsProps = {
|
||||||
priceList: PriceList
|
priceList: PriceListDTO
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PricingTableActions = ({
|
export const PricingTableActions = ({
|
||||||
@@ -15,13 +15,13 @@ export const PricingTableActions = ({
|
|||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const prompt = usePrompt()
|
const prompt = usePrompt()
|
||||||
|
|
||||||
const { mutateAsync } = useAdminDeletePriceList(priceList.id)
|
const { mutateAsync } = useDeletePriceList(priceList.id)
|
||||||
|
|
||||||
const handleDelete = async () => {
|
const handleDelete = async () => {
|
||||||
const res = await prompt({
|
const res = await prompt({
|
||||||
title: t("general.areYouSure"),
|
title: t("general.areYouSure"),
|
||||||
description: t("pricing.deletePriceListWarning", {
|
description: t("pricing.deletePriceListWarning", {
|
||||||
name: priceList.name,
|
name: priceList.title,
|
||||||
}),
|
}),
|
||||||
confirmText: t("actions.delete"),
|
confirmText: t("actions.delete"),
|
||||||
cancelText: t("actions.cancel"),
|
cancelText: t("actions.cancel"),
|
||||||
+5
-20
@@ -1,4 +1,4 @@
|
|||||||
import { PriceList } from "@medusajs/medusa"
|
import { PriceListDTO } from "@medusajs/types"
|
||||||
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"
|
||||||
@@ -6,32 +6,17 @@ import { StatusCell } from "../../../../../components/table/table-cells/common/s
|
|||||||
import { getPriceListStatus } from "../../../common/utils"
|
import { getPriceListStatus } from "../../../common/utils"
|
||||||
import { PricingTableActions } from "./pricing-table-actions"
|
import { PricingTableActions } from "./pricing-table-actions"
|
||||||
|
|
||||||
const columnHelper = createColumnHelper<PriceList>()
|
const columnHelper = createColumnHelper<PriceListDTO>()
|
||||||
|
|
||||||
export const usePricingTableColumns = () => {
|
export const usePricingTableColumns = () => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
return useMemo(
|
return useMemo(
|
||||||
() => [
|
() => [
|
||||||
columnHelper.accessor("name", {
|
columnHelper.accessor("title", {
|
||||||
header: t("fields.name"),
|
header: t("fields.name"),
|
||||||
cell: (info) => info.getValue(),
|
cell: (info) => info.getValue(),
|
||||||
}),
|
}),
|
||||||
columnHelper.accessor("type", {
|
|
||||||
header: t("fields.type"),
|
|
||||||
cell: ({ getValue }) => {
|
|
||||||
const label =
|
|
||||||
getValue() === "sale"
|
|
||||||
? t("pricing.type.sale")
|
|
||||||
: t("pricing.type.override")
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex size-full items-center overflow-hidden">
|
|
||||||
<span>{label}</span>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
columnHelper.accessor("status", {
|
columnHelper.accessor("status", {
|
||||||
header: t("fields.status"),
|
header: t("fields.status"),
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
@@ -40,8 +25,8 @@ export const usePricingTableColumns = () => {
|
|||||||
return <StatusCell color={color}>{text}</StatusCell>
|
return <StatusCell color={color}>{text}</StatusCell>
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
columnHelper.accessor("customer_groups", {
|
columnHelper.accessor("prices", {
|
||||||
header: t("customerGroups.domain"),
|
header: t("fields.prices"),
|
||||||
cell: (info) => info.getValue()?.length || "-",
|
cell: (info) => info.getValue()?.length || "-",
|
||||||
}),
|
}),
|
||||||
columnHelper.display({
|
columnHelper.display({
|
||||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+1
@@ -0,0 +1 @@
|
|||||||
|
export * from "./pricing-products-prices-form"
|
||||||
+388
@@ -0,0 +1,388 @@
|
|||||||
|
import {
|
||||||
|
CreatePriceListPriceDTO,
|
||||||
|
PriceListDTO,
|
||||||
|
UpdatePriceListPriceDTO,
|
||||||
|
} from "@medusajs/types"
|
||||||
|
import { Button } from "@medusajs/ui"
|
||||||
|
import { UseFormReturn, useForm, useWatch } from "react-hook-form"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
import { z } from "zod"
|
||||||
|
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
|
import { useEffect, useMemo } from "react"
|
||||||
|
import { DataGrid } from "../../../../../components/grid/data-grid"
|
||||||
|
import {
|
||||||
|
RouteFocusModal,
|
||||||
|
useRouteModal,
|
||||||
|
} from "../../../../../components/route-modal"
|
||||||
|
import { useCurrencies } from "../../../../../hooks/api/currencies"
|
||||||
|
import {
|
||||||
|
usePriceListAddPrices,
|
||||||
|
usePriceListRemovePrices,
|
||||||
|
useUpdatePriceList,
|
||||||
|
} from "../../../../../hooks/api/price-lists"
|
||||||
|
import { useStore } from "../../../../../hooks/api/store"
|
||||||
|
import { castNumber } from "../../../../../lib/cast-number"
|
||||||
|
import {
|
||||||
|
getDbAmount,
|
||||||
|
getPresentationalAmount,
|
||||||
|
} from "../../../../../lib/money-amount-helpers"
|
||||||
|
import { ExtendedProductDTO } from "../../../../../types/api-responses"
|
||||||
|
import { usePriceListGridColumns } from "../../../common/hooks/use-price-list-grid-columns"
|
||||||
|
import {
|
||||||
|
PricingProductsRecordSchema,
|
||||||
|
PricingVariantsRecordType,
|
||||||
|
} from "../../../common/schemas"
|
||||||
|
import { isProductRow } from "../../../common/utils"
|
||||||
|
|
||||||
|
type PricingProductPricesFormProps = {
|
||||||
|
priceList: PriceListDTO
|
||||||
|
products: ExtendedProductDTO[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const PricingProductPricesSchema = z.object({
|
||||||
|
products: PricingProductsRecordSchema,
|
||||||
|
})
|
||||||
|
|
||||||
|
type VariantsPriceRecord = Record<
|
||||||
|
string,
|
||||||
|
{ currency_code: string; amount: number; id: string }[]
|
||||||
|
>
|
||||||
|
|
||||||
|
const initRecord = (priceList: PriceListDTO): VariantsPriceRecord => {
|
||||||
|
const prices = priceList.prices
|
||||||
|
const sortedPrices: VariantsPriceRecord = {}
|
||||||
|
|
||||||
|
if (!prices) {
|
||||||
|
return sortedPrices
|
||||||
|
}
|
||||||
|
|
||||||
|
prices.forEach((price) => {
|
||||||
|
// @ts-ignore - Type is wrong
|
||||||
|
const { variant_id, currency_code, amount, id } = price
|
||||||
|
|
||||||
|
if (!currency_code || !amount || !variant_id) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!sortedPrices[variant_id]) {
|
||||||
|
sortedPrices[variant_id] = []
|
||||||
|
}
|
||||||
|
|
||||||
|
sortedPrices[variant_id] = [
|
||||||
|
...sortedPrices[variant_id],
|
||||||
|
{ currency_code, amount, id },
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
return sortedPrices
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PricingProductPricesForm = ({
|
||||||
|
priceList,
|
||||||
|
products,
|
||||||
|
}: PricingProductPricesFormProps) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const { handleSuccess } = useRouteModal()
|
||||||
|
|
||||||
|
const record = useMemo(() => initRecord(priceList), [priceList])
|
||||||
|
|
||||||
|
const {
|
||||||
|
store,
|
||||||
|
isLoading: isStoreLoading,
|
||||||
|
isError: isStoreError,
|
||||||
|
error: storeError,
|
||||||
|
} = useStore()
|
||||||
|
|
||||||
|
const {
|
||||||
|
currencies,
|
||||||
|
isLoading: isCurrenciesLoading,
|
||||||
|
isError: isCurrencyError,
|
||||||
|
error: currencyError,
|
||||||
|
} = useCurrencies(
|
||||||
|
{
|
||||||
|
code: store?.supported_currency_codes,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: !!store,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
const form = useForm<z.infer<typeof PricingProductPricesSchema>>({
|
||||||
|
defaultValues: {
|
||||||
|
products: {},
|
||||||
|
},
|
||||||
|
resolver: zodResolver(PricingProductPricesSchema),
|
||||||
|
})
|
||||||
|
|
||||||
|
const existingProducts = useWatch({
|
||||||
|
control: form.control,
|
||||||
|
name: "products",
|
||||||
|
})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
initDefaultValues(products, existingProducts, form, record)
|
||||||
|
}, [existingProducts, form, products, record])
|
||||||
|
|
||||||
|
const { mutateAsync: updateAsync, isPending: isUpdatePending } =
|
||||||
|
useUpdatePriceList(priceList.id)
|
||||||
|
|
||||||
|
const { mutateAsync: deleteAsync, isPending: isDeletePending } =
|
||||||
|
usePriceListRemovePrices(priceList.id)
|
||||||
|
|
||||||
|
const { mutateAsync: addAsync, isPending: isAddPending } =
|
||||||
|
usePriceListAddPrices(priceList.id)
|
||||||
|
|
||||||
|
const isPending = isUpdatePending || isDeletePending || isAddPending
|
||||||
|
|
||||||
|
const handleSubmit = form.handleSubmit(async (values) => {
|
||||||
|
const { products } = values
|
||||||
|
|
||||||
|
const { pricesToDelete, pricesToCreate, pricesToUpdate } = sortPrices(
|
||||||
|
products,
|
||||||
|
record
|
||||||
|
)
|
||||||
|
|
||||||
|
let failed = false
|
||||||
|
|
||||||
|
// TODO: Currently not working, need to fix the API
|
||||||
|
// await updateAsync(
|
||||||
|
// {
|
||||||
|
// // @ts-expect-error - type is wrong
|
||||||
|
// prices: pricesToUpdate,
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// onError(error) {
|
||||||
|
// console.error(error)
|
||||||
|
// failed = true
|
||||||
|
// },
|
||||||
|
// }
|
||||||
|
// )
|
||||||
|
|
||||||
|
// if (failed) {
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
|
||||||
|
if (pricesToDelete.length) {
|
||||||
|
await deleteAsync(
|
||||||
|
{
|
||||||
|
ids: pricesToDelete,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onError(error) {
|
||||||
|
console.error(error)
|
||||||
|
failed = true
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (failed) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pricesToCreate.length) {
|
||||||
|
await addAsync(
|
||||||
|
{
|
||||||
|
// @ts-expect-error - type is wrong
|
||||||
|
prices: pricesToCreate,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onError(error) {
|
||||||
|
console.error(error)
|
||||||
|
failed = true
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (failed) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSuccess()
|
||||||
|
})
|
||||||
|
|
||||||
|
const columns = usePriceListGridColumns({ currencies })
|
||||||
|
|
||||||
|
const initializing =
|
||||||
|
isStoreLoading || isCurrenciesLoading || !products || !store || !currencies
|
||||||
|
|
||||||
|
if (isStoreError) {
|
||||||
|
throw storeError
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isCurrencyError) {
|
||||||
|
throw currencyError
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RouteFocusModal.Form form={form}>
|
||||||
|
<form onSubmit={handleSubmit} className="flex size-full flex-col">
|
||||||
|
<RouteFocusModal.Header>
|
||||||
|
<div className="flex items-center justify-end gap-x-2">
|
||||||
|
<RouteFocusModal.Close asChild>
|
||||||
|
<Button size="small" variant="secondary">
|
||||||
|
{t("actions.cancel")}
|
||||||
|
</Button>
|
||||||
|
</RouteFocusModal.Close>
|
||||||
|
<Button size="small" type="submit" isLoading={isPending}>
|
||||||
|
{t("actions.save")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</RouteFocusModal.Header>
|
||||||
|
<RouteFocusModal.Body className="flex flex-col overflow-hidden">
|
||||||
|
<DataGrid
|
||||||
|
columns={columns}
|
||||||
|
data={products}
|
||||||
|
getSubRows={(row) => {
|
||||||
|
if (isProductRow(row)) {
|
||||||
|
return row.variants
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
isLoading={initializing}
|
||||||
|
state={form}
|
||||||
|
/>
|
||||||
|
</RouteFocusModal.Body>
|
||||||
|
</form>
|
||||||
|
</RouteFocusModal.Form>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function initDefaultValues(
|
||||||
|
products: ExtendedProductDTO[],
|
||||||
|
existingProducts: any,
|
||||||
|
form: UseFormReturn<z.infer<typeof PricingProductPricesSchema>>,
|
||||||
|
record: VariantsPriceRecord
|
||||||
|
) {
|
||||||
|
products.forEach((product) => {
|
||||||
|
if (existingProducts[product.id] || !product.variants) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
form.setValue(
|
||||||
|
`products.${product.id}.variants`,
|
||||||
|
{
|
||||||
|
...product.variants.reduce((variants, variant) => {
|
||||||
|
const currencyPrices = record[variant.id] || []
|
||||||
|
|
||||||
|
variants[variant.id] = {
|
||||||
|
currency_prices: currencyPrices.reduce(
|
||||||
|
(prices, { currency_code, amount, id }) => {
|
||||||
|
const presentationAmount = getPresentationalAmount(
|
||||||
|
amount,
|
||||||
|
currency_code
|
||||||
|
).toString()
|
||||||
|
|
||||||
|
prices[currency_code] = {
|
||||||
|
amount: presentationAmount,
|
||||||
|
id,
|
||||||
|
}
|
||||||
|
return prices
|
||||||
|
},
|
||||||
|
{} as Record<
|
||||||
|
string,
|
||||||
|
{
|
||||||
|
amount: string
|
||||||
|
id: string
|
||||||
|
}
|
||||||
|
>
|
||||||
|
),
|
||||||
|
region_prices: {},
|
||||||
|
}
|
||||||
|
|
||||||
|
return variants
|
||||||
|
}, {} as PricingVariantsRecordType),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
shouldDirty: false,
|
||||||
|
shouldTouch: false,
|
||||||
|
shouldValidate: false,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function sortPrices(
|
||||||
|
products: Record<
|
||||||
|
string,
|
||||||
|
{
|
||||||
|
variants: Record<
|
||||||
|
string,
|
||||||
|
{
|
||||||
|
currency_prices: Record<
|
||||||
|
string,
|
||||||
|
| { amount?: string | undefined; id?: string | null | undefined }
|
||||||
|
| undefined
|
||||||
|
>
|
||||||
|
region_prices: Record<
|
||||||
|
string,
|
||||||
|
| { amount?: string | undefined; id?: string | null | undefined }
|
||||||
|
| undefined
|
||||||
|
>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
}
|
||||||
|
>,
|
||||||
|
record: VariantsPriceRecord
|
||||||
|
) {
|
||||||
|
const pricesToUpdate: UpdatePriceListPriceDTO[] = []
|
||||||
|
const pricesToCreate: CreatePriceListPriceDTO[] = []
|
||||||
|
const pricesToDelete: string[] = []
|
||||||
|
|
||||||
|
for (const [_productId, product] of Object.entries(products)) {
|
||||||
|
const { variants } = product
|
||||||
|
|
||||||
|
for (const [variantId, variant] of Object.entries(variants)) {
|
||||||
|
const { currency_prices } = variant
|
||||||
|
|
||||||
|
for (const [currencyCode, currencyPrice] of Object.entries(
|
||||||
|
currency_prices
|
||||||
|
)) {
|
||||||
|
if (!currencyPrice) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currencyPrice.id && currencyPrice.amount) {
|
||||||
|
const originalPrice = record[variantId].find(
|
||||||
|
(p) => p.id === currencyPrice.id
|
||||||
|
)
|
||||||
|
|
||||||
|
// If the price has not changed, we don't need to update it
|
||||||
|
if (
|
||||||
|
originalPrice &&
|
||||||
|
originalPrice.amount ===
|
||||||
|
getDbAmount(castNumber(currencyPrice.amount), currencyCode)
|
||||||
|
) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
pricesToUpdate.push({
|
||||||
|
id: currencyPrice.id,
|
||||||
|
amount: getDbAmount(castNumber(currencyPrice.amount), currencyCode),
|
||||||
|
currency_code: currencyCode,
|
||||||
|
// @ts-expect-error type is wrong
|
||||||
|
variant_id: variantId,
|
||||||
|
})
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currencyPrice.id && !currencyPrice.amount) {
|
||||||
|
pricesToDelete.push(currencyPrice.id)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!currencyPrice.id && currencyPrice.amount) {
|
||||||
|
pricesToCreate.push({
|
||||||
|
amount: getDbAmount(castNumber(currencyPrice.amount), currencyCode),
|
||||||
|
currency_code: currencyCode,
|
||||||
|
// @ts-expect-error type is wrong
|
||||||
|
variant_id: variantId,
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { pricesToDelete, pricesToCreate, pricesToUpdate }
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { PricingProductsPrices as Component } from "./pricing-products-prices"
|
||||||
+43
@@ -0,0 +1,43 @@
|
|||||||
|
import { useParams, useSearchParams } from "react-router-dom"
|
||||||
|
import { RouteFocusModal } from "../../../components/route-modal"
|
||||||
|
import { usePriceList } from "../../../hooks/api/price-lists"
|
||||||
|
import { useProducts } from "../../../hooks/api/products"
|
||||||
|
import { PricingProductPricesForm } from "./components/pricing-products-prices-form"
|
||||||
|
|
||||||
|
export const PricingProductsPrices = () => {
|
||||||
|
const { id } = useParams()
|
||||||
|
const [searchParams] = useSearchParams()
|
||||||
|
const ids = searchParams.get("ids[]")
|
||||||
|
|
||||||
|
const { price_list, isLoading, isError, error } = usePriceList(id!)
|
||||||
|
const productIds = ids?.split(",")
|
||||||
|
|
||||||
|
const {
|
||||||
|
products,
|
||||||
|
isLoading: isProductsLoading,
|
||||||
|
isError: isProductsError,
|
||||||
|
error: productError,
|
||||||
|
} = useProducts({
|
||||||
|
id: productIds,
|
||||||
|
limit: productIds?.length || 9999, // Temporary until we support lazy loading in the DataGrid
|
||||||
|
fields: "title,thumbnail,*variants",
|
||||||
|
})
|
||||||
|
|
||||||
|
const ready = !isLoading && !!price_list && !isProductsLoading && !!products
|
||||||
|
|
||||||
|
if (isError) {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isProductsError) {
|
||||||
|
throw productError
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RouteFocusModal>
|
||||||
|
{ready && (
|
||||||
|
<PricingProductPricesForm priceList={price_list} products={products} />
|
||||||
|
)}
|
||||||
|
</RouteFocusModal>
|
||||||
|
)
|
||||||
|
}
|
||||||
+11
-12
@@ -5,23 +5,24 @@ import {
|
|||||||
RowSelectionState,
|
RowSelectionState,
|
||||||
createColumnHelper,
|
createColumnHelper,
|
||||||
} from "@tanstack/react-table"
|
} from "@tanstack/react-table"
|
||||||
import { adminCurrenciesKeys, useAdminCustomQuery } from "medusa-react"
|
|
||||||
import { useMemo, useState } from "react"
|
import { useMemo, useState } from "react"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
import * as zod from "zod"
|
import * as zod from "zod"
|
||||||
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod"
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
import { CurrencyDTO, StoreDTO } from "@medusajs/types"
|
import { StoreDTO } from "@medusajs/types"
|
||||||
|
import { keepPreviousData } from "@tanstack/react-query"
|
||||||
import { useForm } from "react-hook-form"
|
import { useForm } from "react-hook-form"
|
||||||
import {
|
import {
|
||||||
RouteFocusModal,
|
RouteFocusModal,
|
||||||
useRouteModal,
|
useRouteModal,
|
||||||
} from "../../../../../components/route-modal"
|
} from "../../../../../components/route-modal"
|
||||||
import { DataTable } from "../../../../../components/table/data-table"
|
import { DataTable } from "../../../../../components/table/data-table"
|
||||||
|
import { useCurrencies } from "../../../../../hooks/api/currencies"
|
||||||
|
import { useUpdateStore } from "../../../../../hooks/api/store"
|
||||||
import { useDataTable } from "../../../../../hooks/use-data-table"
|
import { useDataTable } from "../../../../../hooks/use-data-table"
|
||||||
import { useCurrenciesTableColumns } from "../../../common/hooks/use-currencies-table-columns"
|
import { useCurrenciesTableColumns } from "../../../common/hooks/use-currencies-table-columns"
|
||||||
import { useCurrenciesTableQuery } from "../../../common/hooks/use-currencies-table-query"
|
import { useCurrenciesTableQuery } from "../../../common/hooks/use-currencies-table-query"
|
||||||
import { useUpdateStore } from "../../../../../hooks/api/store"
|
|
||||||
|
|
||||||
type AddCurrenciesFormProps = {
|
type AddCurrenciesFormProps = {
|
||||||
store: StoreDTO
|
store: StoreDTO
|
||||||
@@ -67,12 +68,10 @@ export const AddCurrenciesForm = ({ store }: AddCurrenciesFormProps) => {
|
|||||||
prefix: PREFIX,
|
prefix: PREFIX,
|
||||||
})
|
})
|
||||||
|
|
||||||
const { data, isLoading, isError, error } = useAdminCustomQuery(
|
const { currencies, count, isLoading, isError, error } = useCurrencies(
|
||||||
"/admin/currencies",
|
|
||||||
adminCurrenciesKeys.list(raw),
|
|
||||||
searchParams,
|
searchParams,
|
||||||
{
|
{
|
||||||
keepPreviousData: true,
|
placeholderData: keepPreviousData,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -81,9 +80,9 @@ export const AddCurrenciesForm = ({ store }: AddCurrenciesFormProps) => {
|
|||||||
const columns = useColumns()
|
const columns = useColumns()
|
||||||
|
|
||||||
const { table } = useDataTable({
|
const { table } = useDataTable({
|
||||||
data: (data?.currencies ?? []) as CurrencyDTO[],
|
data: currencies ?? [],
|
||||||
columns,
|
columns,
|
||||||
count: data?.count,
|
count: count,
|
||||||
getRowId: (row) => row.code,
|
getRowId: (row) => row.code,
|
||||||
enableRowSelection: (row) => !preSelectedRows.includes(row.original.code),
|
enableRowSelection: (row) => !preSelectedRows.includes(row.original.code),
|
||||||
enablePagination: true,
|
enablePagination: true,
|
||||||
@@ -95,7 +94,7 @@ export const AddCurrenciesForm = ({ store }: AddCurrenciesFormProps) => {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const { mutateAsync, isLoading: isMutating } = useUpdateStore(store.id)
|
const { mutateAsync, isPending } = useUpdateStore(store.id)
|
||||||
|
|
||||||
const handleSubmit = form.handleSubmit(async (data) => {
|
const handleSubmit = form.handleSubmit(async (data) => {
|
||||||
const currencies = Array.from(
|
const currencies = Array.from(
|
||||||
@@ -139,7 +138,7 @@ export const AddCurrenciesForm = ({ store }: AddCurrenciesFormProps) => {
|
|||||||
{t("actions.cancel")}
|
{t("actions.cancel")}
|
||||||
</Button>
|
</Button>
|
||||||
</RouteFocusModal.Close>
|
</RouteFocusModal.Close>
|
||||||
<Button size="small" type="submit" isLoading={isMutating}>
|
<Button size="small" type="submit" isLoading={isPending}>
|
||||||
{t("actions.save")}
|
{t("actions.save")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -149,7 +148,7 @@ export const AddCurrenciesForm = ({ store }: AddCurrenciesFormProps) => {
|
|||||||
<DataTable
|
<DataTable
|
||||||
table={table}
|
table={table}
|
||||||
pageSize={PAGE_SIZE}
|
pageSize={PAGE_SIZE}
|
||||||
count={data?.count}
|
count={count}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
layout="fill"
|
layout="fill"
|
||||||
pagination
|
pagination
|
||||||
|
|||||||
+2
-2
@@ -1,9 +1,9 @@
|
|||||||
import { RouteFocusModal } from "../../../components/route-modal"
|
import { RouteFocusModal } from "../../../components/route-modal"
|
||||||
import { AddCurrenciesForm } from "./components/add-currencies-form/add-currencies-form"
|
|
||||||
import { useStore } from "../../../hooks/api/store"
|
import { useStore } from "../../../hooks/api/store"
|
||||||
|
import { AddCurrenciesForm } from "./components/add-currencies-form/add-currencies-form"
|
||||||
|
|
||||||
export const StoreAddCurrencies = () => {
|
export const StoreAddCurrencies = () => {
|
||||||
const { store, isLoading, isError, error } = useStore({})
|
const { store, isLoading, isError, error } = useStore()
|
||||||
|
|
||||||
if (isError) {
|
if (isError) {
|
||||||
throw error
|
throw error
|
||||||
|
|||||||
+1
-5
@@ -9,7 +9,6 @@ import {
|
|||||||
} from "@medusajs/ui"
|
} from "@medusajs/ui"
|
||||||
import { keepPreviousData } from "@tanstack/react-query"
|
import { keepPreviousData } from "@tanstack/react-query"
|
||||||
import { RowSelectionState, createColumnHelper } from "@tanstack/react-table"
|
import { RowSelectionState, createColumnHelper } from "@tanstack/react-table"
|
||||||
import { adminStoreKeys, useAdminCustomPost } from "medusa-react"
|
|
||||||
import { useMemo, useState } from "react"
|
import { useMemo, useState } from "react"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
import { ActionMenu } from "../../../../../../components/common/action-menu"
|
import { ActionMenu } from "../../../../../../components/common/action-menu"
|
||||||
@@ -157,10 +156,7 @@ const CurrencyActions = ({
|
|||||||
currency: CurrencyDTO
|
currency: CurrencyDTO
|
||||||
currencyCodes: string[]
|
currencyCodes: string[]
|
||||||
}) => {
|
}) => {
|
||||||
const { mutateAsync } = useAdminCustomPost(
|
const { mutateAsync } = useUpdateStore(storeId)
|
||||||
`/admin/stores/${storeId}`,
|
|
||||||
adminStoreKeys.details()
|
|
||||||
)
|
|
||||||
|
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const prompt = usePrompt()
|
const prompt = usePrompt()
|
||||||
|
|||||||
@@ -3854,15 +3854,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@babel/runtime@npm:^7.12.13, @babel/runtime@npm:^7.23.7, @babel/runtime@npm:^7.23.8":
|
|
||||||
version: 7.23.9
|
|
||||||
resolution: "@babel/runtime@npm:7.23.9"
|
|
||||||
dependencies:
|
|
||||||
regenerator-runtime: ^0.14.0
|
|
||||||
checksum: e71205fdd7082b2656512cc98e647d9ea7e222e4fe5c36e9e5adc026446fcc3ba7b3cdff8b0b694a0b78bb85db83e7b1e3d4c56ef90726682b74f13249cf952d
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"@babel/runtime@npm:^7.19.4, @babel/runtime@npm:^7.20.6":
|
"@babel/runtime@npm:^7.19.4, @babel/runtime@npm:^7.20.6":
|
||||||
version: 7.22.11
|
version: 7.22.11
|
||||||
resolution: "@babel/runtime@npm:7.22.11"
|
resolution: "@babel/runtime@npm:7.22.11"
|
||||||
@@ -3890,6 +3881,15 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@babel/runtime@npm:^7.23.7, @babel/runtime@npm:^7.23.8":
|
||||||
|
version: 7.23.9
|
||||||
|
resolution: "@babel/runtime@npm:7.23.9"
|
||||||
|
dependencies:
|
||||||
|
regenerator-runtime: ^0.14.0
|
||||||
|
checksum: e71205fdd7082b2656512cc98e647d9ea7e222e4fe5c36e9e5adc026446fcc3ba7b3cdff8b0b694a0b78bb85db83e7b1e3d4c56ef90726682b74f13249cf952d
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@babel/template@npm:^7.12.13, @babel/template@npm:^7.12.7, @babel/template@npm:^7.16.7, @babel/template@npm:^7.22.5, @babel/template@npm:^7.3.3":
|
"@babel/template@npm:^7.12.13, @babel/template@npm:^7.12.7, @babel/template@npm:^7.16.7, @babel/template@npm:^7.22.5, @babel/template@npm:^7.3.3":
|
||||||
version: 7.22.5
|
version: 7.22.5
|
||||||
resolution: "@babel/template@npm:7.22.5"
|
resolution: "@babel/template@npm:7.22.5"
|
||||||
@@ -8182,12 +8182,12 @@ __metadata:
|
|||||||
qs: ^6.12.0
|
qs: ^6.12.0
|
||||||
react: 18.2.0
|
react: 18.2.0
|
||||||
react-country-flag: ^3.1.0
|
react-country-flag: ^3.1.0
|
||||||
|
react-currency-input-field: ^3.6.11
|
||||||
react-dom: 18.2.0
|
react-dom: 18.2.0
|
||||||
react-focus-lock: ^2.11.1
|
|
||||||
react-hook-form: 7.49.1
|
react-hook-form: 7.49.1
|
||||||
react-i18next: 13.5.0
|
react-i18next: 13.5.0
|
||||||
react-jwt: ^1.2.0
|
react-jwt: ^1.2.0
|
||||||
react-resizable-panels: ^2.0.9
|
react-resizable-panels: ^2.0.16
|
||||||
react-router-dom: 6.20.1
|
react-router-dom: 6.20.1
|
||||||
tailwindcss: ^3.4.1
|
tailwindcss: ^3.4.1
|
||||||
typescript: 5.2.2
|
typescript: 5.2.2
|
||||||
@@ -28984,15 +28984,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"focus-lock@npm:^1.3.2":
|
|
||||||
version: 1.3.3
|
|
||||||
resolution: "focus-lock@npm:1.3.3"
|
|
||||||
dependencies:
|
|
||||||
tslib: ^2.0.3
|
|
||||||
checksum: 38b978ab30f5be6f061689c747b05193217e9cb9c5d5a41b9b322454c6bb14e84cbd6c928fa245c62e744f070cfa939820a0ae2dd51011da233de7c912cea412
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"follow-redirects@npm:1.5.10":
|
"follow-redirects@npm:1.5.10":
|
||||||
version: 1.5.10
|
version: 1.5.10
|
||||||
resolution: "follow-redirects@npm:1.5.10"
|
resolution: "follow-redirects@npm:1.5.10"
|
||||||
@@ -43946,17 +43937,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"react-clientside-effect@npm:^1.2.6":
|
|
||||||
version: 1.2.6
|
|
||||||
resolution: "react-clientside-effect@npm:1.2.6"
|
|
||||||
dependencies:
|
|
||||||
"@babel/runtime": ^7.12.13
|
|
||||||
peerDependencies:
|
|
||||||
react: ^15.3.0 || ^16.0.0 || ^17.0.0 || ^18.0.0
|
|
||||||
checksum: aba0adb666018e5c64657c31f4914a8558be73f71d6e2210fa871ebfcab94d786c83082868d7c7fa66feddc2aec19e375745cf0903e0619f2efffef08b92d941
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"react-collapsible@npm:^2.8.3":
|
"react-collapsible@npm:^2.8.3":
|
||||||
version: 2.10.0
|
version: 2.10.0
|
||||||
resolution: "react-collapsible@npm:2.10.0"
|
resolution: "react-collapsible@npm:2.10.0"
|
||||||
@@ -44227,26 +44207,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"react-focus-lock@npm:^2.11.1":
|
|
||||||
version: 2.11.1
|
|
||||||
resolution: "react-focus-lock@npm:2.11.1"
|
|
||||||
dependencies:
|
|
||||||
"@babel/runtime": ^7.0.0
|
|
||||||
focus-lock: ^1.3.2
|
|
||||||
prop-types: ^15.6.2
|
|
||||||
react-clientside-effect: ^1.2.6
|
|
||||||
use-callback-ref: ^1.3.0
|
|
||||||
use-sidecar: ^1.1.2
|
|
||||||
peerDependencies:
|
|
||||||
"@types/react": ^16.8.0 || ^17.0.0 || ^18.0.0
|
|
||||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0
|
|
||||||
peerDependenciesMeta:
|
|
||||||
"@types/react":
|
|
||||||
optional: true
|
|
||||||
checksum: b4905ec18bb6bd6732cbd2d3bd70853007dcc31363fa8d0700ea645eec2e2e685f2fe09443d070983c2077fc2d6c06279f2838fc9046c6218facc3e310e50279
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"react-helmet-async@npm:^1.3.0":
|
"react-helmet-async@npm:^1.3.0":
|
||||||
version: 1.3.0
|
version: 1.3.0
|
||||||
resolution: "react-helmet-async@npm:1.3.0"
|
resolution: "react-helmet-async@npm:1.3.0"
|
||||||
@@ -44562,13 +44522,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"react-resizable-panels@npm:^2.0.9":
|
"react-resizable-panels@npm:^2.0.16":
|
||||||
version: 2.0.9
|
version: 2.0.16
|
||||||
resolution: "react-resizable-panels@npm:2.0.9"
|
resolution: "react-resizable-panels@npm:2.0.16"
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: ^16.14.0 || ^17.0.0 || ^18.0.0
|
react: ^16.14.0 || ^17.0.0 || ^18.0.0
|
||||||
react-dom: ^16.14.0 || ^17.0.0 || ^18.0.0
|
react-dom: ^16.14.0 || ^17.0.0 || ^18.0.0
|
||||||
checksum: 0302ab32d3aa7d2887a2ad58882e447a8e22cef3c5c728cde7436442fcd7475969a86bb45d1a6e285b1b4e2a9ac49d13a0e2df4b29bc63542dc99044309e211c
|
checksum: 91e125a10de5908cdca9c82247106927a98a2686097cca5aa31415d3ba7013425f3b3c10f1465eadcc6a4977a2d549cabadb71c58586cddfae4995c8bb575ace
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user