fix(admin-ui): Gift Card manage page (#3532)

**What**
- Updates GC manage page to use product page sections
- Revamps Denomination section
- Updates the location of several components to reflect that they are now shared between the GC and products domain

![image](https://user-images.githubusercontent.com/45367945/226584238-bb0786b1-d21c-4b90-b00b-29530af320f4.png)
![image](https://user-images.githubusercontent.com/45367945/226584362-80c0c9f8-4ec5-4e64-9075-110caa3b5137.png)

Resolves CORE-1089
This commit is contained in:
Kasper Fabricius Kristensen
2023-03-23 09:29:29 +01:00
committed by GitHub
parent 3171b0e518
commit bfef22b33e
77 changed files with 1286 additions and 1706 deletions

View File

@@ -0,0 +1,5 @@
---
"@medusajs/admin-ui": patch
---
fix(admin-ui): Revamps gift card manage page

View File

@@ -1,25 +1,30 @@
import * as RadixSwitch from "@radix-ui/react-switch"
import clsx from "clsx"
import React from "react"
/**
* A controlled switch component atom.
*/
function Switch(props: RadixSwitch.SwitchProps) {
return (
<RadixSwitch.Root
{...props}
disabled={props.disabled}
className={clsx(
"transition-bg radix-state-checked:bg-violet-60 h-[18px] w-8 rounded-full bg-gray-300"
)}
>
<RadixSwitch.Thumb
const Switch = React.forwardRef<HTMLButtonElement, RadixSwitch.SwitchProps>(
(props, ref) => {
return (
<RadixSwitch.Root
ref={ref}
{...props}
className={clsx(
"radix-state-checked:translate-x-[19px] block h-2 w-2 translate-x-[5px] rounded-full bg-white transition-transform"
"transition-bg radix-state-checked:bg-violet-60 h-[18px] w-8 rounded-full bg-gray-300"
)}
/>
</RadixSwitch.Root>
)
}
>
<RadixSwitch.Thumb
className={clsx(
"radix-state-checked:translate-x-[19px] block h-2 w-2 translate-x-[5px] rounded-full bg-white transition-transform"
)}
/>
</RadixSwitch.Root>
)
}
)
Switch.displayName = "Switch"
export default Switch

View File

@@ -42,7 +42,7 @@ const Tooltip = ({
sideOffset={8}
align="center"
className={clsx(
"inter-small-semibold text-grey-50",
"inter-small-semibold text-grey-50 z-[999]",
"bg-grey-0 shadow-dropdown rounded-rounded py-2 px-3",
"border-grey-20 border border-solid",
className

View File

@@ -1,13 +1,13 @@
import clsx from "clsx"
import { Controller } from "react-hook-form"
import { NestedPriceObject, PricesFormType } from "."
import IncludesTaxTooltip from "../../../../components/atoms/includes-tax-tooltip"
import CoinsIcon from "../../../../components/fundamentals/icons/coins-icon"
import MapPinIcon from "../../../../components/fundamentals/icons/map-pin-icon"
import TriangleRightIcon from "../../../../components/fundamentals/icons/triangle-right-icon"
import useToggleState from "../../../../hooks/use-toggle-state"
import { currencies } from "../../../../utils/currencies"
import { NestedForm } from "../../../../utils/nested-form"
import IncludesTaxTooltip from "../../../atoms/includes-tax-tooltip"
import CoinsIcon from "../../../fundamentals/icons/coins-icon"
import MapPinIcon from "../../../fundamentals/icons/map-pin-icon"
import TriangleRightIcon from "../../../fundamentals/icons/triangle-right-icon"
import PriceFormInput from "./price-form-input"
type Props = {
@@ -21,7 +21,7 @@ const NestedPrice = ({ form, nestedPrice }: Props) => {
const { control, path } = form
const { currencyPrice, regionPrices } = nestedPrice
return (
<div key={currencyPrice.id} className="gap-y-2xsmall flex flex-col">
<div className="gap-y-2xsmall flex flex-col">
<div className="gap-x-base p-2xsmall hover:bg-grey-5 focus-within:bg-grey-5 rounded-rounded relative grid grid-cols-[1fr_223px] justify-between pl-10 transition-colors">
<button
className={clsx(

View File

@@ -1,8 +1,8 @@
import clsx from "clsx"
import { useEffect, useState } from "react"
import AmountField from "react-currency-input-field"
import InputError from "../../../../components/atoms/input-error"
import { currencies } from "../../../../utils/currencies"
import InputError from "../../../atoms/input-error"
type Props = {
currencyCode: string

View File

@@ -0,0 +1,177 @@
import { Controller, useFieldArray, useWatch } from "react-hook-form"
import { currencies } from "../../../../utils/currencies"
import { NestedForm } from "../../../../utils/nested-form"
import IncludesTaxTooltip from "../../../atoms/includes-tax-tooltip"
import InputError from "../../../atoms/input-error"
import Switch from "../../../atoms/switch"
import CoinsIcon from "../../../fundamentals/icons/coins-icon"
import IconTooltip from "../../../molecules/icon-tooltip"
import PriceFormInput from "../../general/prices-form/price-form-input"
type DenominationType = {
amount: number
currency_code: string
includes_tax?: boolean
}
export type DenominationFormType = {
defaultDenomination: DenominationType
currencyDenominations: DenominationType[]
useSameValue: boolean
}
type Props = {
form: NestedForm<DenominationFormType>
}
const DenominationForm = ({ form }: Props) => {
const {
control,
path,
formState: { errors },
} = form
const { fields } = useFieldArray({
control,
name: path("currencyDenominations"),
keyName: "fieldKey",
})
const defaultCurrency = useWatch({
control,
name: path("defaultDenomination"),
})
const useSameValue = useWatch({
control,
name: path("useSameValue"),
})
return (
<div className="gap-y-xlarge flex flex-col">
<div>
<div className="gap-x-2xsmall flex items-center">
<h2 className="inter-large-semibold">Default currency</h2>
<IconTooltip
type="info"
content="The denomination in your store's default currency"
/>
</div>
<div className="pt-small gap-x-base rounded-rounded relative grid grid-cols-[1fr_223px] justify-between transition-colors">
<div className="gap-x-small flex items-center">
<div className="bg-grey-10 rounded-rounded text-grey-50 flex h-10 w-10 items-center justify-center">
<CoinsIcon size={20} />
</div>
<div className="gap-x-xsmall flex items-center">
<span className="inter-base-semibold">
{defaultCurrency.currency_code.toUpperCase()}
</span>
<span className="inter-base-regular text-grey-50">
{currencies[defaultCurrency.currency_code.toUpperCase()].name}
</span>
<IncludesTaxTooltip includesTax={defaultCurrency.includes_tax} />
</div>
</div>
<Controller
name={path(`defaultDenomination.amount`)}
control={control}
rules={{
required:
"An amount for your store's default currency is required",
}}
render={({ field: { value, onChange, name } }) => {
return (
<>
<PriceFormInput
onChange={onChange}
amount={value !== null ? value : undefined}
currencyCode={defaultCurrency.currency_code}
errors={errors}
/>
<InputError errors={errors} name={name} />
</>
)
}}
/>
</div>
</div>
<div>
<div className="flex items-start justify-between">
<div className="gap-y-2xsmall flex flex-col">
<h2 className="inter-base-semibold">
Use value for all currencies?
</h2>
<p className="inter-small-regular text-grey-50 max-w-[60%]">
If enabled the value used for the store&apos;s default currency
code will also be applied to all other currencies in your store.
</p>
</div>
<Controller
control={control}
name={path("useSameValue")}
render={({ field: { value, onChange, ...rest } }) => {
return (
<Switch checked={value} onCheckedChange={onChange} {...rest} />
)
}}
/>
</div>
</div>
{!useSameValue && (
<div>
<div className="gap-x-2xsmall flex items-center">
<h2 className="inter-base-semibold">Other currencies</h2>
<IconTooltip
type="info"
content="The denomination in your store's other currencies"
/>
</div>
<div className="gap-y-xsmall pt-small flex flex-col">
{fields.map((denom, index) => {
return (
<div
key={denom.fieldKey}
className="gap-x-base rounded-rounded relative grid grid-cols-[1fr_223px] justify-between transition-colors"
>
<div className="gap-x-small flex items-center">
<div className="bg-grey-10 rounded-rounded text-grey-50 flex h-10 w-10 items-center justify-center">
<CoinsIcon size={20} />
</div>
<div className="gap-x-xsmall flex items-center">
<span className="inter-base-semibold">
{denom.currency_code.toUpperCase()}
</span>
<span className="inter-base-regular text-grey-50">
{currencies[denom.currency_code.toUpperCase()].name}
</span>
<IncludesTaxTooltip includesTax={denom.includes_tax} />
</div>
</div>
<Controller
name={path(`currencyDenominations.${index}.amount`)}
control={control}
render={({
field: { value, onChange },
formState: { errors },
}) => {
return (
<PriceFormInput
onChange={onChange}
amount={value !== null ? value : undefined}
currencyCode={denom.currency_code}
errors={errors}
/>
)
}}
/>
</div>
)
})}
</div>
</div>
)}
</div>
)
}
export default DenominationForm

View File

@@ -1,10 +1,10 @@
import { Controller } from "react-hook-form"
import InputField from "../../../../components/molecules/input"
import { NextSelect } from "../../../../components/molecules/select/next-select"
import { Option } from "../../../../types/shared"
import { countries } from "../../../../utils/countries"
import FormValidator from "../../../../utils/form-validator"
import { NestedForm } from "../../../../utils/nested-form"
import InputField from "../../../molecules/input"
import { NextSelect } from "../../../molecules/select/next-select"
export type CustomsFormType = {
mid_code: string | null

View File

@@ -1,6 +1,6 @@
import InputField from "../../../../components/molecules/input"
import FormValidator from "../../../../utils/form-validator"
import { NestedForm } from "../../../../utils/nested-form"
import InputField from "../../../molecules/input"
export type DimensionsFormType = {
length: number | null

View File

@@ -1,6 +1,6 @@
import { Controller } from "react-hook-form"
import Switch from "../../../../components/atoms/switch"
import { NestedForm } from "../../../../utils/nested-form"
import Switch from "../../../atoms/switch"
export type DiscountableFormType = {
value: boolean
@@ -8,9 +8,10 @@ export type DiscountableFormType = {
type Props = {
form: NestedForm<DiscountableFormType>
isGiftCard?: boolean
}
const DiscountableForm = ({ form }: Props) => {
const DiscountableForm = ({ form, isGiftCard }: Props) => {
const { control, path } = form
return (
<div>
@@ -25,7 +26,8 @@ const DiscountableForm = ({ form }: Props) => {
/>
</div>
<p className="inter-base-regular text-grey-50">
When unchecked discounts will not be applied to this product.
When unchecked discounts will not be applied to this{" "}
{isGiftCard ? "gift card" : "product"}.
</p>
</div>
)

View File

@@ -1,7 +1,7 @@
import InputField from "../../../../components/molecules/input"
import TextArea from "../../../../components/molecules/textarea"
import FormValidator from "../../../../utils/form-validator"
import { NestedForm } from "../../../../utils/nested-form"
import InputField from "../../../molecules/input"
import TextArea from "../../../molecules/textarea"
export type GeneralFormType = {
title: string
@@ -14,9 +14,10 @@ export type GeneralFormType = {
type Props = {
form: NestedForm<GeneralFormType>
requireHandle?: boolean
isGiftCard?: boolean
}
const GeneralForm = ({ form, requireHandle = true }: Props) => {
const GeneralForm = ({ form, requireHandle = true, isGiftCard }: Props) => {
const {
register,
path,
@@ -28,7 +29,7 @@ const GeneralForm = ({ form, requireHandle = true }: Props) => {
<div className="gap-x-large mb-small grid grid-cols-2">
<InputField
label="Title"
placeholder="Winter Jacket"
placeholder={isGiftCard ? "Gift Card" : "Winter Jacket"}
required
{...register(path("title"), {
required: "Title is required",
@@ -50,7 +51,8 @@ const GeneralForm = ({ form, requireHandle = true }: Props) => {
/>
</div>
<p className="inter-base-regular text-grey-50 mb-large">
Give your product a short and clear title.
Give your {isGiftCard ? "gift card" : "product"} a short and clear
title.
<br />
50-60 characters is the recommended length for search engines.
</p>
@@ -59,10 +61,12 @@ const GeneralForm = ({ form, requireHandle = true }: Props) => {
label="Handle"
tooltipContent={
!requireHandle
? "The handle is the part of the URL that identifies the product. If not specified, it will be generated from the title."
? `The handle is the part of the URL that identifies the ${
isGiftCard ? "gift card" : "product"
}. If not specified, it will be generated from the title.`
: undefined
}
placeholder="winter-jacket"
placeholder={isGiftCard ? "gift-card" : "winter-jacket"}
required={requireHandle}
{...register(path("handle"), {
required: requireHandle ? "Handle is required" : undefined,
@@ -74,7 +78,7 @@ const GeneralForm = ({ form, requireHandle = true }: Props) => {
/>
<InputField
label="Material"
placeholder="100% cotton"
placeholder={isGiftCard ? "Paper" : "100% Cotton"}
{...register(path("material"), {
minLength: FormValidator.minOneCharRule("Material"),
pattern: FormValidator.whiteSpaceRule("Material"),
@@ -84,14 +88,17 @@ const GeneralForm = ({ form, requireHandle = true }: Props) => {
</div>
<TextArea
label="Description"
placeholder="A warm and cozy jacket..."
placeholder={
isGiftCard ? "The gift card is..." : "A warm and cozy jacket..."
}
rows={3}
className="mb-small"
{...register(path("description"))}
errors={errors}
/>
<p className="inter-base-regular text-grey-50">
Give your product a short and clear description.
Give your {isGiftCard ? "gift card" : "product"} a short and clear
description.
<br />
120-160 characters is the recommended length for search engines.
</p>

View File

@@ -6,15 +6,13 @@ import {
useFieldArray,
useWatch,
} from "react-hook-form"
import FileUploadField from "../../../../components/atoms/file-upload-field"
import Button from "../../../../components/fundamentals/button"
import CheckCircleFillIcon from "../../../../components/fundamentals/icons/check-circle-fill-icon"
import TrashIcon from "../../../../components/fundamentals/icons/trash-icon"
import Actionables, {
ActionType,
} from "../../../../components/molecules/actionables"
import { FormImage } from "../../../../types/shared"
import { NestedForm } from "../../../../utils/nested-form"
import FileUploadField from "../../../atoms/file-upload-field"
import Button from "../../../fundamentals/button"
import CheckCircleFillIcon from "../../../fundamentals/icons/check-circle-fill-icon"
import TrashIcon from "../../../fundamentals/icons/trash-icon"
import Actionables, { ActionType } from "../../../molecules/actionables"
type ImageType = { selected: boolean } & FormImage

View File

@@ -1,18 +1,18 @@
import { Controller } from "react-hook-form"
import NestedMultiselect from "../../../../domain/categories/components/multiselect"
import {
FeatureFlag,
useFeatureFlag,
} from "../../../../providers/feature-flag-provider"
import { Option } from "../../../../types/shared"
import { NestedForm } from "../../../../utils/nested-form"
import InputHeader from "../../../fundamentals/input-header"
import {
NextCreateableSelect,
NextSelect,
} from "../../../../components/molecules/select/next-select"
import TagInput from "../../../../components/molecules/tag-input"
import { Option } from "../../../../types/shared"
import { NestedForm } from "../../../../utils/nested-form"
} from "../../../molecules/select/next-select"
import TagInput from "../../../molecules/tag-input"
import useOrganizeData from "./use-organize-data"
import NestedMultiselect from "../../../categories/components/multiselect"
import InputHeader from "../../../../components/fundamentals/input-header"
import {
useFeatureFlag,
FeatureFlag,
} from "../../../../providers/feature-flag-provider"
export type OrganizeFormType = {
type: Option | null
@@ -86,21 +86,18 @@ const OrganizeForm = ({ form }: Props) => {
/>
</div>
{isFeatureEnabled(FeatureFlag.PRODUCT_CATEGORIES) && (
{isFeatureEnabled(FeatureFlag.PRODUCT_CATEGORIES) &&
categoriesOptions?.length ? (
<>
<InputHeader label="Categories" className="mb-2" />
<Controller
name={path("categories")}
control={control}
render={({ field: { value, onChange } }) => {
if (categoriesOptions.length === 0) {
return null
}
const initiallySelected = (value || []).reduce((acc, val) => {
acc[val] = true
return acc
}, {})
}, {} as Record<string, true>)
return (
<NestedMultiselect
@@ -112,7 +109,7 @@ const OrganizeForm = ({ form }: Props) => {
}}
/>
</>
)}
) : null}
<div className="mb-large" />

View File

@@ -3,14 +3,13 @@ import { useMemo } from "react"
import {
useAdminCollections,
useAdminProductCategories,
useAdminProductTypes,
useAdminProductTypes
} from "medusa-react"
import { NestedMultiselectOption } from "../../../categories/components/multiselect"
import { transformCategoryToNestedFormOptions } from "../../../categories/utils/transform-response"
import { NestedMultiselectOption } from "../../../../domain/categories/components/multiselect"
import { transformCategoryToNestedFormOptions } from "../../../../domain/categories/utils/transform-response"
import {
useFeatureFlag,
FeatureFlag,
FeatureFlag, useFeatureFlag
} from "../../../../providers/feature-flag-provider"
const useOrganizeData = () => {

View File

@@ -1,10 +1,10 @@
import { useAdminSalesChannels } from "medusa-react"
import React, { useContext, useMemo, useState } from "react"
import React, { useMemo, useState } from "react"
import { usePagination, useRowSelect, useTable } from "react-table"
import Button from "../../../../components/fundamentals/button"
import Modal from "../../../../components/molecules/modal"
import { LayeredModalContext } from "../../../../components/molecules/modal/layered-modal"
import { useDebounce } from "../../../../hooks/use-debounce"
import Button from "../../../fundamentals/button"
import Modal from "../../../molecules/modal"
import { useLayeredModal } from "../../../molecules/modal/layered-modal"
import SalesChannelTable, { useSalesChannelsTableColumns } from "./table"
import { useSalesChannelsModal } from "./use-sales-channels-modal"
@@ -33,7 +33,7 @@ const AddScreen = () => {
return salesChannels?.filter(({ id }) => !ids.includes(id)) || []
}, [salesChannels, source])
const { pop, reset } = useContext(LayeredModalContext)
const { pop, reset } = useLayeredModal()
const state = useTable(
{
@@ -120,7 +120,7 @@ const AddScreen = () => {
}
export const useAddChannelsModalScreen = () => {
const { pop } = React.useContext(LayeredModalContext)
const { pop } = useLayeredModal()
return {
title: "Add Sales Channels",

View File

@@ -1,12 +1,12 @@
import React, { useState } from "react"
import { usePagination, useRowSelect, useTable } from "react-table"
import SalesChannelTable, {
SalesChannelTableActions,
useSalesChannelsTableColumns,
} from "./table"
import { usePagination, useRowSelect, useTable } from "react-table"
import Modal from "../../../../components/molecules/modal"
import { useDebounce } from "../../../../hooks/use-debounce"
import Modal from "../../../molecules/modal"
import { useSalesChannelsModal } from "./use-sales-channels-modal"
const LIMIT = 12

View File

@@ -1,10 +1,9 @@
import { SalesChannel } from "@medusajs/medusa"
import React from "react"
import Button from "../../../../components/fundamentals/button"
import Modal from "../../../../components/molecules/modal"
import Button from "../../../fundamentals/button"
import Modal from "../../../molecules/modal"
import LayeredModal, {
LayeredModalContext,
} from "../../../../components/molecules/modal/layered-modal"
useLayeredModal,
} from "../../../molecules/modal/layered-modal"
import AvailableScreen from "./available-screen"
import { SalesChannelsModalContext } from "./use-sales-channels-modal"
@@ -19,7 +18,7 @@ type Props = {
* Re-usable Sales Channels Modal, used for adding and editing sales channels both when creating a new product and editing an existing product.
*/
const SalesChannelsModal = ({ open, source = [], onClose, onSave }: Props) => {
const context = React.useContext(LayeredModalContext)
const context = useLayeredModal()
return (
<SalesChannelsModalContext.Provider

View File

@@ -2,12 +2,12 @@ import { SalesChannel } from "@medusajs/medusa"
import clsx from "clsx"
import React, { useMemo } from "react"
import { TableInstance } from "react-table"
import Button from "../../../../components/fundamentals/button"
import PlusIcon from "../../../../components/fundamentals/icons/plus-icon"
import IndeterminateCheckbox from "../../../../components/molecules/indeterminate-checkbox"
import { LayeredModalContext } from "../../../../components/molecules/modal/layered-modal"
import Table from "../../../../components/molecules/table"
import TableContainer from "../../../../components/organisms/table-container"
import Button from "../../../fundamentals/button"
import PlusIcon from "../../../fundamentals/icons/plus-icon"
import IndeterminateCheckbox from "../../../molecules/indeterminate-checkbox"
import { useLayeredModal } from "../../../molecules/modal/layered-modal"
import Table from "../../../molecules/table"
import TableContainer from "../../../organisms/table-container"
import { useAddChannelsModalScreen } from "./add-screen"
type SalesChannelsTableProps = {
@@ -181,7 +181,7 @@ export const SalesChannelTableActions = ({
"translate-y-[0px]": showAddChannels,
}
const { push } = React.useContext(LayeredModalContext)
const { push } = useLayeredModal()
return (
<div className="space-x-xsmall flex h-[34px] overflow-hidden">

View File

@@ -1,11 +1,11 @@
import { FieldArrayWithId, useFieldArray } from "react-hook-form"
import FileUploadField from "../../../../components/atoms/file-upload-field"
import TrashIcon from "../../../../components/fundamentals/icons/trash-icon"
import Actionables, {
ActionType,
} from "../../../../components/molecules/actionables"
import { FormImage } from "../../../../types/shared"
import { NestedForm } from "../../../../utils/nested-form"
import FileUploadField from "../../../atoms/file-upload-field"
import TrashIcon from "../../../fundamentals/icons/trash-icon"
import Actionables, {
ActionType
} from "../../../molecules/actionables"
export type ThumbnailFormType = {
images: FormImage[]

View File

@@ -1,11 +1,11 @@
import { UseFormReturn } from "react-hook-form"
import InputError from "../../../../../components/atoms/input-error"
import IconTooltip from "../../../../../components/molecules/icon-tooltip"
import Accordion from "../../../../../components/organisms/accordion"
import { nestedForm } from "../../../../../utils/nested-form"
import InputError from "../../../../atoms/input-error"
import IconTooltip from "../../../../molecules/icon-tooltip"
import Accordion from "../../../../organisms/accordion"
import { PricesFormType } from "../../../general/prices-form"
import CustomsForm, { CustomsFormType } from "../../customs-form"
import DimensionsForm, { DimensionsFormType } from "../../dimensions-form"
import { PricesFormType } from "../../prices-form"
import VariantGeneralForm, {
VariantGeneralFormType,
} from "../variant-general-form"

View File

@@ -1,18 +1,18 @@
import { useFieldArray, UseFormReturn } from "react-hook-form"
import CustomsForm, { CustomsFormType } from "../../customs-form"
import DimensionsForm, { DimensionsFormType } from "../../dimensions-form"
import { UseFormReturn, useFieldArray } from "react-hook-form"
import VariantGeneralForm, {
VariantGeneralFormType,
VariantGeneralFormType
} from "../variant-general-form"
import VariantStockForm, { VariantStockFormType } from "../variant-stock-form"
import Accordion from "../../../../../components/organisms/accordion"
import IconTooltip from "../../../../../components/molecules/icon-tooltip"
import InputField from "../../../../../components/molecules/input"
import { PricesFormType } from "../../prices-form"
import VariantPricesForm from "../variant-prices-form"
import { nestedForm } from "../../../../../utils/nested-form"
import { useFeatureFlag } from "../../../../../providers/feature-flag-provider"
import { nestedForm } from "../../../../../utils/nested-form"
import IconTooltip from "../../../../molecules/icon-tooltip"
import InputField from "../../../../molecules/input"
import Accordion from "../../../../organisms/accordion"
import { PricesFormType } from "../../../general/prices-form"
import VariantPricesForm from "../variant-prices-form"
export type EditFlowVariantFormType = {
/**

View File

@@ -1,6 +1,6 @@
import InputField from "../../../../../components/molecules/input"
import FormValidator from "../../../../../utils/form-validator"
import { NestedForm } from "../../../../../utils/nested-form"
import InputField from "../../../../molecules/input"
export type VariantGeneralFormType = {
title: string | null

View File

@@ -1,5 +1,5 @@
import { NestedForm } from "../../../../../utils/nested-form"
import PricesForm, { PricesFormType } from "../../prices-form"
import PricesForm, { PricesFormType } from "../../../general/prices-form"
type Props = {
form: NestedForm<PricesFormType>

View File

@@ -2,8 +2,8 @@ import { isEqual } from "lodash"
import { useMemo } from "react"
import { useWatch } from "react-hook-form"
import { VariantOptionValueType } from "."
import { AddVariantsFormType } from "../../../../../domain/products/new/add-variants"
import { NestedForm } from "../../../../../utils/nested-form"
import { AddVariantsFormType } from "../../../new/add-variants"
const useCheckOptions = (variantForm: NestedForm<AddVariantsFormType>) => {
const { control: variantControl, path: variantPath } = variantForm

View File

@@ -1,6 +1,6 @@
import { Controller, useFieldArray } from "react-hook-form"
import { NextCreateableSelect } from "../../../../../components/molecules/select/next-select"
import { NestedForm } from "../../../../../utils/nested-form"
import { NextCreateableSelect } from "../../../../molecules/select/next-select"
export type VariantOptionValueType = {
option_id: string

View File

@@ -1,20 +1,20 @@
import { Controller, useFieldArray } from "react-hook-form"
import { InventoryLevelDTO, StockLocationDTO } from "@medusajs/medusa"
import { Controller, useFieldArray } from "react-hook-form"
import BuildingsIcon from "../../../../../components/fundamentals/icons/buildings-icon"
import Button from "../../../../../components/fundamentals/button"
import FeatureToggle from "../../../../../components/fundamentals/feature-toggle"
import IconBadge from "../../../../../components/fundamentals/icon-badge"
import InputField from "../../../../../components/molecules/input"
import { LayeredModalContext } from "../../../../../components/molecules/modal/layered-modal"
import { ManageLocationsScreen } from "../../variant-inventory-form/variant-stock-form"
import { NestedForm } from "../../../../../utils/nested-form"
import React from "react"
import Switch from "../../../../../components/atoms/switch"
import clsx from "clsx"
import { sum } from "lodash"
import { useAdminStockLocations } from "medusa-react"
import React from "react"
import { useFeatureFlag } from "../../../../../providers/feature-flag-provider"
import { NestedForm } from "../../../../../utils/nested-form"
import Switch from "../../../../atoms/switch"
import Button from "../../../../fundamentals/button"
import FeatureToggle from "../../../../fundamentals/feature-toggle"
import IconBadge from "../../../../fundamentals/icon-badge"
import BuildingsIcon from "../../../../fundamentals/icons/buildings-icon"
import InputField from "../../../../molecules/input"
import { LayeredModalContext } from "../../../../molecules/modal/layered-modal"
import { ManageLocationsScreen } from "../../variant-inventory-form/variant-stock-form"
export type VariantStockFormType = {
manage_inventory: boolean

View File

@@ -1,16 +1,15 @@
import { Controller, useFieldArray } from "react-hook-form"
import { InventoryLevelDTO, StockLocationDTO } from "@medusajs/medusa"
import React, { useContext, useMemo, useState } from "react"
import BuildingsIcon from "../../../../../components/fundamentals/icons/buildings-icon"
import Button from "../../../../../components/fundamentals/button"
import IconBadge from "../../../../../components/fundamentals/icon-badge"
import InputField from "../../../../../components/molecules/input"
import { LayeredModalContext } from "../../../../../components/molecules/modal/layered-modal"
import Modal from "../../../../../components/molecules/modal"
import { NestedForm } from "../../../../../utils/nested-form"
import Switch from "../../../../../components/atoms/switch"
import { useAdminStockLocations } from "medusa-react"
import React, { useMemo, useState } from "react"
import { Controller, useFieldArray } from "react-hook-form"
import { NestedForm } from "../../../../../utils/nested-form"
import Switch from "../../../../atoms/switch"
import Button from "../../../../fundamentals/button"
import IconBadge from "../../../../fundamentals/icon-badge"
import BuildingsIcon from "../../../../fundamentals/icons/buildings-icon"
import InputField from "../../../../molecules/input"
import Modal from "../../../../molecules/modal"
import { useLayeredModal } from "../../../../molecules/modal/layered-modal"
export type VariantStockFormType = {
manage_inventory?: boolean
@@ -34,7 +33,7 @@ const VariantStockForm = ({ form, locationLevels }: Props) => {
[locationLevels]
)
const layeredModalContext = useContext(LayeredModalContext)
const layeredModalContext = useLayeredModal()
const { stock_locations: locations, isLoading } = useAdminStockLocations()
@@ -238,7 +237,7 @@ const ManageLocationsForm = ({
locationOptions,
onSubmit,
}: ManageLocationFormProps) => {
const layeredModalContext = useContext(LayeredModalContext)
const layeredModalContext = useLayeredModal()
const { pop } = layeredModalContext
const existingLocations = useMemo(() => {

View File

@@ -2,6 +2,7 @@ import clsx from "clsx"
import React, { useImperativeHandle } from "react"
import CheckIcon from "../../fundamentals/icons/check-icon"
import MinusIcon from "../../fundamentals/icons/minus-icon"
type IndeterminateCheckboxProps = {
type?: "checkbox" | "radio"
@@ -66,6 +67,7 @@ const IndeterminateCheckbox = React.forwardRef<
>
<span className="self-center">
{checked && <CheckIcon size={16} />}
{indeterminate && <MinusIcon size={16} />}
</span>
</div>
<input

View File

@@ -1,262 +0,0 @@
import { Product } from "@medusajs/medusa"
import { useAdminCreateVariant } from "medusa-react"
import React from "react"
import { Controller, useForm } from "react-hook-form"
import useNotification from "../../../hooks/use-notification"
import { getErrorMessage } from "../../../utils/error-messages"
import FormValidator from "../../../utils/form-validator"
import Button from "../../fundamentals/button"
import PlusIcon from "../../fundamentals/icons/plus-icon"
import TrashIcon from "../../fundamentals/icons/trash-icon"
import IconTooltip from "../../molecules/icon-tooltip"
import Modal from "../../molecules/modal"
import CurrencyInput from "../currency-input"
import { useValuesFieldArray } from "./use-values-field-array"
type AddDenominationModalProps = {
giftCard: Omit<Product, "beforeInsert">
storeCurrency: string
currencyCodes: string[]
handleClose: () => void
}
const AddDenominationModal: React.FC<AddDenominationModalProps> = ({
giftCard,
storeCurrency,
currencyCodes,
handleClose,
}) => {
const { watch, handleSubmit, control } = useForm<{
default_price: number
prices: {
price: {
amount: number
currency_code: string
}
}[]
}>()
const notification = useNotification()
const { mutate, isLoading } = useAdminCreateVariant(giftCard.id)
// passed to useValuesFieldArray so new prices are intialized with the currenct default price
const defaultValue = watch("default_price", 10000)
const { fields, appendPrice, deletePrice, availableCurrencies } =
useValuesFieldArray(
currencyCodes,
{
control,
name: "prices",
keyName: "indexId",
},
{
defaultAmount: defaultValue,
defaultCurrencyCode: storeCurrency,
}
)
const onSubmit = async (data: any) => {
const prices = [
{
amount: data.default_price,
currency_code: storeCurrency,
},
]
if (data.prices) {
data.prices.forEach((p) => {
prices.push({
amount: p.price.amount,
currency_code: p.price.currency_code,
})
})
}
mutate(
{
title: `${giftCard.variants.length}`,
options: [
{
value: `${data.default_price}`,
option_id: giftCard.options[0].id,
},
],
prices,
inventory_quantity: 0,
manage_inventory: false,
},
{
onSuccess: () => {
notification("Success", "Denomination added successfully", "success")
handleClose()
},
onError: (error) => {
const errorMessage = () => {
// @ts-ignore
if (error.response?.data?.type === "duplicate_error") {
return `A denomination with that default value already exists`
} else {
return getErrorMessage(error)
}
}
notification("Error", errorMessage(), "error")
},
}
)
}
return (
<Modal handleClose={handleClose} isLargeModal>
<form onSubmit={handleSubmit(onSubmit)}>
<Modal.Body>
<Modal.Header handleClose={handleClose}>
<span className="inter-xlarge-semibold">Add Denomination</span>
</Modal.Header>
<Modal.Content>
<div className="mb-xlarge flex-1">
<div className="mb-base flex gap-x-2">
<h3 className="inter-base-semibold">Default Value</h3>
<IconTooltip content="This is the denomination in your store's default currency" />
</div>
<Controller
control={control}
name="default_price"
rules={{
required: "Default value is required",
max: FormValidator.maxInteger("Default value", storeCurrency),
}}
render={({ field: { onChange, value, ref } }) => {
return (
<CurrencyInput.Root
currentCurrency={storeCurrency}
readOnly
size="medium"
>
<CurrencyInput.Amount
ref={ref}
label="Amount"
amount={value}
onChange={onChange}
/>
</CurrencyInput.Root>
)
}}
/>
</div>
<div>
<div className="mb-base flex gap-x-2">
<h3 className="inter-base-semibold">Other Values</h3>
<IconTooltip content="Here you can add values in other currencies" />
</div>
<div className="gap-y-xsmall flex flex-col">
{fields.map((field, index) => {
return (
<div
key={field.indexId}
className="mb-xsmall flex items-end last:mb-0"
>
<div className="flex-1">
<Controller
control={control}
key={field.indexId}
name={`prices.${index}.price`}
rules={{
required: FormValidator.required("Price"),
validate: (val) => {
return FormValidator.validateMaxInteger(
"Price",
val.amount,
val.currency_code
)
},
}}
defaultValue={field.price}
render={({ field: { onChange, value } }) => {
const codes = [
value?.currency_code,
...availableCurrencies,
]
codes.sort()
return (
<CurrencyInput.Root
currencyCodes={codes}
currentCurrency={value?.currency_code}
size="medium"
readOnly={index === 0}
onChange={(code) =>
onChange({ ...value, currency_code: code })
}
>
<CurrencyInput.Amount
label="Amount"
onChange={(amount) =>
onChange({ ...value, amount })
}
amount={value?.amount}
/>
</CurrencyInput.Root>
)
}}
/>
</div>
<Button
variant="ghost"
size="small"
className="ml-large h-10 w-10"
type="button"
>
<TrashIcon
onClick={deletePrice(index)}
className="text-grey-40"
size="20"
/>
</Button>
</div>
)
})}
</div>
<div className="mt-large mb-small">
<Button
onClick={appendPrice}
type="button"
variant="ghost"
size="small"
disabled={availableCurrencies?.length === 0}
>
<PlusIcon size={20} />
Add a price
</Button>
</div>
</div>
</Modal.Content>
<Modal.Footer>
<div className="flex w-full justify-end">
<Button
variant="ghost"
size="small"
onClick={handleClose}
className="mr-2 min-w-[130px] justify-center"
>
Cancel
</Button>
<Button
variant="primary"
size="small"
className="mr-2 min-w-[130px] justify-center"
type="submit"
loading={isLoading}
disabled={isLoading}
>
Save
</Button>
</div>
</Modal.Footer>
</Modal.Body>
</form>
</Modal>
)
}
export default AddDenominationModal

View File

@@ -1,72 +0,0 @@
import { useFieldArray, UseFieldArrayOptions, useWatch } from "react-hook-form"
type UseValuesFieldArrayOptions = {
defaultAmount: number
defaultCurrencyCode: string
}
type ValuesFormValue = {
price: {
currency_code: string
amount: number
}
}
export const useValuesFieldArray = <TKeyName extends string = "id">(
currencyCodes: string[],
{ control, name, keyName }: UseFieldArrayOptions<TKeyName>,
options: UseValuesFieldArrayOptions = {
defaultAmount: 1000,
defaultCurrencyCode: "usd",
}
) => {
const { defaultAmount } = options
const { fields, append, remove } = useFieldArray<ValuesFormValue, TKeyName>({
control,
name,
keyName,
})
const watchedFields = useWatch({
control,
name,
defaultValue: fields,
})
const selectedCurrencies = watchedFields.map(
(field) => field?.price?.currency_code
)
const availableCurrencies = currencyCodes?.filter(
(currency) => !selectedCurrencies.includes(currency)
)
const controlledFields = fields.map((field, index) => {
return {
...field,
...watchedFields[index],
}
})
const appendPrice = () => {
const newCurrency = availableCurrencies[0]
append({
price: {
currency_code: newCurrency,
amount: defaultAmount,
},
})
}
const deletePrice = (index) => {
return () => {
remove(index)
}
}
return {
fields: controlledFields,
appendPrice,
deletePrice,
availableCurrencies,
selectedCurrencies,
} as const
}

View File

@@ -1,179 +0,0 @@
import _ from "lodash"
import * as React from "react"
import { v4 as uuidv4 } from "uuid"
import Button from "../../fundamentals/button"
import PlusIcon from "../../fundamentals/icons/plus-icon"
import TrashIcon from "../../fundamentals/icons/trash-icon"
import IconTooltip from "../../molecules/icon-tooltip"
import Modal from "../../molecules/modal"
import CurrencyInput from "../../organisms/currency-input"
export type PriceType = {
currency_code: string
amount: number
id?: string
}
type EditDenominationsModalProps = {
defaultDenominations: PriceType[]
handleClose: () => void
onSubmit: (denominations: PriceType[]) => void
defaultNewAmount?: number
defaultNewCurrencyCode?: string
currencyCodes?: string[]
}
const EditDenominationsModal = ({
defaultDenominations = [],
onSubmit,
handleClose,
currencyCodes = [],
defaultNewAmount = 1000,
}: EditDenominationsModalProps) => {
const [denominations, setDenominations] = React.useState(
augmentWithIds(defaultDenominations)
)
const selectedCurrencies = denominations.map(
(denomination) => denomination.currency_code
)
const availableCurrencies = currencyCodes?.filter(
(currency) => !selectedCurrencies.includes(currency)
)
const onAmountChange = (index) => {
return (amount) => {
const newDenominations = denominations.slice()
newDenominations[index] = { ...newDenominations[index], amount }
setDenominations(newDenominations)
}
}
const onCurrencyChange = (index) => {
return (currencyCode) => {
const newDenominations = denominations.slice()
newDenominations[index] = {
...newDenominations[index],
currency_code: currencyCode,
}
setDenominations(newDenominations)
}
}
const onClickDelete = (index) => {
return () => {
const newDenominations = denominations.slice()
newDenominations.splice(index, 1)
setDenominations(newDenominations)
}
}
const appendDenomination = () => {
const newDenomination = {
amount: defaultNewAmount,
currency_code: availableCurrencies[0],
}
setDenominations([...denominations, augmentWithId(newDenomination)])
}
const submitHandler = () => {
const strippedDenominations = stripDenominationFromIndexId(denominations)
if (onSubmit) {
onSubmit(strippedDenominations)
}
}
return (
<Modal handleClose={handleClose}>
<Modal.Body>
<Modal.Header handleClose={handleClose}>
<span className="inter-xlarge-semibold">Edit Denominations</span>
</Modal.Header>
<Modal.Content>
<div className="pt-1">
<div className="flex items-center">
<label className="inter-base-semibold text-grey-90 mr-1.5">
Prices
</label>
<IconTooltip content={"Helpful denominations"} />
</div>
{denominations.map((field, index) => {
return (
<div
key={field.indexId}
className="mt-xsmall flex items-center first:mt-0"
>
<div className="flex-1">
<CurrencyInput.Root
currencyCodes={currencyCodes}
currentCurrency={field.currency_code}
onChange={onCurrencyChange(index)}
size="medium"
>
<CurrencyInput.Amount
label="Amount"
onChange={onAmountChange(index)}
amount={field.amount}
/>
</CurrencyInput.Root>
</div>
<button className="ml-2xlarge">
<TrashIcon
onClick={onClickDelete(index)}
className="text-grey-40"
size="20"
/>
</button>
</div>
)
})}
</div>
<div className="mt-large">
<Button
onClick={appendDenomination}
type="button"
variant="ghost"
size="small"
disabled={availableCurrencies.length === 0}
>
<PlusIcon size={20} />
Add a price
</Button>
</div>
</Modal.Content>
<Modal.Footer>
<div className="flex w-full justify-end">
<Button
variant="ghost"
size="small"
onClick={handleClose}
className="mr-2 min-w-[130px] justify-center"
>
Cancel
</Button>
<Button
variant="primary"
size="small"
className="mr-2 min-w-[130px] justify-center"
onClick={submitHandler}
>
Save
</Button>
</div>
</Modal.Footer>
</Modal.Body>
</Modal>
)
}
export default EditDenominationsModal
const augmentWithId = (obj) => ({ ...obj, indexId: uuidv4() })
const augmentWithIds = (list) => {
return list.map(augmentWithId)
}
const stripDenominationFromIndexId = (list) => {
return list.map((element) => _.omit(element, "indexId"))
}

View File

@@ -0,0 +1,175 @@
import { Product } from "@medusajs/medusa"
import { useAdminCreateVariant, useAdminStore } from "medusa-react"
import { useCallback, useMemo } from "react"
import { useForm } from "react-hook-form"
import useNotification from "../../../hooks/use-notification"
import { getErrorMessage } from "../../../utils/error-messages"
import { nestedForm } from "../../../utils/nested-form"
import DenominationForm, {
DenominationFormType,
} from "../../forms/gift-card/denomination-form"
import Button from "../../fundamentals/button"
import Modal from "../../molecules/modal"
type Props = {
/**
* Whether the modal is open or not
*/
open: boolean
/**
* Callback to close the modal
*/
onClose: () => void
/**
* Gift card
*/
giftCard: Product
}
type AddDenominationModalFormType = {
denominations: DenominationFormType
}
const AddDenominationModal = ({ open, onClose, giftCard }: Props) => {
const { mutate, isLoading: isMutating } = useAdminCreateVariant(giftCard.id)
const { store } = useAdminStore()
const defaultValues: AddDenominationModalFormType | undefined =
useMemo(() => {
if (!store) {
return undefined
}
return {
denominations: {
defaultDenomination: {
currency_code: store.default_currency_code,
includes_tax: store.currencies.find(
(c) => c.code === store.default_currency_code
)?.includes_tax,
},
currencyDenominations: store.currencies
.filter((c) => c.code !== store.default_currency_code)
.map((currency) => {
return {
currency_code: currency.code,
includes_tax: currency.includes_tax,
}
}),
},
} as AddDenominationModalFormType
}, [store])
const form = useForm<AddDenominationModalFormType>({
defaultValues,
})
const {
handleSubmit,
reset,
formState: { isDirty },
} = form
const notification = useNotification()
const handleClose = useCallback(() => {
reset(defaultValues)
onClose()
}, [reset, defaultValues, onClose])
const onSubmit = handleSubmit((data) => {
const payload = {
title: `${giftCard.variants.length}`,
options: [
{
value: `${data.denominations.defaultDenomination.amount}`,
option_id: giftCard.options[0].id,
},
],
prices: [
{
amount: data.denominations.defaultDenomination.amount,
currency_code: data.denominations.defaultDenomination.currency_code,
},
],
inventory_quantity: 0,
manage_inventory: false,
}
data.denominations.currencyDenominations.forEach((currency) => {
if (
(currency.amount !== null && currency.amount !== undefined) ||
data.denominations.useSameValue
) {
payload.prices.push({
amount: data.denominations.useSameValue
? data.denominations.defaultDenomination.amount
: currency.amount,
currency_code: currency.currency_code,
})
}
})
mutate(payload, {
onSuccess: () => {
notification(
"Denomination added",
"A new denomination was succesfully added",
"success"
)
handleClose()
},
onError: (error) => {
const errorMessage = () => {
// @ts-ignore
if (error.response?.data?.type === "duplicate_error") {
return `A denomination with that default value already exists`
} else {
return getErrorMessage(error)
}
}
notification("Error", errorMessage(), "error")
},
})
})
return (
<Modal open={open} handleClose={handleClose}>
<Modal.Body>
<Modal.Header handleClose={handleClose}>
<h1 className="inter-xlarge-semibold">Add Denomination</h1>
</Modal.Header>
<form onSubmit={onSubmit}>
<Modal.Content>
<DenominationForm form={nestedForm(form, "denominations")} />
</Modal.Content>
<Modal.Footer>
<div className="gap-x-xsmall flex w-full items-center justify-end">
<Button
variant="secondary"
size="small"
type="button"
onClick={handleClose}
>
Cancel
</Button>
<Button
variant="primary"
size="small"
type="submit"
disabled={isMutating || !isDirty}
loading={isMutating}
>
Save and close
</Button>
</div>
</Modal.Footer>
</form>
</Modal.Body>
</Modal>
)
}
export default AddDenominationModal

View File

@@ -0,0 +1,75 @@
import { ProductVariant } from "@medusajs/medusa"
import {
flexRender,
getCoreRowModel,
useReactTable,
} from "@tanstack/react-table"
import Table from "../../molecules/table"
import { useDenominationColumns } from "./use-denominations-columns"
type DenominationsTableProps = {
denominations: ProductVariant[]
}
const DenominationsTable = ({ denominations }: DenominationsTableProps) => {
const columns = useDenominationColumns()
const { getHeaderGroups, getRowModel } = useReactTable({
columns,
data: denominations,
getCoreRowModel: getCoreRowModel(),
})
return (
<Table>
<Table.Head>
{getHeaderGroups().map((headerGroup) => {
return (
<Table.HeadRow key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<Table.HeadCell
key={header.id}
className="inter-small-semibold text-grey-50"
style={{
width: header.getSize(),
maxWidth: header.getSize(),
}}
>
{flexRender(
header.column.columnDef.header,
header.getContext()
)}
</Table.HeadCell>
)
})}
</Table.HeadRow>
)
})}
</Table.Head>
<Table.Body>
{getRowModel().rows.map((row) => {
return (
<Table.Row key={row.id} className="last-of-type:border-b-0">
{row.getVisibleCells().map((cell) => {
return (
<Table.Cell
key={cell.id}
style={{
width: cell.column.getSize(),
maxWidth: cell.column.getSize(),
}}
>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</Table.Cell>
)
})}
</Table.Row>
)
})}
</Table.Body>
</Table>
)
}
export default DenominationsTable

View File

@@ -0,0 +1,191 @@
import { MoneyAmount, ProductVariant, Store } from "@medusajs/medusa"
import { useQueryClient } from "@tanstack/react-query"
import {
adminProductKeys,
useAdminStore,
useAdminUpdateVariant,
} from "medusa-react"
import { useCallback, useEffect } from "react"
import { useForm } from "react-hook-form"
import useNotification from "../../../hooks/use-notification"
import { getErrorMessage } from "../../../utils/error-messages"
import { nestedForm } from "../../../utils/nested-form"
import DenominationForm, {
DenominationFormType,
} from "../../forms/gift-card/denomination-form"
import Button from "../../fundamentals/button"
import Modal from "../../molecules/modal"
type EditDenominationsModalProps = {
open: boolean
onClose: () => void
denomination: ProductVariant
}
type EditDenominationModalFormType = {
denominations: DenominationFormType
}
const EditDenominationsModal = ({
denomination,
onClose,
open,
}: EditDenominationsModalProps) => {
const { store } = useAdminStore()
const { mutate, isLoading } = useAdminUpdateVariant(denomination.product_id)
const queryClient = useQueryClient()
const form = useForm<EditDenominationModalFormType>({
defaultValues: getDefaultValues(store, denomination.prices),
})
const {
handleSubmit,
reset,
formState: { isDirty },
} = form
const notification = useNotification()
useEffect(() => {
if (open) {
reset(getDefaultValues(store, denomination.prices))
}
}, [open, store, denomination.prices, reset])
const handleClose = useCallback(() => {
reset()
onClose()
}, [reset, onClose])
const onSubmit = handleSubmit((data) => {
const payload = {
prices: [
{
amount: data.denominations.defaultDenomination.amount,
currency_code: data.denominations.defaultDenomination.currency_code,
},
],
}
data.denominations.currencyDenominations.forEach((currency) => {
if (
(currency.amount !== undefined && currency.amount !== null) ||
data.denominations.useSameValue
) {
payload.prices.push({
amount: data.denominations.useSameValue
? data.denominations.defaultDenomination.amount
: currency.amount,
currency_code: currency.currency_code,
})
}
})
mutate(
{
variant_id: denomination.id,
...payload,
},
{
onSuccess: () => {
notification(
"Denomination updated",
"A new denomination was succesfully updated",
"success"
)
queryClient.invalidateQueries(adminProductKeys.all)
handleClose()
},
onError: (error) => {
notification("Error", getErrorMessage(error), "error")
},
}
)
})
return (
<Modal open={open} handleClose={handleClose}>
<Modal.Body>
<Modal.Header handleClose={handleClose}>
<h1 className="inter-xlarge-semibold">Edit Denominations</h1>
</Modal.Header>
<form onSubmit={onSubmit}>
<Modal.Content>
<DenominationForm form={nestedForm(form, "denominations")} />
</Modal.Content>
<Modal.Footer>
<div className="gap-x-2xsmall flex w-full items-center justify-end">
<Button
variant="secondary"
size="small"
type="button"
onClick={handleClose}
>
Cancel
</Button>
<Button
variant="primary"
size="small"
type="submit"
loading={isLoading}
disabled={!isDirty || isLoading}
>
Save and close
</Button>
</div>
</Modal.Footer>
</form>
</Modal.Body>
</Modal>
)
}
const getDefaultValues = (store: Store | undefined, prices: MoneyAmount[]) => {
if (!store) {
return undefined
}
const defaultValues = {
denominations: {
defaultDenomination: {
currency_code: store.default_currency_code,
includes_tax: store.currencies.find(
(c) => c.code === store.default_currency_code
)?.includes_tax,
amount: findPrice(store.default_currency_code, prices),
},
currencyDenominations: store.currencies
.filter((c) => c.code !== store.default_currency_code)
.map((currency) => {
return {
currency_code: currency.code,
includes_tax: currency.includes_tax,
amount: findPrice(currency.code, prices),
}
}),
},
} as EditDenominationModalFormType
if (
defaultValues.denominations.currencyDenominations.every(
(c) => c.amount === defaultValues.denominations.defaultDenomination.amount
)
) {
defaultValues.denominations.useSameValue = true
}
return defaultValues
}
const findPrice = (currencyCode: string, prices: MoneyAmount[]) => {
return prices.find(
(p) =>
p.currency_code === currencyCode &&
p.region_id === null &&
p.price_list_id === null
)?.amount
}
export default EditDenominationsModal

View File

@@ -0,0 +1,48 @@
import { Product } from "@medusajs/medusa"
import useToggleState from "../../../hooks/use-toggle-state"
import PlusIcon from "../../fundamentals/icons/plus-icon"
import Section from "../section"
import AddDenominationModal from "./add-denominations-modal"
import DenominationsTable from "./denominations-table"
type GiftCardDenominationsSectionProps = {
giftCard: Product
}
const GiftCardDenominationsSection = ({
giftCard,
}: GiftCardDenominationsSectionProps) => {
const {
state: addDenomination,
close: closeAddDenomination,
open: openAddDenomination,
} = useToggleState()
return (
<>
<Section
title="Denominations"
forceDropdown
actions={[
{
label: "Add Denomination",
onClick: openAddDenomination,
icon: <PlusIcon size={20} />,
},
]}
>
<div className="mb-large mt-base">
<DenominationsTable denominations={[...giftCard.variants]} />
</div>
</Section>
<AddDenominationModal
giftCard={giftCard}
open={addDenomination}
onClose={closeAddDenomination}
/>
</>
)
}
export default GiftCardDenominationsSection

View File

@@ -0,0 +1,176 @@
import { MoneyAmount, ProductVariant } from "@medusajs/medusa"
import { createColumnHelper } from "@tanstack/react-table"
import { useAdminDeleteVariant, useAdminStore } from "medusa-react"
import { useMemo } from "react"
import useImperativeDialog from "../../../hooks/use-imperative-dialog"
import useNotification from "../../../hooks/use-notification"
import useToggleState from "../../../hooks/use-toggle-state"
import { getErrorMessage } from "../../../utils/error-messages"
import { normalizeAmount } from "../../../utils/prices"
import Tooltip from "../../atoms/tooltip"
import EditIcon from "../../fundamentals/icons/edit-icon"
import TrashIcon from "../../fundamentals/icons/trash-icon"
import Actionables, { ActionType } from "../../molecules/actionables"
import EditDenominationsModal from "./edit-denominations-modal"
const columnHelper = createColumnHelper<ProductVariant>()
export const useDenominationColumns = () => {
const { store } = useAdminStore()
const columns = useMemo(() => {
if (!store) {
return []
}
const defaultCurrency = store.default_currency_code
return [
columnHelper.display({
header: "Denomination",
id: "denomination",
cell: ({ row }) => {
const defaultDenomination = row.original.prices.find(
(p) =>
p.currency_code === defaultCurrency &&
p.region_id === null &&
p.price_list_id === null
)
return defaultDenomination ? (
<p>
{normalizeAmount(defaultCurrency, defaultDenomination.amount)}{" "}
<span className="text-grey-50">
{defaultCurrency.toUpperCase()}
</span>
</p>
) : (
"-"
)
},
}),
columnHelper.display({
header: "In other currencies",
id: "other_currencies",
cell: ({ row }) => {
const otherCurrencies = row.original.prices.filter(
(p) =>
p.currency_code !== defaultCurrency &&
p.region_id === null &&
p.price_list_id === null
)
let remainder: MoneyAmount[] = []
if (otherCurrencies.length > 2) {
remainder = otherCurrencies.splice(2)
}
return otherCurrencies.length > 0 ? (
<p>
{otherCurrencies.map((p, index) => {
return (
<span key={index}>
{normalizeAmount(p.currency_code, p.amount)}{" "}
<span className="text-grey-50">
{p.currency_code.toUpperCase()}
</span>
{index < otherCurrencies.length - 1 && ", "}
</span>
)
})}
{remainder.length > 0 && (
<Tooltip
content={
<ul>
{remainder.map((p, index) => {
return (
<li key={index}>
<p>
{normalizeAmount(p.currency_code, p.amount)}{" "}
<span className="text-grey-50">
{p.currency_code.toUpperCase()}
</span>
</p>
</li>
)
})}
</ul>
}
>
<span className="text-grey-50 cursor-default">{`, and ${remainder.length} more`}</span>
</Tooltip>
)}
</p>
) : (
"-"
)
},
}),
columnHelper.display({
id: "actions",
cell: ({ row: { original } }) => <Actions original={original} />,
}),
]
}, [store])
return columns
}
const Actions = ({ original }: { original: ProductVariant }) => {
const { state, open, close } = useToggleState()
const { mutateAsync } = useAdminDeleteVariant(original.product_id)
const notification = useNotification()
const dialog = useImperativeDialog()
const onDelete = async () => {
const shouldDelete = await dialog({
heading: "Delete denomination",
text: "Are you sure you want to delete this denomination?",
})
if (shouldDelete) {
mutateAsync(original.id, {
onSuccess: () => {
notification(
"Denomination deleted",
"Denomination was successfully deleted",
"success"
)
},
onError: (error) => {
notification("Error", getErrorMessage(error), "error")
},
})
}
}
const actions: ActionType[] = [
{
label: "Edit",
onClick: open,
icon: <EditIcon size={20} />,
},
{
label: "Delete",
onClick: onDelete,
icon: <TrashIcon size={20} />,
variant: "danger",
},
]
return (
<>
<div className="flex items-center justify-end">
<Actionables actions={actions} forceDropdown />
</div>
<EditDenominationsModal
open={state}
onClose={close}
denomination={original}
/>
</>
)
}

View File

@@ -1,15 +1,15 @@
import { Product } from "@medusajs/medusa"
import { useEffect } from "react"
import { useForm } from "react-hook-form"
import Button from "../../../../../components/fundamentals/button"
import Modal from "../../../../../components/molecules/modal"
import { countries } from "../../../../../utils/countries"
import { nestedForm } from "../../../../../utils/nested-form"
import CustomsForm, { CustomsFormType } from "../../../components/customs-form"
import useEditProductActions from "../../../hooks/use-edit-product-actions"
import { countries } from "../../../utils/countries"
import { nestedForm } from "../../../utils/nested-form"
import CustomsForm, { CustomsFormType } from "../../forms/product/customs-form"
import DimensionsForm, {
DimensionsFormType,
} from "../../../components/dimensions-form"
import useEditProductActions from "../../hooks/use-edit-product-actions"
} from "../../forms/product/dimensions-form"
import Button from "../../fundamentals/button"
import Modal from "../../molecules/modal"
type Props = {
product: Product

View File

@@ -1,15 +1,15 @@
import { Product } from "@medusajs/medusa"
import EditIcon from "../../../../../components/fundamentals/icons/edit-icon"
import { ActionType } from "../../../../../components/molecules/actionables"
import Section from "../../../../../components/organisms/section"
import useToggleState from "../../../../../hooks/use-toggle-state"
import useToggleState from "../../../hooks/use-toggle-state"
import EditIcon from "../../fundamentals/icons/edit-icon"
import { ActionType } from "../../molecules/actionables"
import Section from "../section"
import AttributeModal from "./attribute-modal"
type Props = {
product: Product
}
const AttributesSection = ({ product }: Props) => {
const ProductAttributesSection = ({ product }: Props) => {
const { state, toggle, close } = useToggleState()
const actions: ActionType[] = [
@@ -64,4 +64,4 @@ const Attribute = ({ attribute, value }: AttributeProps) => {
)
}
export default AttributesSection
export default ProductAttributesSection

View File

@@ -1,6 +1,6 @@
import { Product, SalesChannel } from "@medusajs/medusa"
import { useAdminUpdateProduct } from "medusa-react"
import SalesChannelsModal from "../../../components/sales-channels-modal"
import SalesChannelsModal from "../../forms/product/sales-channels-modal"
type Props = {
product: Product

View File

@@ -1,17 +1,17 @@
import { Product } from "@medusajs/medusa"
import { useEffect } from "react"
import { useForm } from "react-hook-form"
import Button from "../../../../../components/fundamentals/button"
import Modal from "../../../../../components/molecules/modal"
import { nestedForm } from "../../../../../utils/nested-form"
import useEditProductActions from "../../../hooks/use-edit-product-actions"
import { nestedForm } from "../../../utils/nested-form"
import DiscountableForm, {
DiscountableFormType,
} from "../../../components/discountable-form"
import GeneralForm, { GeneralFormType } from "../../../components/general-form"
} from "../../forms/product/discountable-form"
import GeneralForm, { GeneralFormType } from "../../forms/product/general-form"
import OrganizeForm, {
OrganizeFormType,
} from "../../../components/organize-form"
import useEditProductActions from "../../hooks/use-edit-product-actions"
} from "../../forms/product/organize-form"
import Button from "../../fundamentals/button"
import Modal from "../../molecules/modal"
type Props = {
product: Product
@@ -39,7 +39,7 @@ const GeneralModal = ({ product, open, onClose }: Props) => {
useEffect(() => {
reset(getDefaultValues(product))
}, [product])
}, [product, reset])
const onReset = () => {
reset(getDefaultValues(product))
@@ -90,12 +90,20 @@ const GeneralModal = ({ product, open, onClose }: Props) => {
</Modal.Header>
<form onSubmit={onSubmit}>
<Modal.Content>
<GeneralForm form={nestedForm(form, "general")} />
<GeneralForm
form={nestedForm(form, "general")}
isGiftCard={product.is_giftcard}
/>
<div className="my-xlarge">
<h2 className="inter-base-semibold mb-base">Organize Product</h2>
<h2 className="inter-base-semibold mb-base">
Organize {product.is_giftcard ? "Gift Card" : "Product"}
</h2>
<OrganizeForm form={nestedForm(form, "organize")} />
</div>
<DiscountableForm form={nestedForm(form, "discountable")} />
<DiscountableForm
form={nestedForm(form, "discountable")}
isGiftCard={product.is_giftcard}
/>
</Modal.Content>
<Modal.Footer>
<div className="flex w-full justify-end gap-x-2">

View File

@@ -1,19 +1,19 @@
import { Product } from "@medusajs/medusa"
import FeatureToggle from "../../../../../components/fundamentals/feature-toggle"
import ChannelsIcon from "../../../../../components/fundamentals/icons/channels-icon"
import EditIcon from "../../../../../components/fundamentals/icons/edit-icon"
import TrashIcon from "../../../../../components/fundamentals/icons/trash-icon"
import { ActionType } from "../../../../../components/molecules/actionables"
import SalesChannelsDisplay from "../../../../../components/molecules/sales-channels-display"
import StatusSelector from "../../../../../components/molecules/status-selector"
import DelimitedList from "../../../../../components/molecules/delimited-list"
import Section from "../../../../../components/organisms/section"
import useEditProductActions from "../../../hooks/use-edit-product-actions"
import useToggleState from "../../../hooks/use-toggle-state"
import {
useFeatureFlag,
FeatureFlag,
} from "../../../../../providers/feature-flag-provider"
import useToggleState from "../../../../../hooks/use-toggle-state"
import useEditProductActions from "../../hooks/use-edit-product-actions"
useFeatureFlag,
} from "../../../providers/feature-flag-provider"
import FeatureToggle from "../../fundamentals/feature-toggle"
import ChannelsIcon from "../../fundamentals/icons/channels-icon"
import EditIcon from "../../fundamentals/icons/edit-icon"
import TrashIcon from "../../fundamentals/icons/trash-icon"
import { ActionType } from "../../molecules/actionables"
import DelimitedList from "../../molecules/delimited-list"
import SalesChannelsDisplay from "../../molecules/sales-channels-display"
import StatusSelector from "../../molecules/status-selector"
import Section from "../section"
import ChannelsModal from "./channels-modal"
import GeneralModal from "./general-modal"
@@ -21,7 +21,7 @@ type Props = {
product: Product
}
const GeneralSection = ({ product }: Props) => {
const ProductGeneralSection = ({ product }: Props) => {
const { onDelete, onStatusChange } = useEditProductActions(product.id)
const {
state: infoState,
@@ -174,4 +174,4 @@ const ProductSalesChannels = ({ product }: Props) => {
)
}
export default GeneralSection
export default ProductGeneralSection

View File

@@ -1,14 +1,14 @@
import { Product } from "@medusajs/medusa"
import { ActionType } from "../../../../../components/molecules/actionables"
import Section from "../../../../../components/organisms/section"
import useToggleState from "../../../../../hooks/use-toggle-state"
import useToggleState from "../../../hooks/use-toggle-state"
import { ActionType } from "../../molecules/actionables"
import Section from "../../organisms/section"
import MediaModal from "./media-modal"
type Props = {
product: Product
}
const MediaSection = ({ product }: Props) => {
const ProductMediaSection = ({ product }: Props) => {
const { state, close, toggle } = useToggleState()
const actions: ActionType[] = [
@@ -46,4 +46,4 @@ const MediaSection = ({ product }: Props) => {
)
}
export default MediaSection
export default ProductMediaSection

View File

@@ -1,14 +1,14 @@
import { Product } from "@medusajs/medusa"
import { useEffect } from "react"
import { useForm } from "react-hook-form"
import Button from "../../../../../components/fundamentals/button"
import Modal from "../../../../../components/molecules/modal"
import useNotification from "../../../../../hooks/use-notification"
import { FormImage } from "../../../../../types/shared"
import { prepareImages } from "../../../../../utils/images"
import { nestedForm } from "../../../../../utils/nested-form"
import MediaForm, { MediaFormType } from "../../../components/media-form"
import useEditProductActions from "../../hooks/use-edit-product-actions"
import useEditProductActions from "../../../hooks/use-edit-product-actions"
import useNotification from "../../../hooks/use-notification"
import { FormImage } from "../../../types/shared"
import { prepareImages } from "../../../utils/images"
import { nestedForm } from "../../../utils/nested-form"
import MediaForm, { MediaFormType } from "../../forms/product/media-form"
import Button from "../../fundamentals/button"
import Modal from "../../molecules/modal"
type Props = {
product: Product
@@ -36,7 +36,7 @@ const MediaModal = ({ product, open, onClose }: Props) => {
useEffect(() => {
reset(getDefaultValues(product))
}, [product])
}, [product, reset])
const onReset = () => {
reset(getDefaultValues(product))

View File

@@ -1,15 +1,15 @@
import { Product } from "@medusajs/medusa"
import JSONView from "../../../../../components/molecules/json-view"
import Section from "../../../../../components/organisms/section"
import JSONView from "../../molecules/json-view"
import Section from "../section"
type Props = {
product: Product
}
/** Temporary component, should be replaced with <RawJson /> but since the design is different we will use this to not break the existing design across admin. */
const RawSection = ({ product }: Props) => {
const ProductRawSection = ({ product }: Props) => {
return (
<Section title="Raw Product">
<Section title={product.is_giftcard ? "Raw Gift Card" : "Raw Product"}>
<div className="pt-base">
<JSONView data={product} />
</div>
@@ -17,4 +17,4 @@ const RawSection = ({ product }: Props) => {
)
}
export default RawSection
export default ProductRawSection

View File

@@ -1,19 +1,19 @@
import { Product } from "@medusajs/medusa"
import clsx from "clsx"
import TwoStepDelete from "../../../../../components/atoms/two-step-delete"
import Button from "../../../../../components/fundamentals/button"
import Section from "../../../../../components/organisms/section"
import useNotification from "../../../../../hooks/use-notification"
import useToggleState from "../../../../../hooks/use-toggle-state"
import { getErrorMessage } from "../../../../../utils/error-messages"
import useEditProductActions from "../../hooks/use-edit-product-actions"
import useEditProductActions from "../../../hooks/use-edit-product-actions"
import useNotification from "../../../hooks/use-notification"
import useToggleState from "../../../hooks/use-toggle-state"
import { getErrorMessage } from "../../../utils/error-messages"
import TwoStepDelete from "../../atoms/two-step-delete"
import Button from "../../fundamentals/button"
import Section from "../../organisms/section"
import ThumbnailModal from "./thumbnail-modal"
type Props = {
product: Product
}
const TumbnailSection = ({ product }: Props) => {
const ProductThumbnailSection = ({ product }: Props) => {
const { onUpdate, updating } = useEditProductActions(product.id)
const { state, toggle, close } = useToggleState()
@@ -78,4 +78,4 @@ const TumbnailSection = ({ product }: Props) => {
)
}
export default TumbnailSection
export default ProductThumbnailSection

View File

@@ -1,16 +1,16 @@
import { Product } from "@medusajs/medusa"
import { useEffect } from "react"
import { useForm } from "react-hook-form"
import Button from "../../../../../components/fundamentals/button"
import Modal from "../../../../../components/molecules/modal"
import useNotification from "../../../../../hooks/use-notification"
import { FormImage } from "../../../../../types/shared"
import { prepareImages } from "../../../../../utils/images"
import { nestedForm } from "../../../../../utils/nested-form"
import useEditProductActions from "../../../hooks/use-edit-product-actions"
import useNotification from "../../../hooks/use-notification"
import { FormImage } from "../../../types/shared"
import { prepareImages } from "../../../utils/images"
import { nestedForm } from "../../../utils/nested-form"
import ThumbnailForm, {
ThumbnailFormType,
} from "../../../components/thumbnail-form"
import useEditProductActions from "../../hooks/use-edit-product-actions"
} from "../../forms/product/thumbnail-form"
import Button from "../../fundamentals/button"
import Modal from "../../molecules/modal"
type Props = {
product: Product
@@ -38,7 +38,7 @@ const ThumbnailModal = ({ product, open, onClose }: Props) => {
useEffect(() => {
reset(getDefaultValues(product))
}, [product])
}, [product, reset])
const onReset = () => {
reset(getDefaultValues(product))

View File

@@ -1,17 +1,17 @@
import { AdminPostProductsProductVariantsReq, Product } from "@medusajs/medusa"
import { useContext, useEffect } from "react"
import EditFlowVariantForm, {
EditFlowVariantFormType,
} from "../../../components/variant-form/edit-flow-variant-form"
} from "../../forms/product/variant-form/edit-flow-variant-form"
import LayeredModal, {
LayeredModalContext,
} from "../../../../../components/molecules/modal/layered-modal"
import { useContext, useEffect } from "react"
} from "../../molecules/modal/layered-modal"
import Button from "../../../../../components/fundamentals/button"
import Modal from "../../../../../components/molecules/modal"
import useEditProductActions from "../../hooks/use-edit-product-actions"
import { useForm } from "react-hook-form"
import { useMedusa } from "medusa-react"
import { useForm } from "react-hook-form"
import useEditProductActions from "../../../hooks/use-edit-product-actions"
import Button from "../../fundamentals/button"
import Modal from "../../molecules/modal"
type Props = {
onClose: () => void
@@ -32,7 +32,7 @@ const AddVariantModal = ({ open, onClose, product }: Props) => {
useEffect(() => {
reset(getDefaultValues(product))
}, [product])
}, [product, reset])
const resetAndClose = () => {
reset(getDefaultValues(product))
@@ -40,7 +40,7 @@ const AddVariantModal = ({ open, onClose, product }: Props) => {
}
const createStockLocationsForVariant = async (
productRes,
productRes: Product,
stock_locations: { stocked_quantity: number; location_id: string }[]
) => {
const { variants } = productRes
@@ -48,9 +48,11 @@ const AddVariantModal = ({ open, onClose, product }: Props) => {
const pvMap = new Map(product.variants.map((v) => [v.id, true]))
const addedVariant = variants.find((variant) => !pvMap.get(variant.id))
const inventory = await client.admin.variants.getInventory(addedVariant.id)
if (!addedVariant) {
return
}
console.log(inventory)
const inventory = await client.admin.variants.getInventory(addedVariant.id)
await Promise.all(
inventory.variant.inventory

View File

@@ -4,21 +4,20 @@ import {
ProductVariant,
VariantInventory,
} from "@medusajs/medusa"
import { useMedusa } from "medusa-react"
import { useAdminVariantsInventory } from "medusa-react"
import { useAdminVariantsInventory, useMedusa } from "medusa-react"
import { useContext } from "react"
import { useForm } from "react-hook-form"
import useEditProductActions from "../../../hooks/use-edit-product-actions"
import { removeNullish } from "../../../utils/remove-nullish"
import EditFlowVariantForm, {
EditFlowVariantFormType,
} from "../../../components/variant-inventory-form/edit-flow-variant-form"
} from "../../forms/product/variant-form/edit-flow-variant-form"
import Button from "../../fundamentals/button"
import Modal from "../../molecules/modal"
import LayeredModal, {
LayeredModalContext,
} from "../../../../../components/molecules/modal/layered-modal"
import Button from "../../../../../components/fundamentals/button"
import Modal from "../../../../../components/molecules/modal"
} from "../../molecules/modal/layered-modal"
import { createUpdatePayload } from "./edit-variants-modal/edit-variant-screen"
import useEditProductActions from "../../hooks/use-edit-product-actions"
import { removeNullish } from "../../../../../utils/remove-nullish"
type Props = {
onClose: () => void

View File

@@ -1,20 +1,20 @@
import { Product, ProductVariant } from "@medusajs/medusa"
import EditFlowVariantForm, {
EditFlowVariantFormType,
} from "../../../components/variant-form/edit-flow-variant-form"
} from "../../forms/product/variant-form/edit-flow-variant-form"
import LayeredModal, {
LayeredModalContext,
} from "../../../../../components/molecules/modal/layered-modal"
import { Product, ProductVariant } from "@medusajs/medusa"
} from "../../molecules/modal/layered-modal"
import Button from "../../../../../components/fundamentals/button"
import Modal from "../../../../../components/molecules/modal"
import { countries } from "../../../../../utils/countries"
import { useMedusa } from "medusa-react"
import { useContext } from "react"
import { useForm } from "react-hook-form"
import useEditProductActions from "../../../hooks/use-edit-product-actions"
import { countries } from "../../../utils/countries"
import Button from "../../fundamentals/button"
import Modal from "../../molecules/modal"
import { createAddPayload } from "./add-variant-modal"
import { createUpdatePayload } from "./edit-variants-modal/edit-variant-screen"
import { useContext } from "react"
import useEditProductActions from "../../hooks/use-edit-product-actions"
import { useForm } from "react-hook-form"
import { useMedusa } from "medusa-react"
type Props = {
onClose: () => void
@@ -51,7 +51,7 @@ const EditVariantModal = ({
const { client } = useMedusa()
const createStockLocationsForVariant = async (
productRes,
productRes: Product,
stock_locations: { stocked_quantity: number; location_id: string }[]
) => {
const { variants } = productRes
@@ -59,6 +59,10 @@ const EditVariantModal = ({
const pvMap = new Map(product.variants.map((v) => [v.id, true]))
const addedVariant = variants.find((variant) => !pvMap.get(variant.id))
if (!addedVariant) {
return
}
const inventory = await client.admin.variants.getInventory(addedVariant.id)
await Promise.all(

View File

@@ -3,18 +3,18 @@ import {
Product,
ProductVariant,
} from "@medusajs/medusa"
import React, { useContext, useEffect, useMemo } from "react"
import EditFlowVariantForm, {
EditFlowVariantFormType,
} from "../../../../components/variant-form/edit-flow-variant-form"
import React, { useContext, useEffect, useMemo } from "react"
} from "../../../forms/product/variant-form/edit-flow-variant-form"
import Button from "../../../../../../components/fundamentals/button"
import { LayeredModalContext } from "../../../../../../components/molecules/modal/layered-modal"
import Modal from "../../../../../../components/molecules/modal"
import { getEditVariantDefaultValues } from "../edit-variant-modal"
import useEditProductActions from "../../../hooks/use-edit-product-actions"
import { useEditVariantsModal } from "./use-edit-variants-modal"
import { useForm } from "react-hook-form"
import useEditProductActions from "../../../../hooks/use-edit-product-actions"
import Button from "../../../fundamentals/button"
import Modal from "../../../molecules/modal"
import { LayeredModalContext } from "../../../molecules/modal/layered-modal"
import { getEditVariantDefaultValues } from "../edit-variant-modal"
import { useEditVariantsModal } from "./use-edit-variants-modal"
type Props = {
variant: ProductVariant
@@ -27,23 +27,25 @@ const EditVariantScreen = ({ variant, product }: Props) => {
defaultValues: getEditVariantDefaultValues(variant, product),
})
const { reset: formReset } = form
const { pop, reset } = useContext(LayeredModalContext)
const { updatingVariant, onUpdateVariant } = useEditProductActions(product.id)
const popAndReset = () => {
form.reset(getEditVariantDefaultValues(variant, product))
formReset(getEditVariantDefaultValues(variant, product))
pop()
}
const closeAndReset = () => {
form.reset(getEditVariantDefaultValues(variant, product))
formReset(getEditVariantDefaultValues(variant, product))
reset()
onClose()
}
useEffect(() => {
form.reset(getEditVariantDefaultValues(variant, product))
}, [variant, product])
formReset(getEditVariantDefaultValues(variant, product))
}, [variant, product, formReset])
const onSubmitAndBack = form.handleSubmit((data) => {
// @ts-ignore

View File

@@ -6,12 +6,12 @@ import {
useFieldArray,
useForm,
} from "react-hook-form"
import Button from "../../../../../../components/fundamentals/button"
import Modal from "../../../../../../components/molecules/modal"
import useEditProductActions from "../../../../hooks/use-edit-product-actions"
import Button from "../../../fundamentals/button"
import Modal from "../../../molecules/modal"
import LayeredModal, {
LayeredModalContext,
} from "../../../../../../components/molecules/modal/layered-modal"
import useEditProductActions from "../../../hooks/use-edit-product-actions"
} from "../../../molecules/modal/layered-modal"
import { EditVariantsModalContext } from "./use-edit-variants-modal"
import { VariantCard } from "./variant-card"
@@ -56,6 +56,7 @@ const EditVariantsModal = ({ open, onClose, product }: Props) => {
const moveCard = useCallback((dragIndex: number, hoverIndex: number) => {
move(dragIndex, hoverIndex)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
const renderCard = useCallback(

View File

@@ -1,11 +1,11 @@
import React, { useContext } from "react"
type EditVariantsModalContext = {
type EditVariantsModalContextType = {
onClose: () => void
}
export const EditVariantsModalContext =
React.createContext<EditVariantsModalContext | null>(null)
React.createContext<EditVariantsModalContextType | null>(null)
export const useEditVariantsModal = () => {
const context = useContext(EditVariantsModalContext)

View File

@@ -5,17 +5,15 @@ import { useContext, useMemo, useRef } from "react"
import { useDrag, useDrop } from "react-dnd"
import { useFormContext } from "react-hook-form"
import { VariantItem } from "."
import Button from "../../../../../../components/fundamentals/button"
import EditIcon from "../../../../../../components/fundamentals/icons/edit-icon"
import GripIcon from "../../../../../../components/fundamentals/icons/grip-icon"
import MoreHorizontalIcon from "../../../../../../components/fundamentals/icons/more-horizontal-icon"
import Actionables, {
ActionType,
} from "../../../../../../components/molecules/actionables"
import InputField from "../../../../../../components/molecules/input"
import { LayeredModalContext } from "../../../../../../components/molecules/modal/layered-modal"
import { DragItem } from "../../../../../../types/shared"
import FormValidator from "../../../../../../utils/form-validator"
import { DragItem } from "../../../../types/shared"
import FormValidator from "../../../../utils/form-validator"
import Button from "../../../fundamentals/button"
import EditIcon from "../../../fundamentals/icons/edit-icon"
import GripIcon from "../../../fundamentals/icons/grip-icon"
import MoreHorizontalIcon from "../../../fundamentals/icons/more-horizontal-icon"
import Actionables, { ActionType } from "../../../molecules/actionables"
import InputField from "../../../molecules/input"
import { LayeredModalContext } from "../../../molecules/modal/layered-modal"
import { useEditVariantScreen } from "./edit-variant-screen"
const ItemTypes = {

View File

@@ -1,29 +1,26 @@
import OptionsProvider, { useOptionsContext } from "./options-provider"
import { Product, ProductVariant } from "@medusajs/medusa"
import OptionsProvider, { useOptionsContext } from "./options-provider"
import { ActionType } from "../../../../../components/molecules/actionables"
import { useState } from "react"
import useEditProductActions from "../../../hooks/use-edit-product-actions"
import useToggleState from "../../../hooks/use-toggle-state"
import EditIcon from "../../fundamentals/icons/edit-icon"
import GearIcon from "../../fundamentals/icons/gear-icon"
import PlusIcon from "../../fundamentals/icons/plus-icon"
import { ActionType } from "../../molecules/actionables"
import Section from "../../organisms/section"
import AddVariantModal from "./add-variant-modal"
import EditIcon from "../../../../../components/fundamentals/icons/edit-icon"
import EditVariantInventoryModal from "./edit-variant-inventory-modal"
import EditVariantModal from "./edit-variant-modal"
import EditVariantsModal from "./edit-variants-modal"
import GearIcon from "../../../../../components/fundamentals/icons/gear-icon"
import OptionsModal from "./options-modal"
import PlusIcon from "../../../../../components/fundamentals/icons/plus-icon"
import Section from "../../../../../components/organisms/section"
import VariantsTable from "./table"
import useEditProductActions from "../../hooks/use-edit-product-actions"
import { useFeatureFlag } from "../../../../../providers/feature-flag-provider"
import { useState } from "react"
import useToggleState from "../../../../../hooks/use-toggle-state"
type Props = {
product: Product
}
const VariantsSection = ({ product }: Props) => {
const { isFeatureEnabled } = useFeatureFlag()
const ProductVariantsSection = ({ product }: Props) => {
const [variantToEdit, setVariantToEdit] = useState<
| {
base: ProductVariant
@@ -202,4 +199,4 @@ const ProductOptions = () => {
)
}
export default VariantsSection
export default ProductVariantsSection

View File

@@ -6,13 +6,13 @@ import {
} from "medusa-react"
import { useEffect, useMemo } from "react"
import { useFieldArray, useForm } from "react-hook-form"
import Button from "../../../../../components/fundamentals/button"
import PlusIcon from "../../../../../components/fundamentals/icons/plus-icon"
import TrashIcon from "../../../../../components/fundamentals/icons/trash-icon"
import InputField from "../../../../../components/molecules/input"
import Modal from "../../../../../components/molecules/modal"
import useNotification from "../../../../../hooks/use-notification"
import FormValidator from "../../../../../utils/form-validator"
import useNotification from "../../../hooks/use-notification"
import FormValidator from "../../../utils/form-validator"
import Button from "../../fundamentals/button"
import PlusIcon from "../../fundamentals/icons/plus-icon"
import TrashIcon from "../../fundamentals/icons/trash-icon"
import InputField from "../../molecules/input"
import Modal from "../../molecules/modal"
import { useOptionsContext } from "./options-provider"
type Props = {

View File

@@ -8,7 +8,7 @@ type OptionsContext = {
refetch: () => void
}
const OptionsContext = createContext<OptionsContext | null>(null)
const OptionsContextType = createContext<OptionsContext | null>(null)
type Props = {
product: Product
@@ -30,14 +30,14 @@ const OptionsProvider = ({ product, children }: Props) => {
}, [products, status])
return (
<OptionsContext.Provider value={{ options, status, refetch }}>
<OptionsContextType.Provider value={{ options, status, refetch }}>
{children}
</OptionsContext.Provider>
</OptionsContextType.Provider>
)
}
export const useOptionsContext = () => {
const context = useContext(OptionsContext)
const context = useContext(OptionsContextType)
if (!context) {
throw new Error("useOptionsContext must be used within a OptionsProvider")
}

View File

@@ -1,14 +1,14 @@
import { Column, useTable } from "react-table"
import Actionables from "../../../../../components/molecules/actionables"
import BuildingsIcon from "../../../../../components/fundamentals/icons/buildings-icon"
import DuplicateIcon from "../../../../../components/fundamentals/icons/duplicate-icon"
import EditIcon from "../../../../../components/fundamentals/icons/edit-icon"
import { ProductVariant } from "@medusajs/medusa"
import Table from "../../../../../components/molecules/table"
import TrashIcon from "../../../../../components/fundamentals/icons/trash-icon"
import { useFeatureFlag } from "../../../../../providers/feature-flag-provider"
import { useMemo } from "react"
import { useFeatureFlag } from "../../../providers/feature-flag-provider"
import BuildingsIcon from "../../fundamentals/icons/buildings-icon"
import DuplicateIcon from "../../fundamentals/icons/duplicate-icon"
import EditIcon from "../../fundamentals/icons/edit-icon"
import TrashIcon from "../../fundamentals/icons/trash-icon"
import Actionables from "../../molecules/actionables"
import Table from "../../molecules/table"
type Props = {
variants: ProductVariant[]

View File

@@ -1,119 +0,0 @@
import { useAdminDeleteVariant } from "medusa-react"
import React, { useState } from "react"
import EditIcon from "../../../components/fundamentals/icons/edit-icon"
import TrashIcon from "../../../components/fundamentals/icons/trash-icon"
import Table from "../../../components/molecules/table"
import DeletePrompt from "../../../components/organisms/delete-prompt"
import { stringDisplayPrice } from "../../../utils/prices"
type DenominationTableProps = {
giftCardId: string
denominations: any[]
defaultCurrency: string
setEditDenom: (denom) => void
}
const DenominationTable: React.FC<DenominationTableProps> = ({
giftCardId,
denominations,
defaultCurrency,
setEditDenom,
}) => {
const [selectedDenom, setSelectedDenom] = useState<string | null>(null)
const deleteDenomination = useAdminDeleteVariant(giftCardId)
const getDenominationPrices = (denomination) => {
const sortHelper = (p1, p2) => {
const curr1 = p1.currency_code
const curr2 = p2.currency_code
if (curr1 < curr2) {
return -1
}
if (curr1 > curr2) {
return 1
}
return 0
}
return denomination.prices
.filter((p) => p.currency_code !== defaultCurrency) // without default
.sort(sortHelper) // sort by currency code
.map((p) =>
stringDisplayPrice({ currencyCode: p.currency_code, amount: p.amount })
) // get formatted price
.join(", ") // concatenate to single comma separated string
}
const getDenominationRow = (denomination, index) => {
let defaultPrice = denomination.prices.find(
(p) => p.currency_code === defaultCurrency
)
if (!defaultPrice) {
defaultPrice = denomination.prices[0]
}
return (
<Table.Row
key={`denomination-${index}`}
color={"inherit"}
actions={[
{
label: "Edit denomination",
onClick: () => setEditDenom(denomination),
icon: <EditIcon size={20} />,
},
{
label: "Delete denomination",
variant: "danger",
onClick: () => setSelectedDenom(denomination.id),
icon: <TrashIcon size={20} />,
},
]}
>
<Table.Cell>
{stringDisplayPrice({
currencyCode: defaultPrice.currency_code,
amount: defaultPrice.amount,
})}
</Table.Cell>
<Table.Cell>{getDenominationPrices(denomination)}</Table.Cell>
<Table.Cell></Table.Cell>
</Table.Row>
)
}
const handleDeleteDenomination = async () => {
deleteDenomination.mutateAsync(selectedDenom!)
}
return (
<div className="h-full w-full overflow-y-auto">
<Table>
<Table.Head>
<Table.HeadRow>
<Table.HeadCell>Default Value</Table.HeadCell>
<Table.HeadCell>Other Values</Table.HeadCell>
</Table.HeadRow>
</Table.Head>
<Table.Body className="text-grey-90">
{denominations?.map((d, idx) => getDenominationRow(d, idx))}
</Table.Body>
</Table>
{selectedDenom && (
<DeletePrompt
handleClose={() => setSelectedDenom(null)}
text="Are you sure you want to delete this denomination from your Medusa Store?"
heading="Delete denomination"
onDelete={() => handleDeleteDenomination()}
successText="Successfully deleted denomination"
confirmText="Yes, delete"
/>
)}
</div>
)
}
export default DenominationTable

View File

@@ -1,92 +0,0 @@
import { useAdminUpdateProduct } from "medusa-react"
import React, { useEffect, useState } from "react"
import { FormProvider, useForm, useFormContext } from "react-hook-form"
import useNotification from "../../../../hooks/use-notification"
import { handleFormError } from "../../../../utils/handle-form-error"
import { trimValues } from "../../../../utils/trim-values"
import { ManageGiftCardFormData } from "../utils/types"
import { formValuesToUpdateGiftCardMapper } from "./mappers"
type GiftCardFormProviderProps = {
giftCardId: string
giftCard: ManageGiftCardFormData
children: React.ReactNode
}
export const GiftCardFormProvider = ({
giftCardId,
giftCard,
children,
}: GiftCardFormProviderProps) => {
const [imageDirtyState, setImageDirtyState] = useState(false)
const methods = useForm<ManageGiftCardFormData>()
const resetForm = () => {
methods.reset({
handle: giftCard.handle || undefined,
description: giftCard.description || undefined,
subtitle: giftCard.subtitle || undefined,
tags: giftCard.tags || [],
title: giftCard.title || undefined,
type: giftCard.type || undefined,
thumbnail: giftCard.thumbnail || 0,
images: giftCard.images || [],
})
setImageDirtyState(false)
}
useEffect(() => {
resetForm()
}, [giftCard])
const { mutate: update } = useAdminUpdateProduct(giftCardId)
const notification = useNotification()
const onUpdate = methods.handleSubmit(async (values) => {
const cleanedValues = trimValues(values)
const payload = await formValuesToUpdateGiftCardMapper(cleanedValues)
update(payload, {
onSuccess: () => {
notification("Success", "Product updated successfully", "success")
},
})
}, handleFormError)
return (
<FormProvider {...methods}>
<GiftCardFormContext.Provider
value={{
onUpdate,
resetForm,
setImageDirtyState,
additionalDirtyState: {
images: imageDirtyState,
},
}}
>
<form>{children}</form>
</GiftCardFormContext.Provider>
</FormProvider>
)
}
const GiftCardFormContext = React.createContext<{
onUpdate: (
e?: React.BaseSyntheticEvent<object, any, any> | undefined
) => Promise<void>
resetForm: () => void
setImageDirtyState: (value: boolean) => void
additionalDirtyState: Record<string, boolean>
} | null>(null)
export const useGiftCardForm = () => {
const context = React.useContext(GiftCardFormContext)
const form = useFormContext<ManageGiftCardFormData>()
if (!context) {
throw new Error("useGiftCardForm must be a child of GiftCardFormContext")
}
return { form, ...context }
}

View File

@@ -1,59 +0,0 @@
import { AdminPostProductsProductReq, Product } from "@medusajs/medusa"
import { prepareImages } from "../../../../utils/images"
import { ManageGiftCardFormData } from "../utils/types"
export const formValuesToUpdateGiftCardMapper = async (
values: ManageGiftCardFormData
): Promise<AdminPostProductsProductReq> => {
const imagePayload = {} as Pick<
AdminPostProductsProductReq,
"images" | "thumbnail"
>
if (values.images?.length) {
const images = await prepareImages(values.images)
imagePayload.images = images.map((img) => img.url)
}
if (values.thumbnail) {
imagePayload.thumbnail = imagePayload.images?.length
? imagePayload.images[values.thumbnail]
: undefined
}
return {
title: values.title,
subtitle: values.subtitle ?? undefined,
description: values.description ?? undefined,
handle: values.handle ?? undefined,
type: values.type
? { id: values.type?.value, value: values.type.label }
: undefined,
tags: values.tags?.map((tag) => ({ value: tag })) ?? [],
// @ts-ignore
sales_channels: undefined,
...imagePayload,
}
}
export const giftCardToFormValuesMapper = (
giftCard: Product
): ManageGiftCardFormData => {
let thumbnail = giftCard.images?.length
? giftCard.images.findIndex((img) => img.url === giftCard.thumbnail)
: 0
thumbnail = thumbnail === -1 ? 0 : thumbnail
return {
title: giftCard.title,
subtitle: giftCard.subtitle,
description: giftCard.description,
handle: giftCard.handle || undefined,
type: giftCard.type
? { label: giftCard.type.value, value: giftCard.type.id }
: null,
tags: giftCard.tags.map((t) => t.value),
images: giftCard.images.map((img) => ({ url: img.url })),
thumbnail,
}
}

View File

@@ -1,21 +1,20 @@
import { Product } from "@medusajs/medusa"
import { useAdminProducts } from "medusa-react"
import { useEffect, useState } from "react"
import toast from "react-hot-toast"
import { useNavigate } from "react-router-dom"
import BackButton from "../../../components/atoms/back-button"
import Spinner from "../../../components/atoms/spinner"
import Toaster from "../../../components/declarative-toaster"
import FormToasterContainer from "../../../components/molecules/form-toaster"
import { checkForDirtyState } from "../../../utils/form-helpers"
import {
GiftCardFormProvider,
useGiftCardForm,
} from "./form/gift-card-form-context"
import { giftCardToFormValuesMapper } from "./form/mappers"
import Denominations from "./sections/denominations"
import Images from "./sections/images"
import Information from "./sections/information"
import GiftCardDenominationsSection from "../../../components/organisms/gift-card-denominations-section"
import ProductAttributesSection from "../../../components/organisms/product-attributes-section"
import ProductGeneralSection from "../../../components/organisms/product-general-section"
import ProductMediaSection from "../../../components/organisms/product-media-section"
import ProductRawSection from "../../../components/organisms/product-raw-section"
import ProductThumbnailSection from "../../../components/organisms/product-thumbnail-section"
import { getErrorStatus } from "../../../utils/get-error-status"
const ManageGiftCard = () => {
const { products } = useAdminProducts(
const Manage = () => {
const navigate = useNavigate()
const { products, error } = useAdminProducts(
{
is_giftcard: true,
},
@@ -24,7 +23,7 @@ const ManageGiftCard = () => {
}
)
const giftCard = products?.[0]
const giftCard = products?.[0] as Product | undefined
if (!giftCard) {
return (
@@ -34,72 +33,42 @@ const ManageGiftCard = () => {
)
}
if (error) {
const errorStatus = getErrorStatus(error)
if (errorStatus) {
// If the product is not found, redirect to the 404 page
if (errorStatus.status === 404) {
navigate("/404")
return null
}
}
// Let the error boundary handle the error
throw error
}
return (
<GiftCardFormProvider
giftCard={giftCardToFormValuesMapper(giftCard)}
giftCardId={giftCard.id}
>
<div className="gap-y-large pb-xlarge flex flex-col">
<Information giftCard={giftCard} />
<Denominations giftCard={giftCard} />
<Images />
<div className="pb-5xlarge">
<BackButton
path="/a/gift-cards"
label="Back to Gift Cards"
className="mb-xsmall"
/>
<div className="gap-x-base grid grid-cols-12">
<div className="gap-y-xsmall col-span-8 flex flex-col">
<ProductGeneralSection product={giftCard} />
<GiftCardDenominationsSection giftCard={giftCard} />
<ProductAttributesSection product={giftCard} />
<ProductRawSection product={giftCard} />
</div>
<div className="gap-y-xsmall col-span-4 flex flex-col">
<ProductThumbnailSection product={giftCard} />
<ProductMediaSection product={giftCard} />
</div>
</div>
<UpdateNotification />
</GiftCardFormProvider>
</div>
)
}
const TOAST_ID = "edit-gc-dirty"
const UpdateNotification = ({ isLoading = false }) => {
const {
form: { formState },
onUpdate,
resetForm,
additionalDirtyState,
} = useGiftCardForm()
const [visible, setVisible] = useState(false)
const [blocking, setBlocking] = useState(true)
useEffect(() => {
const timeout = setTimeout(setBlocking, 300, false)
return () => clearTimeout(timeout)
}, [])
const isDirty = checkForDirtyState(
formState.dirtyFields,
additionalDirtyState
)
useEffect(() => {
if (!blocking) {
setVisible(isDirty)
}
return () => {
toast.dismiss(TOAST_ID)
}
}, [isDirty])
return (
<Toaster
visible={visible}
duration={Infinity}
id={TOAST_ID}
position="bottom-right"
>
<FormToasterContainer isLoading={isLoading}>
<FormToasterContainer.Actions>
<FormToasterContainer.ActionButton onClick={onUpdate}>
Save
</FormToasterContainer.ActionButton>
<FormToasterContainer.DiscardButton onClick={resetForm}>
Discard
</FormToasterContainer.DiscardButton>
</FormToasterContainer.Actions>
</FormToasterContainer>
</Toaster>
)
}
export default ManageGiftCard
export default Manage

View File

@@ -1,126 +0,0 @@
import { Product, ProductVariant } from "@medusajs/medusa"
import {
useAdminDeleteVariant,
useAdminStore,
useAdminUpdateVariant,
} from "medusa-react"
import React, { useState } from "react"
import PlusIcon from "../../../../components/fundamentals/icons/plus-icon"
import AddDenominationModal from "../../../../components/organisms/add-denomination-modal"
import BodyCard from "../../../../components/organisms/body-card"
import EditDenominationsModal from "../../../../components/organisms/edit-denominations-modal"
import useNotification from "../../../../hooks/use-notification"
import { getErrorMessage } from "../../../../utils/error-messages"
import DenominationTable from "../denomination-table"
type DenominationsProps = {
giftCard: Product
}
const Denominations: React.FC<DenominationsProps> = ({ giftCard }) => {
const [editDenom, setEditDenom] = useState<Omit<
ProductVariant,
"beforeInsert"
> | null>(null)
const [addDenom, setAddDenom] = useState(false)
const { store } = useAdminStore()
const updateGiftCardVariant = useAdminUpdateVariant(giftCard.id)
const deleteGiftCardVariant = useAdminDeleteVariant(giftCard.id)
const notification = useNotification()
const currencyCodes =
store?.currencies
.filter((currency) => currency.code !== store.default_currency_code)
.map((currency) => currency.code) || []
const submitDenomations = (denoms) => {
if (!denoms.length) {
// if a update would result in the variant having 0 prices, then we delete it instead
deleteGiftCardVariant.mutate(editDenom!.id, {
onSuccess: () => {
notification(
"Success",
"Successfully updated denominations",
"success"
)
setEditDenom(null)
},
onError: (err) => notification("Error", getErrorMessage(err), "error"),
})
return
}
updateGiftCardVariant.mutate(
{
variant_id: editDenom!.id,
prices: denoms.map(({ amount, currency_code }) => ({
amount,
currency_code,
})),
title: editDenom!.title,
inventory_quantity: editDenom!.inventory_quantity,
options: editDenom!.options.map((opt) => ({
option_id: opt.option_id,
value: opt.value,
})),
},
{
onSuccess: () => {
notification(
"Success",
"Successfully updated denominations",
"success"
)
setEditDenom(null)
},
onError: (err) => notification("Error", getErrorMessage(err), "error"),
}
)
}
return (
<>
<BodyCard
title="Denominations"
subtitle="Manage your denominations"
className={"h-auto w-full"}
actionables={[
{
label: "Add Denomination",
onClick: () => setAddDenom(true),
icon: <PlusIcon size={20} />,
},
]}
>
<DenominationTable
giftCardId={giftCard.id}
denominations={giftCard.variants || []}
defaultCurrency={store?.default_currency_code || ""}
setEditDenom={setEditDenom}
/>
</BodyCard>
{editDenom && (
<EditDenominationsModal
currencyCodes={store?.currencies.map((c) => c.code)}
onSubmit={submitDenomations}
defaultDenominations={editDenom.prices.map((p) => ({
amount: p.amount,
currency_code: p.currency_code,
id: p.id,
}))}
handleClose={() => setEditDenom(null)}
/>
)}
{addDenom && (
<AddDenominationModal
giftCard={giftCard}
handleClose={() => setAddDenom(false)}
storeCurrency={store?.default_currency_code!}
currencyCodes={currencyCodes}
/>
)}
</>
)
}
export default Denominations

View File

@@ -1,104 +0,0 @@
import { Controller, useFieldArray } from "react-hook-form"
import FileUploadField from "../../../../components/atoms/file-upload-field"
import BodyCard from "../../../../components/organisms/body-card"
import RadioGroup from "../../../../components/organisms/radio-group"
import ImageTable, {
ImageTableDataType,
} from "../../../../components/templates/image-table"
import { nestedForm } from "../../../../utils/nested-form"
import { useGiftCardForm } from "../form/gift-card-form-context"
const Images = () => {
const { form, setImageDirtyState } = useGiftCardForm()
const { fields, append, remove } = useFieldArray({
control: form.control,
name: "images",
})
const handleRemove = (index: number) => {
setImageDirtyState(true)
remove(index)
}
const handleFilesChosen = (files: File[]) => {
if (files.length) {
const toAppend = files.map((file) => ({
url: URL.createObjectURL(file),
name: file.name,
size: file.size,
nativeFile: file,
}))
append(toAppend)
}
}
return (
<BodyCard title="Images" subtitle="Add up to 10 images to your product">
<div className="mt-base">
<Controller
name="thumbnail"
control={form.control}
render={({ field: { value, onChange } }) => {
return (
<RadioGroup.Root
value={value ? `${value}` : undefined}
onValueChange={(value) => {
onChange(parseInt(value))
}}
>
<ImageTable
data={fields as ImageTableDataType[]}
form={nestedForm(form, "images")}
onDelete={handleRemove}
/>
{fields.map((field, index) => {
return (
<div key={field.id} className="flex items-center">
<input
className="hidden"
{...form.register(`images.${index}.url`)}
defaultValue={field.url}
/>
{field.nativeFile && (
<>
<input
className="hidden"
{...form.register(`images.${index}.name`)}
defaultValue={field.name}
/>
<input
className="hidden"
{...form.register(`images.${index}.size`)}
defaultValue={field.size}
/>
<Controller
name={`images.${index}.nativeFile`}
control={form.control}
defaultValue={field.nativeFile}
render={() => <></>}
/>
</>
)}
</div>
)
})}
</RadioGroup.Root>
)
}}
/>
</div>
<div className="mt-2xlarge">
<FileUploadField
onFileChosen={handleFilesChosen}
placeholder="1200 x 1600 (3:4) recommended, up to 10MB each"
filetypes={["image/gif", "image/jpeg", "image/png", "image/webp"]}
className="py-large"
/>
</div>
</BodyCard>
)
}
export default Images

View File

@@ -1,227 +0,0 @@
import { Product } from "@medusajs/medusa"
import {
useAdminDeleteProduct,
useAdminProductTypes,
useAdminUpdateProduct,
} from "medusa-react"
import React from "react"
import { Controller } from "react-hook-form"
import { useNavigate } from "react-router-dom"
import TrashIcon from "../../../../components/fundamentals/icons/trash-icon"
import UnpublishIcon from "../../../../components/fundamentals/icons/unpublish-icon"
import Input from "../../../../components/molecules/input"
import Select from "../../../../components/molecules/select"
import StatusSelector from "../../../../components/molecules/status-selector"
import TagInput from "../../../../components/molecules/tag-input"
import TextArea from "../../../../components/molecules/textarea"
import BodyCard from "../../../../components/organisms/body-card"
import DetailsCollapsible from "../../../../components/organisms/details-collapsible"
import useNotification from "../../../../hooks/use-notification"
import { getErrorMessage } from "../../../../utils/error-messages"
import FormValidator from "../../../../utils/form-validator"
import { useGiftCardForm } from "../form/gift-card-form-context"
type InformationProps = {
giftCard: Omit<Product, "beforeInsert">
}
const Information: React.FC<InformationProps> = ({ giftCard }) => {
const {
form: {
register,
setValue,
control,
formState: { errors },
},
} = useGiftCardForm()
const navigate = useNavigate()
const notification = useNotification()
const { product_types } = useAdminProductTypes(undefined, {
cacheTime: 0,
})
const typeOptions =
product_types?.map((tag) => ({ label: tag.value, value: tag.id })) || []
const updateGiftCard = useAdminUpdateProduct(giftCard.id)
const deleteGiftCard = useAdminDeleteProduct(giftCard.id)
const setNewType = (value: string) => {
const newType = {
label: value,
value,
}
typeOptions.push(newType)
setValue("type", newType)
return newType
}
const onUpdate = () => {
updateGiftCard.mutate(
{
// @ts-ignore
status: giftCard.status === "draft" ? "published" : "draft",
},
{
onSuccess: () => {
notification("Success", "Gift card updated successfully", "success")
},
onError: (error) => {
notification("Error", getErrorMessage(error), "error")
},
}
)
}
const onDelete = () => {
deleteGiftCard.mutate(undefined, {
onSuccess: () => {
navigate("/a/gift-cards")
notification("Success", "Gift card updated successfully", "success")
},
onError: (error) => {
notification("Error", getErrorMessage(error), "error")
},
})
}
return (
<BodyCard
title="Product information"
subtitle="Manage the settings for your Gift Card"
className={"h-auto w-full"}
status={
<GiftCardStatusSelector
currentStatus={giftCard.status}
onUpdate={onUpdate}
/>
}
actionables={[
{
label:
giftCard?.status !== "published"
? "Publish Gift Card"
: "Unpublish Gift Card",
onClick: onUpdate,
icon: <UnpublishIcon size="16" />,
},
{
label: "Delete Gift Card",
onClick: onDelete,
variant: "danger",
icon: <TrashIcon size="16" />,
},
]}
>
<div className="flex flex-col space-y-6">
<div className="gap-large grid grid-cols-2">
<Input
label="Name"
placeholder="Add name"
required
defaultValue={giftCard?.title}
{...register("title", {
required: FormValidator.required("Name"),
pattern: FormValidator.whiteSpaceRule("Name"),
minLength: FormValidator.minOneCharRule("Name"),
})}
errors={errors}
/>
<Input
label="Subtitle"
placeholder="Add a subtitle"
{...register("subtitle", {
pattern: FormValidator.whiteSpaceRule("Subtitle"),
minLength: FormValidator.minOneCharRule("Subtitle"),
})}
errors={errors}
/>
<TextArea
label="Description"
placeholder="Add a description"
{...register("description", {
pattern: FormValidator.whiteSpaceRule("Description"),
minLength: FormValidator.minOneCharRule("Description"),
})}
errors={errors}
/>
</div>
<DetailsCollapsible
triggerProps={{ className: "ml-2" }}
contentProps={{
forceMount: true,
}}
>
<div className="gap-large grid grid-cols-2">
<Input
label="Handle"
placeholder="Product handle"
{...register("handle", {
pattern: FormValidator.whiteSpaceRule("Handle"),
minLength: FormValidator.minOneCharRule("Handle"),
})}
tooltipContent="URL of the product"
errors={errors}
/>
<Controller
control={control}
name="type"
render={({ field: { value, onChange } }) => {
return (
<Select
label="Type"
placeholder="Select type..."
options={typeOptions}
onChange={onChange}
value={value}
isCreatable
onCreateOption={(value) => {
return setNewType(value)
}}
clearSelected
/>
)
}}
/>
<Controller
name="tags"
render={({ field: { onChange, value } }) => {
return (
<TagInput
label="Tags (separated by comma)"
className="w-full"
placeholder="Spring, Summer..."
onChange={onChange}
values={value || []}
/>
)
}}
control={control}
/>
</div>
</DetailsCollapsible>
</div>
</BodyCard>
)
}
const GiftCardStatusSelector = ({
currentStatus,
onUpdate,
}: {
currentStatus: "draft" | "proposed" | "published" | "rejected"
onUpdate: () => void
}) => {
return (
<StatusSelector
activeState="Published"
draftState="Draft"
isDraft={currentStatus === "draft"}
onChange={onUpdate}
/>
)
}
export default Information

View File

@@ -1,12 +0,0 @@
import { FormImage, Option } from "../../../../types/shared"
export type ManageGiftCardFormData = {
title: string
handle?: string
subtitle?: string | null
description?: string | null
type: Option | null
tags?: string[]
images: FormImage[]
thumbnail: number
}

View File

@@ -1,11 +1,11 @@
import { SalesChannel, StockLocationExpandedDTO } from "@medusajs/medusa"
import {
useAdminAddLocationToSalesChannel,
useAdminRemoveLocationFromSalesChannel,
useAdminRemoveLocationFromSalesChannel
} from "medusa-react"
import SalesChannelsModal from "../../../../../components/forms/product/sales-channels-modal"
import Button from "../../../../../components/fundamentals/button"
import SalesChannelsModal from "../../../../products/components/sales-channels-modal"
import useToggleState from "../../../../../hooks/use-toggle-state"
const EditSalesChannels = ({

View File

@@ -1,10 +1,10 @@
import { SalesChannel, StockLocationExpandedDTO } from "@medusajs/medusa"
import { useFieldArray } from "react-hook-form"
import SalesChannelsModal from "../../../../../components/forms/product/sales-channels-modal"
import Button from "../../../../../components/fundamentals/button"
import SalesChannelsList from "../../../../../components/molecules/sales-channels-list"
import useToggleState from "../../../../../hooks/use-toggle-state"
import { NestedForm } from "../../../../../utils/nested-form"
import SalesChannelsModal from "../../../../products/components/sales-channels-modal"
import { AddSalesChannelsFormType } from "../../../../products/new/add-sales-channels"
const SalesChannelsForm = ({

View File

@@ -4,12 +4,12 @@ import clsx from "clsx"
import { useAdminShippingOptions } from "medusa-react"
import { useMemo } from "react"
import { Controller, useWatch } from "react-hook-form"
import PriceFormInput from "../../../../components/forms/general/prices-form/price-form-input"
import Button from "../../../../components/fundamentals/button"
import TrashIcon from "../../../../components/fundamentals/icons/trash-icon"
import { NextSelect } from "../../../../components/molecules/select/next-select"
import { NestedForm } from "../../../../utils/nested-form"
import { formatAmountWithSymbol } from "../../../../utils/prices"
import PriceFormInput from "../../../products/components/prices-form/price-form-input"
export type ShippingFormType = {
option: {

View File

@@ -2,13 +2,13 @@ import { useAdminProduct } from "medusa-react"
import { useNavigate, useParams } from "react-router-dom"
import BackButton from "../../../components/atoms/back-button"
import Spinner from "../../../components/atoms/spinner"
import ProductAttributesSection from "../../../components/organisms/product-attributes-section"
import ProductGeneralSection from "../../../components/organisms/product-general-section"
import ProductMediaSection from "../../../components/organisms/product-media-section"
import ProductRawSection from "../../../components/organisms/product-raw-section"
import ProductThumbnailSection from "../../../components/organisms/product-thumbnail-section"
import ProductVariantsSection from "../../../components/organisms/product-variants-section"
import { getErrorStatus } from "../../../utils/get-error-status"
import AttributesSection from "./sections/attributes"
import GeneralSection from "./sections/general"
import MediaSection from "./sections/media"
import RawSection from "./sections/raw"
import ThumbnailSection from "./sections/thumbnail"
import VariantsSection from "./sections/variants"
const Edit = () => {
const { id } = useParams()
@@ -17,13 +17,9 @@ const Edit = () => {
const { product, status, error } = useAdminProduct(id || "")
if (error) {
let message = "An unknown error occurred"
const errorStatus = getErrorStatus(error)
if (errorStatus) {
message = errorStatus.message
// If the product is not found, redirect to the 404 page
if (errorStatus.status === 404) {
navigate("/404")
@@ -53,14 +49,14 @@ const Edit = () => {
/>
<div className="gap-x-base grid grid-cols-12">
<div className="gap-y-xsmall col-span-8 flex flex-col">
<GeneralSection product={product} />
<VariantsSection product={product} />
<AttributesSection product={product} />
<RawSection product={product} />
<ProductGeneralSection product={product} />
<ProductVariantsSection product={product} />
<ProductAttributesSection product={product} />
<ProductRawSection product={product} />
</div>
<div className="gap-y-xsmall col-span-4 flex flex-col">
<ThumbnailSection product={product} />
<MediaSection product={product} />
<ProductThumbnailSection product={product} />
<ProductMediaSection product={product} />
</div>
</div>
</div>

View File

@@ -4,12 +4,12 @@ import { useAdminStore } from "medusa-react"
import { useEffect, useState } from "react"
import { useFieldArray } from "react-hook-form"
import Switch from "../../../components/atoms/switch"
import SalesChannelsModal from "../../../components/forms/product/sales-channels-modal"
import Button from "../../../components/fundamentals/button"
import ChannelsIcon from "../../../components/fundamentals/icons/channels-icon"
import SalesChannelsDisplay from "../../../components/molecules/sales-channels-display"
import useToggleState from "../../../hooks/use-toggle-state"
import { NestedForm } from "../../../utils/nested-form"
import SalesChannelsModal from "../components/sales-channels-modal"
export type AddSalesChannelsFormType = {
channels: SalesChannel[]

View File

@@ -2,6 +2,13 @@ import clsx from "clsx"
import { useCallback, useContext, useEffect, useMemo } from "react"
import { Controller, useFieldArray, useForm, useWatch } from "react-hook-form"
import { v4 as uuidv4 } from "uuid"
import { CustomsFormType } from "../../../../components/forms/product/customs-form"
import { DimensionsFormType } from "../../../../components/forms/product/dimensions-form"
import CreateFlowVariantForm, {
CreateFlowVariantFormType
} from "../../../../components/forms/product/variant-form/create-flow-variant-form"
import { VariantOptionType } from "../../../../components/forms/product/variant-form/variant-select-options-form"
import useCheckOptions from "../../../../components/forms/product/variant-form/variant-select-options-form/hooks"
import Button from "../../../../components/fundamentals/button"
import PlusIcon from "../../../../components/fundamentals/icons/plus-icon"
import TrashIcon from "../../../../components/fundamentals/icons/trash-icon"
@@ -9,19 +16,12 @@ import IconTooltip from "../../../../components/molecules/icon-tooltip"
import InputField from "../../../../components/molecules/input"
import Modal from "../../../../components/molecules/modal"
import LayeredModal, {
LayeredModalContext,
LayeredModalContext
} from "../../../../components/molecules/modal/layered-modal"
import TagInput from "../../../../components/molecules/tag-input"
import { useDebounce } from "../../../../hooks/use-debounce"
import useToggleState from "../../../../hooks/use-toggle-state"
import { NestedForm } from "../../../../utils/nested-form"
import { CustomsFormType } from "../../components/customs-form"
import { DimensionsFormType } from "../../components/dimensions-form"
import CreateFlowVariantForm, {
CreateFlowVariantFormType,
} from "../../components/variant-form/create-flow-variant-form"
import { VariantOptionType } from "../../components/variant-form/variant-select-options-form"
import useCheckOptions from "../../components/variant-form/variant-select-options-form/hooks"
import NewVariant from "./new-variant"
type ProductOptionType = {

View File

@@ -4,6 +4,12 @@ import { useContext, useEffect, useRef } from "react"
import { useDrag, useDrop } from "react-dnd"
import { useForm } from "react-hook-form"
import Tooltip from "../../../../../components/atoms/tooltip"
import { CustomsFormType } from "../../../../../components/forms/product/customs-form"
import { DimensionsFormType } from "../../../../../components/forms/product/dimensions-form"
import CreateFlowVariantForm, {
CreateFlowVariantFormType
} from "../../../../../components/forms/product/variant-form/create-flow-variant-form"
import { VariantOptionValueType } from "../../../../../components/forms/product/variant-form/variant-select-options-form"
import Button from "../../../../../components/fundamentals/button"
import CheckCircleFillIcon from "../../../../../components/fundamentals/icons/check-circle-fill-icon"
import EditIcon from "../../../../../components/fundamentals/icons/edit-icon"
@@ -14,17 +20,11 @@ import Actionables from "../../../../../components/molecules/actionables"
import IconTooltip from "../../../../../components/molecules/icon-tooltip"
import Modal from "../../../../../components/molecules/modal"
import LayeredModal, {
LayeredModalContext,
LayeredModalContext
} from "../../../../../components/molecules/modal/layered-modal"
import useImperativeDialog from "../../../../../hooks/use-imperative-dialog"
import useToggleState from "../../../../../hooks/use-toggle-state"
import { DragItem } from "../../../../../types/shared"
import { CustomsFormType } from "../../../components/customs-form"
import { DimensionsFormType } from "../../../components/dimensions-form"
import CreateFlowVariantForm, {
CreateFlowVariantFormType,
} from "../../../components/variant-form/create-flow-variant-form"
import { VariantOptionValueType } from "../../../components/variant-form/variant-select-options-form"
const ItemTypes = {
CARD: "card",

View File

@@ -1,36 +1,36 @@
import AddSalesChannelsForm, {
AddSalesChannelsFormType,
} from "./add-sales-channels"
import AddVariantsForm, { AddVariantsFormType } from "./add-variants"
import { AdminPostProductsReq, ProductVariant } from "@medusajs/medusa"
import CustomsForm, { CustomsFormType } from "../components/customs-form"
import DimensionsForm, {
DimensionsFormType,
} from "../components/dimensions-form"
import DiscountableForm, {
DiscountableFormType,
} from "../components/discountable-form"
import { FormImage, ProductStatus } from "../../../types/shared"
import GeneralForm, { GeneralFormType } from "../components/general-form"
import MediaForm, { MediaFormType } from "../components/media-form"
import OrganizeForm, { OrganizeFormType } from "../components/organize-form"
import ThumbnailForm, { ThumbnailFormType } from "../components/thumbnail-form"
import { useAdminCreateProduct, useMedusa } from "medusa-react"
import { useForm, useWatch } from "react-hook-form"
import CustomsForm, { CustomsFormType } from "../../../components/forms/product/customs-form"
import DimensionsForm, {
DimensionsFormType
} from "../../../components/forms/product/dimensions-form"
import DiscountableForm, {
DiscountableFormType
} from "../../../components/forms/product/discountable-form"
import GeneralForm, { GeneralFormType } from "../../../components/forms/product/general-form"
import MediaForm, { MediaFormType } from "../../../components/forms/product/media-form"
import OrganizeForm, { OrganizeFormType } from "../../../components/forms/product/organize-form"
import ThumbnailForm, { ThumbnailFormType } from "../../../components/forms/product/thumbnail-form"
import { FormImage, ProductStatus } from "../../../types/shared"
import AddSalesChannelsForm, {
AddSalesChannelsFormType
} from "./add-sales-channels"
import AddVariantsForm, { AddVariantsFormType } from "./add-variants"
import Accordion from "../../../components/organisms/accordion"
import Button from "../../../components/fundamentals/button"
import CrossIcon from "../../../components/fundamentals/icons/cross-icon"
import FeatureToggle from "../../../components/fundamentals/feature-toggle"
import FocusModal from "../../../components/molecules/modal/focus-modal"
import { PricesFormType } from "../components/prices-form"
import { getErrorMessage } from "../../../utils/error-messages"
import { nestedForm } from "../../../utils/nested-form"
import { prepareImages } from "../../../utils/images"
import { useEffect } from "react"
import { useFeatureFlag } from "../../../providers/feature-flag-provider"
import { useNavigate } from "react-router-dom"
import { PricesFormType } from "../../../components/forms/general/prices-form"
import Button from "../../../components/fundamentals/button"
import FeatureToggle from "../../../components/fundamentals/feature-toggle"
import CrossIcon from "../../../components/fundamentals/icons/cross-icon"
import FocusModal from "../../../components/molecules/modal/focus-modal"
import Accordion from "../../../components/organisms/accordion"
import useNotification from "../../../hooks/use-notification"
import { useFeatureFlag } from "../../../providers/feature-flag-provider"
import { getErrorMessage } from "../../../utils/error-messages"
import { prepareImages } from "../../../utils/images"
import { nestedForm } from "../../../utils/nested-form"
type NewProductForm = {
general: GeneralFormType
@@ -357,14 +357,14 @@ const createPayload = (
mid_code: data.customs.mid_code || undefined,
type: data.organize.type
? {
value: data.organize.type.label,
id: data.organize.type.value,
}
value: data.organize.type.label,
id: data.organize.type.value,
}
: undefined,
tags: data.organize.tags
? data.organize.tags.map((t) => ({
value: t,
}))
value: t,
}))
: undefined,
categories: data.organize.categories?.length
? data.organize.categories.map((id) => ({ id }))

View File

@@ -2,12 +2,12 @@ import { Region } from "@medusajs/medusa"
import { Controller, UseFormReturn } from "react-hook-form"
import IncludesTaxTooltip from "../../../../../components/atoms/includes-tax-tooltip"
import Switch from "../../../../../components/atoms/switch"
import PriceFormInput from "../../../../../components/forms/general/prices-form/price-form-input"
import InputHeader from "../../../../../components/fundamentals/input-header"
import InputField from "../../../../../components/molecules/input"
import { NextSelect } from "../../../../../components/molecules/select/next-select"
import { Option, ShippingOptionPriceType } from "../../../../../types/shared"
import FormValidator from "../../../../../utils/form-validator"
import PriceFormInput from "../../../../products/components/prices-form/price-form-input"
import { useShippingOptionFormData } from "./use-shipping-option-form-data"
type Requirement = {

View File

@@ -2,6 +2,7 @@ import {
AdminPostProductsProductReq,
AdminPostProductsProductVariantsReq,
AdminPostProductsProductVariantsVariantReq,
Product,
} from "@medusajs/medusa"
import {
useAdminCreateVariant,
@@ -12,11 +13,11 @@ import {
useAdminUpdateVariant,
} from "medusa-react"
import { getErrorMessage } from "../../../../utils/error-messages"
import { removeNullish } from "../../../../utils/remove-nullish"
import useImperativeDialog from "../../../../hooks/use-imperative-dialog"
import { useNavigate } from "react-router-dom"
import useNotification from "../../../../hooks/use-notification"
import { getErrorMessage } from "../utils/error-messages"
import { removeNullish } from "../utils/remove-nullish"
import useImperativeDialog from "./use-imperative-dialog"
import useNotification from "./use-notification"
const useEditProductActions = (productId: string) => {
const dialog = useImperativeDialog()
@@ -49,7 +50,7 @@ const useEditProductActions = (productId: string) => {
const onAddVariant = (
payload: AdminPostProductsProductVariantsReq,
onSuccess: (variantRes) => void,
onSuccess: (variantRes: Product) => void,
successMessage = "Variant was created successfully"
) => {
addVariant.mutate(payload, {