fix(medusa, admin-ui, medusa-react): Gift Card update fixes and admin UI cleanup (#3676)

* fix gc domain issues

* add changeset

* update changeset

* more minor fixes, remove breadcrumb

* more cleanup

* address feedback
This commit is contained in:
Kasper Fabricius Kristensen authored and GitHub committed 2023-04-02 19:04:32 +02:00
1 parent a5ad6c0542
commit 788ddc0f43
51 files changed
+997 -709

No files matched your search

+7
View File
@@ -0,0 +1,7 @@
---
"medusa-react": patch
"@medusajs/admin-ui": patch
"@medusajs/medusa": patch
---
fix(admin-ui, medusa-react, medusa): Minor fixes to GC domain in admin UI. Also fixes GC update payload type in medusa-react and medusa.
@@ -60,6 +60,7 @@ const DatePicker: React.FC<DateTimePickerProps> = ({
"shadow-input border-violet-60": isOpen, "shadow-input border-violet-60": isOpen,
"border-grey-20": !isOpen, "border-grey-20": !isOpen,
})} })}
type="button"
> >
<InputContainer className="shadown-none border-0 focus-within:shadow-none"> <InputContainer className="shadown-none border-0 focus-within:shadow-none">
<div className="text-grey-50 flex w-full justify-between pr-0.5"> <div className="text-grey-50 flex w-full justify-between pr-0.5">
@@ -53,6 +53,7 @@ const TimePicker: React.FC<DateTimePickerProps> = ({
"shadow-input border-violet-60": isOpen, "shadow-input border-violet-60": isOpen,
"border-grey-20": !isOpen, "border-grey-20": !isOpen,
})} })}
type="button"
> >
<InputContainer className="shadown-none border-0 focus-within:shadow-none"> <InputContainer className="shadown-none border-0 focus-within:shadow-none">
<div className="text-grey-50 flex w-full justify-between pr-0.5"> <div className="text-grey-50 flex w-full justify-between pr-0.5">
@@ -34,7 +34,11 @@ const NumberScroller: React.FC<NumberScrollerProps> = ({
} }
)} )}
> >
<button onClick={() => onSelect(n)} className="h-full w-full py-2"> <button
onClick={() => onSelect(n)}
className="h-full w-full py-2"
type="button"
>
{n.toLocaleString("en-US", { minimumIntegerDigits: 2 })} {n.toLocaleString("en-US", { minimumIntegerDigits: 2 })}
</button> </button>
</div> </div>
@@ -0,0 +1,9 @@
/**
* Spacer that ensures a consistent bottom margin on all pages
* Temporary fix to BodyCard and main layout not having a consistent bottom margin
*/
const Spacer = () => {
return <div className="h-xlarge w-full" />
}
export default Spacer
@@ -0,0 +1,67 @@
import { Controller } from "react-hook-form"
import FormValidator from "../../../../utils/form-validator"
import { NestedForm } from "../../../../utils/nested-form"
import { formatAmountWithSymbol } from "../../../../utils/prices"
import PriceFormInput from "../../general/prices-form/price-form-input"
export type GiftCardBalanceFormType = {
amount: number
}
export type GiftCardBalanceFormProps = {
form: NestedForm<GiftCardBalanceFormType>
currencyCode: string
originalAmount?: number
}
const GiftCardBalanceForm = ({
form,
currencyCode,
originalAmount,
}: GiftCardBalanceFormProps) => {
const {
control,
path,
formState: { errors },
} = form
return (
<Controller
name={path("amount")}
rules={{
required: FormValidator.required("Balance"),
min: {
value: 0,
message: "Balance must be greater than 0",
},
max: originalAmount
? {
value: originalAmount,
message: `The updated balance cannot exceed the original value of ${formatAmountWithSymbol(
{
amount: originalAmount,
currency: currencyCode,
}
)}`,
}
: undefined,
}}
control={control}
render={({ field: { value, onChange, name } }) => {
return (
<PriceFormInput
label="Amount"
currencyCode={currencyCode}
onChange={onChange}
amount={value}
name={name}
errors={errors}
required
/>
)
}}
/>
)
}
export default GiftCardBalanceForm
@@ -0,0 +1,57 @@
import { Controller } from "react-hook-form"
import { NestedForm } from "../../../../utils/nested-form"
import DatePicker from "../../../atoms/date-picker/date-picker"
import TimePicker from "../../../atoms/date-picker/time-picker"
import SwitchableItem from "../../../molecules/switchable-item"
export type GiftCardEndsAtFormType = {
ends_at: Date | null
}
type GiftCardEndsAtFormProps = {
form: NestedForm<GiftCardEndsAtFormType>
}
const GiftCardEndsAtForm = ({ form }: GiftCardEndsAtFormProps) => {
const { control, path } = form
return (
<Controller
name={path("ends_at")}
control={control}
render={({ field: { value, onChange } }) => {
return (
<SwitchableItem
open={!!value}
onSwitch={() => {
if (value) {
onChange(null)
} else {
onChange(
new Date(new Date().getTime() + 7 * 24 * 60 * 60 * 1000)
)
}
}}
title="Gift Card has an expiry date?"
description="Schedule the Gift Card to deactivate in the future."
>
<div className="gap-x-xsmall flex items-center">
<DatePicker
date={value!}
label="Expiry date"
onSubmitDate={onChange}
/>
<TimePicker
label="Expiry time"
date={value!}
onSubmitDate={onChange}
/>
</div>
</SwitchableItem>
)
}}
/>
)
}
export default GiftCardEndsAtForm
@@ -0,0 +1,43 @@
import FormValidator from "../../../../utils/form-validator"
import { NestedForm } from "../../../../utils/nested-form"
import InputField from "../../../molecules/input"
import TextArea from "../../../molecules/textarea"
export type GiftCardReceiverFormType = {
email: string
message?: string
}
type GiftCardReceiverFormProps = {
form: NestedForm<GiftCardReceiverFormType>
}
const GiftCardReceiverForm = ({ form }: GiftCardReceiverFormProps) => {
const {
register,
path,
formState: { errors },
} = form
return (
<div className="gap-y-large flex flex-col">
<InputField
label="Email"
{...register(path("email"), {
required: FormValidator.required("Email"),
pattern: FormValidator.email("Email"),
})}
required
errors={errors}
/>
<TextArea
label="Personal Message"
placeholder="Write a personal message here"
rows={7}
{...register(path("message"))}
/>
</div>
)
}
export default GiftCardReceiverForm
@@ -0,0 +1,78 @@
import { useAdminRegions } from "medusa-react"
import { useEffect, useMemo } from "react"
import { Controller, useWatch } from "react-hook-form"
import { Option } from "../../../../types/shared"
import FormValidator from "../../../../utils/form-validator"
import { NestedForm } from "../../../../utils/nested-form"
import { NextSelect } from "../../../molecules/select/next-select"
type RegionOption = Option & {
currency_code: string
}
export type GiftCardRegionFormType = {
region_id: RegionOption
}
type GiftCardRegionFormProps = {
form: NestedForm<GiftCardRegionFormType>
}
const GiftCardRegionForm = ({ form }: GiftCardRegionFormProps) => {
const {
control,
path,
formState: { errors },
setValue,
} = form
const { regions } = useAdminRegions({
limit: 100,
})
const regionOptions: RegionOption[] = useMemo(() => {
return (
regions?.map((r) => ({
label: r.name,
value: r.id,
currency_code: r.currency_code,
})) || []
)
}, [regions])
const subscriber = useWatch({
control,
name: path("region_id"),
})
useEffect(() => {
if (!subscriber) {
setValue(path("region_id"), regionOptions[0], {
shouldDirty: true,
})
}
}, [subscriber, regionOptions, setValue, path])
return (
<Controller
name={path("region_id")}
rules={{
required: FormValidator.required("Region"),
}}
control={control}
render={({ field }) => {
return (
<NextSelect
label="Region"
required
{...field}
errors={errors}
options={regionOptions}
/>
)
}}
/>
)
}
export default GiftCardRegionForm
@@ -1,22 +1,23 @@
import React from "react" import React, { PropsWithChildren } from "react"
import Actionables, { ActionType } from "../../molecules/actionables" import Actionables, { ActionType } from "../../molecules/actionables"
type BannerCardProps = { type BannerCardProps = PropsWithChildren<{
actions?: ActionType[] actions?: ActionType[]
title: string title: string
thumbnail: string | null thumbnail?: string | null
} & React.RefAttributes<HTMLDivElement> }> &
React.RefAttributes<HTMLDivElement>
type BannerCardDescriptionProps = { type BannerCardDescriptionProps = PropsWithChildren<{
cta?: { cta?: {
label: string label: string
onClick: (e: React.MouseEvent<HTMLButtonElement>) => void onClick: (e: React.MouseEvent<HTMLButtonElement>) => void
} }
} }>
const BannerCard: React.FC<BannerCardProps> & { const BannerCard: React.FC<BannerCardProps> & {
Description: React.FC<BannerCardDescriptionProps> Description: React.FC<BannerCardDescriptionProps>
Footer: React.FC Footer: React.FC<PropsWithChildren>
} = ({ title, thumbnail, actions, children }) => { } = ({ title, thumbnail, actions, children }) => {
return ( return (
<div className="rounded-rounded bg-grey-0 border-grey-20 p-base medium:p-xlarge w-full border"> <div className="rounded-rounded bg-grey-0 border-grey-20 p-base medium:p-xlarge w-full border">
@@ -63,7 +64,7 @@ const Description: React.FC<BannerCardDescriptionProps> = ({
) )
} }
const Footer: React.FC = ({ children }) => { const Footer = ({ children }: PropsWithChildren) => {
return <div className="mt-base">{children}</div> return <div className="mt-base">{children}</div>
} }
@@ -1,42 +0,0 @@
import clsx from "clsx"
import React from "react"
import { useNavigate } from "react-router-dom"
import ChevronRightIcon from "../../fundamentals/icons/chevron-right-icon"
type BreadcrumbProps = React.HtmlHTMLAttributes<HTMLDivElement> & {
previousRoute?: string
previousBreadcrumb?: string
currentPage: string
}
const Breadcrumb: React.FC<BreadcrumbProps> = ({
previousRoute = "/a/settings",
previousBreadcrumb = "Settings",
currentPage,
className,
...props
}) => {
const navigate = useNavigate()
return (
<div
className={clsx(
"inter-small-semibold text-grey-50 mb-4 flex w-full items-center",
className
)}
{...props}
>
<span
className="text-violet-60 cursor-pointer"
onClick={() => navigate(previousRoute)}
>
{previousBreadcrumb}
</span>
<span className="mx-0.5">
<ChevronRightIcon size={16} />
</span>
<span>{currentPage}</span>
</div>
)
}
export default Breadcrumb
@@ -64,7 +64,7 @@ const BodyCard: React.FC<BodyCardProps> = ({
"border-grey-20 border-b border-solid": setBorders, "border-grey-20 border-b border-solid": setBorders,
})} })}
> >
<div className="flex items-center justify-between"> <div className="flex items-start justify-between">
<div> <div>
{customHeader ? ( {customHeader ? (
<div>{customHeader}</div> <div>{customHeader}</div>
@@ -112,8 +112,8 @@ const BodyCard: React.FC<BodyCardProps> = ({
<Button <Button
key={i} key={i}
onClick={event.onClick} onClick={event.onClick}
className="first:ml-xsmall min-w-[130px] justify-center" className="first:ml-xsmall justify-center"
variant={i === 0 ? "primary" : "ghost"} variant={i === 0 ? "primary" : "secondary"}
size={"small"} size={"small"}
type={event.type} type={event.type}
> >
@@ -1,9 +1,9 @@
import React, { useState } from "react" import React, { useState } from "react"
import useNotification from "../../hooks/use-notification"
import { getErrorMessage } from "../../utils/error-messages"
import Button from "../fundamentals/button" import Button from "../fundamentals/button"
import Modal from "../molecules/modal" import Modal from "../molecules/modal"
import { getErrorMessage } from "../../utils/error-messages"
import useNotification from "../../hooks/use-notification"
type DeletePromptProps = { type DeletePromptProps = {
heading?: string heading?: string
@@ -54,10 +54,10 @@ const DeletePrompt: React.FC<DeletePromptProps> = ({
</div> </div>
</Modal.Content> </Modal.Content>
<Modal.Footer> <Modal.Footer>
<div className="flex h-8 w-full justify-end"> <div className="gap-x-xsmall flex h-8 w-full justify-end">
<Button <Button
variant="ghost" variant="secondary"
className="text-small min-w-24 mr-2 justify-center" className="justify-center"
size="small" size="small"
onClick={handleClose} onClick={handleClose}
> >
@@ -66,7 +66,7 @@ const DeletePrompt: React.FC<DeletePromptProps> = ({
<Button <Button
loading={isLoading} loading={isLoading}
size="small" size="small"
className="text-small w-24 justify-center" className="justify-center"
variant="nuclear" variant="nuclear"
onClick={handleSubmit} onClick={handleSubmit}
disabled={isLoading} disabled={isLoading}
@@ -1,5 +1,4 @@
import React, { useMemo } from "react" import React, { useMemo } from "react"
import { currencies } from "../../../utils/currencies"
import { normalizeAmount } from "../../../utils/prices" import { normalizeAmount } from "../../../utils/prices"
import EditIcon from "../../fundamentals/icons/edit-icon" import EditIcon from "../../fundamentals/icons/edit-icon"
import TrashIcon from "../../fundamentals/icons/trash-icon" import TrashIcon from "../../fundamentals/icons/trash-icon"
@@ -75,7 +74,7 @@ const GiftCardBanner: React.FC<GiftCardBannerProps> = ({
)} ${defaultCurrency.toUpperCase()}` )} ${defaultCurrency.toUpperCase()}`
}) })
.filter(Boolean) .filter(Boolean)
}, [variants, defaultCurrency, currencies]) }, [variants, defaultCurrency])
return ( return (
<BannerCard title={title} thumbnail={thumbnail} actions={actions}> <BannerCard title={title} thumbnail={thumbnail} actions={actions}>
@@ -7,7 +7,7 @@ import { getErrorMessage } from "../../../utils/error-messages"
import Button from "../../fundamentals/button" import Button from "../../fundamentals/button"
import InputField from "../../molecules/input" import InputField from "../../molecules/input"
import Modal from "../../molecules/modal" import Modal from "../../molecules/modal"
import Select from "../../molecules/select" import { NextSelect } from "../../molecules/select/next-select"
type InviteModalProps = { type InviteModalProps = {
handleClose: () => void handleClose: () => void
@@ -68,10 +68,13 @@ const InviteModal: React.FC<InviteModalProps> = ({ handleClose }) => {
name="role" name="role"
control={control} control={control}
defaultValue={{ label: "Member", value: "member" }} defaultValue={{ label: "Member", value: "member" }}
render={({ field: { value, onChange } }) => { render={({ field: { value, onChange, onBlur, ref } }) => {
return ( return (
<Select <NextSelect
label="Role" label="Role"
placeholder="Select role"
onBlur={onBlur}
ref={ref}
onChange={onChange} onChange={onChange}
options={roleOptions} options={roleOptions}
value={value} value={value}
@@ -27,7 +27,7 @@ function RawJSON(props: RawJSONProps) {
} }
return ( return (
<BodyCard className={"mb-4 h-auto min-h-0 w-full"} title={title}> <BodyCard className={"h-auto min-h-0 w-full"} title={title}>
<div className="flex flex-grow items-center"> <div className="flex flex-grow items-center">
<JSONView data={data} /> <JSONView data={data} />
</div> </div>
@@ -1,5 +1,6 @@
import React from "react" import React from "react"
import PageDescription from "../atoms/page-description" import PageDescription from "../atoms/page-description"
import Spacer from "../atoms/spacer"
const SettingsOverview: React.FC<React.PropsWithChildren> = ({ children }) => { const SettingsOverview: React.FC<React.PropsWithChildren> = ({ children }) => {
return ( return (
@@ -11,6 +12,7 @@ const SettingsOverview: React.FC<React.PropsWithChildren> = ({ children }) => {
<div className="medium:grid-cols-2 gap-x-base gap-y-xsmall grid auto-cols-fr grid-cols-1"> <div className="medium:grid-cols-2 gap-x-base gap-y-xsmall grid auto-cols-fr grid-cols-1">
{children} {children}
</div> </div>
<Spacer />
</div> </div>
) )
} }
@@ -38,7 +38,7 @@ const TwoSplitPane: React.FC<TwoSplitPaneProps> = ({
{Children.map(children, (child, i) => { {Children.map(children, (child, i) => {
return ( return (
<div <div
className={clsx("h-full w-full", { className={clsx("w-full", {
"col-span-2": threeCols && i === 1, "col-span-2": threeCols && i === 1,
})} })}
key={i} key={i}
@@ -107,7 +107,7 @@ const CollectionDetails = () => {
return ( return (
<> <>
<div className="!pb-xlarge flex flex-col"> <div className="flex flex-col">
<BackButton <BackButton
className="mb-xsmall" className="mb-xsmall"
path="/a/products?view=collections" path="/a/products?view=collections"
@@ -45,7 +45,7 @@ const CustomerDetail = () => {
path="/a/customers" path="/a/customers"
className="mb-xsmall" className="mb-xsmall"
/> />
<div className="gap-y-small flex flex-col"> <div className="gap-y-xsmall flex flex-col">
<Section> <Section>
<div className="flex w-full items-start justify-between"> <div className="flex w-full items-start justify-between">
<div className="gap-x-base flex w-full items-center"> <div className="gap-x-base flex w-full items-center">
@@ -1,4 +1,5 @@
import { Route, Routes } from "react-router-dom" import { Route, Routes } from "react-router-dom"
import Spacer from "../../components/atoms/spacer"
import BodyCard from "../../components/organisms/body-card" import BodyCard from "../../components/organisms/body-card"
import CustomerTable from "../../components/templates/customer-table" import CustomerTable from "../../components/templates/customer-table"
import Details from "./details" import Details from "./details"
@@ -7,15 +8,14 @@ import CustomersPageTableHeader from "./header"
const CustomerIndex = () => { const CustomerIndex = () => {
return ( return (
<div className="flex h-full grow flex-col"> <div>
<div className="flex w-full grow flex-col"> <BodyCard
<BodyCard customHeader={<CustomersPageTableHeader activeView="customers" />}
customHeader={<CustomersPageTableHeader activeView="customers" />} className="h-fit"
className="h-fit" >
> <CustomerTable />
<CustomerTable /> </BodyCard>
</BodyCard> <Spacer />
</div>
</div> </div>
) )
} }
@@ -22,54 +22,56 @@ const DiscountDetailsConditions: React.FC<DiscountDetailsConditionsProps> = ({
return ( return (
<ConditionsProvider discount={discount}> <ConditionsProvider discount={discount}>
<BodyCard <div>
title="Conditions" <BodyCard
className="min-h-[200px]" title="Conditions"
forceDropdown className="min-h-[200px]"
actionables={[ forceDropdown
{ actionables={[
label: "Add condition", {
icon: <PlusIcon size={16} />, label: "Add condition",
onClick: () => setShow(true), icon: <PlusIcon size={16} />,
}, onClick: () => setShow(true),
]} },
> ]}
{conditions.length ? ( >
<div {conditions.length ? (
style={{ <div
gridTemplateRows: `repeat(${Math.ceil( style={{
conditions?.length / 2 gridTemplateRows: `repeat(${Math.ceil(
)}, minmax(0, 1fr))`, conditions?.length / 2
}} )}, minmax(0, 1fr))`,
className="gap-y-base gap-x-xlarge grid grid-flow-col grid-cols-2" }}
> className="gap-y-base gap-x-xlarge grid grid-flow-col grid-cols-2"
{conditions.map((condition, i) => ( >
<NumberedItem {conditions.map((condition, i) => (
key={i} <NumberedItem
title={condition.title} key={i}
index={i + 1} title={condition.title}
description={condition.description} index={i + 1}
actions={condition.actions} description={condition.description}
/> actions={condition.actions}
))} />
</div> ))}
) : ( </div>
<div className="gap-y-small flex flex-1 flex-col items-center justify-center"> ) : (
<span className="inter-base-regular text-grey-50"> <div className="gap-y-small flex flex-1 flex-col items-center justify-center">
This discount has no conditions <span className="inter-base-regular text-grey-50">
</span> This discount has no conditions
</div> </span>
</div>
)}
</BodyCard>
<AddCondition show={show} onClose={() => setShow(false)} />
{selectedCondition && (
<EditConditionsModal
open={!!selectedCondition}
condition={selectedCondition}
discount={discount}
onClose={() => deSelectCondition()}
/>
)} )}
</BodyCard> </div>
<AddCondition show={show} onClose={() => setShow(false)} />
{selectedCondition && (
<EditConditionsModal
open={!!selectedCondition}
condition={selectedCondition}
discount={discount}
onClose={() => deSelectCondition()}
/>
)}
</ConditionsProvider> </ConditionsProvider>
) )
} }
@@ -1,8 +1,8 @@
import { useAdminDeleteDiscount, useAdminDiscount } from "medusa-react" import { useAdminDeleteDiscount, useAdminDiscount } from "medusa-react"
import { useState } from "react" import { useState } from "react"
import { useParams } from "react-router-dom" import { useParams } from "react-router-dom"
import BackButton from "../../../components/atoms/back-button"
import Spinner from "../../../components/atoms/spinner" import Spinner from "../../../components/atoms/spinner"
import Breadcrumb from "../../../components/molecules/breadcrumb"
import DeletePrompt from "../../../components/organisms/delete-prompt" import DeletePrompt from "../../../components/organisms/delete-prompt"
import RawJSON from "../../../components/organisms/raw-json" import RawJSON from "../../../components/organisms/raw-json"
import useNotification from "../../../hooks/use-notification" import useNotification from "../../../hooks/use-notification"
@@ -50,22 +50,22 @@ const Edit = () => {
/> />
)} )}
<Breadcrumb <BackButton
currentPage="Add Discount" label="Back to Discounts"
previousBreadcrumb="Discount" path="/a/discounts"
previousRoute="/a/discounts" className="mb-xsmall"
/> />
{isLoading || !discount ? ( {isLoading || !discount ? (
<div className="flex h-full items-center justify-center"> <div className="flex h-full items-center justify-center">
<Spinner variant="secondary" /> <Spinner variant="secondary" />
</div> </div>
) : ( ) : (
<div className="flex flex-col gap-y-4"> <div className="gap-y-xsmall flex flex-col">
<DiscountFormProvider> <DiscountFormProvider>
<General discount={discount} /> <General discount={discount} />
<Configurations discount={discount} /> <Configurations discount={discount} />
<DiscountDetailsConditions discount={discount} /> <DiscountDetailsConditions discount={discount} />
<RawJSON data={discount} title="Raw discount" rootName="discount" /> <RawJSON data={discount} title="Raw discount" />
</DiscountFormProvider> </DiscountFormProvider>
</div> </div>
)} )}
@@ -1,6 +1,7 @@
import { useState } from "react" import { useState } from "react"
import { Route, Routes } from "react-router-dom" import { Route, Routes } from "react-router-dom"
import Fade from "../../components/atoms/fade-wrapper" import Fade from "../../components/atoms/fade-wrapper"
import Spacer from "../../components/atoms/spacer"
import PlusIcon from "../../components/fundamentals/icons/plus-icon" import PlusIcon from "../../components/fundamentals/icons/plus-icon"
import BodyCard from "../../components/organisms/body-card" import BodyCard from "../../components/organisms/body-card"
import TableViewHeader from "../../components/organisms/custom-table-header" import TableViewHeader from "../../components/organisms/custom-table-header"
@@ -31,6 +32,7 @@ const DiscountIndex = () => {
> >
<DiscountTable /> <DiscountTable />
</BodyCard> </BodyCard>
<Spacer />
</div> </div>
<DiscountFormProvider> <DiscountFormProvider>
<Fade isVisible={isOpen} isFullScreen={true}> <Fade isVisible={isOpen} isFullScreen={true}>
@@ -1,161 +1,136 @@
import { useAdminCreateGiftCard, useAdminRegions } from "medusa-react" import { useAdminCreateGiftCard } from "medusa-react"
import React, { useEffect, useState } from "react" import React, { useEffect } from "react"
import { useForm } from "react-hook-form" import { useForm, useWatch } from "react-hook-form"
import GiftCardBalanceForm, {
GiftCardBalanceFormType,
} from "../../components/forms/gift-card/gift-card-balance-form"
import GiftCardEndsAtForm, {
GiftCardEndsAtFormType,
} from "../../components/forms/gift-card/gift-card-ends-at-form"
import GiftCardReceiverForm, {
GiftCardReceiverFormType,
} from "../../components/forms/gift-card/gift-card-receiver-form"
import GiftCardRegionForm, {
GiftCardRegionFormType,
} from "../../components/forms/gift-card/gift-card-region-form"
import Button from "../../components/fundamentals/button" import Button from "../../components/fundamentals/button"
import InputField from "../../components/molecules/input"
import Modal from "../../components/molecules/modal" import Modal from "../../components/molecules/modal"
import Select from "../../components/molecules/select"
import TextArea from "../../components/molecules/textarea"
import CurrencyInput from "../../components/organisms/currency-input"
import useNotification from "../../hooks/use-notification" import useNotification from "../../hooks/use-notification"
import { getErrorMessage } from "../../utils/error-messages" import { getErrorMessage } from "../../utils/error-messages"
import { focusByName } from "../../utils/focus-by-name" import { nestedForm } from "../../utils/nested-form"
import { validateEmail } from "../../utils/validate-email"
type CustomGiftcardProps = { type CustomGiftcardProps = {
onDismiss: () => void onClose: () => void
open: boolean
} }
const CustomGiftcard: React.FC<CustomGiftcardProps> = ({ onDismiss }) => { type CustomGiftCardFormType = {
const { isLoading, regions } = useAdminRegions() region: GiftCardRegionFormType
const [selectedRegion, setSelectedRegion] = useState<any>(null) ends_at: GiftCardEndsAtFormType
const [giftCardAmount, setGiftCardAmount] = useState(0) balance: GiftCardBalanceFormType
receiver: GiftCardReceiverFormType
}
const { register, handleSubmit } = useForm() const CustomGiftcard: React.FC<CustomGiftcardProps> = ({ onClose, open }) => {
const form = useForm<CustomGiftCardFormType>()
const {
handleSubmit,
reset,
control,
formState: { isDirty },
} = form
const currencySubscriber = useWatch({
control,
name: "region.region_id.currency_code",
defaultValue: "usd",
})
const notification = useNotification() const notification = useNotification()
const { mutate, isLoading: isSubmitting } = useAdminCreateGiftCard() const { mutate, isLoading: isSubmitting } = useAdminCreateGiftCard()
useEffect(() => { useEffect(() => {
if (!isLoading) { if (open) {
setSelectedRegion({ reset()
value: regions[0],
label: regions[0].name,
})
} }
}, [isLoading]) }, [open, reset])
const onSubmit = (data) => { const onSubmit = handleSubmit((data) => {
if (!giftCardAmount) { mutate(
notification("Error", "Please enter an amount", "error") {
focusByName("amount") region_id: data.region.region_id.value,
return value: data.balance.amount,
} ends_at: data.ends_at.ends_at || undefined,
metadata: {
if (!validateEmail(data.metadata.email)) { email: data.receiver.email,
notification("Error", "Invalid email address", "error") personal_message: data.receiver.message,
focusByName("metadata.email") },
return
}
const update = {
region_id: selectedRegion.value.id,
value: Math.round(
giftCardAmount / (1 + selectedRegion.value.tax_rate / 100)
),
...data,
}
mutate(update, {
onSuccess: () => {
notification("Success", "Created Custom Gift Card", "success")
onDismiss()
}, },
onError: (error) => { {
notification("Error", getErrorMessage(error), "error") onSuccess: () => {
onDismiss() notification(
}, "Created gift card",
}) "Custom gift card was created successfully",
} "success"
)
onClose()
},
onError: (error) => {
notification("Error", getErrorMessage(error), "error")
},
}
)
})
return ( return (
<Modal handleClose={onDismiss}> <Modal open={open} handleClose={onClose}>
<Modal.Body> <Modal.Body>
<Modal.Header handleClose={onDismiss}> <Modal.Header handleClose={onClose}>
<h2 className="inter-xlarge-semibold">Custom Gift Card</h2> <h2 className="inter-xlarge-semibold">Custom Gift Card</h2>
</Modal.Header> </Modal.Header>
<Modal.Content> <form onSubmit={onSubmit}>
<div className="flex flex-col"> <Modal.Content>
<span className="inter-base-semibold">Value</span> <div className="gap-y-xlarge flex flex-col">
<div className="gap-x-2xsmall mt-4 flex"> <div>
<div className="w-[267px]"> <h2 className="inter-base-semibold mb-base">Details</h2>
<Select <div className="gap-x-xsmall grid grid-cols-2">
label={"Region"} <GiftCardRegionForm form={nestedForm(form, "region")} />
value={selectedRegion} <GiftCardBalanceForm
onChange={(value) => setSelectedRegion(value)} form={nestedForm(form, "balance")}
options={ currencyCode={currencySubscriber}
regions?.map((r) => ({
value: r,
label: r.name,
})) || []
}
/>
</div>
<div className="w-[415px]">
<CurrencyInput.Root
size="medium"
currencyCodes={
isLoading ? undefined : regions?.map((r) => r.currency_code)
}
readOnly
currentCurrency={selectedRegion?.value?.currency_code}
>
<CurrencyInput.Amount
label={"Amount"}
amount={giftCardAmount}
onChange={(value) => {
setGiftCardAmount(value || 0)
}}
name="amount"
required={true}
/> />
</CurrencyInput.Root> </div>
</div>
<GiftCardEndsAtForm form={nestedForm(form, "ends_at")} />
<div>
<h2 className="inter-base-semibold mb-base">Receiver</h2>
<GiftCardReceiverForm form={nestedForm(form, "receiver")} />
</div> </div>
</div> </div>
</div> </Modal.Content>
<div className="mt-8"> <Modal.Footer>
<span className="inter-base-semibold">Receiver</span> <div className="gap-x-xsmall flex w-full justify-end">
<div className="gap-y-xsmall mt-4 grid grid-cols-1"> <Button
<InputField variant="secondary"
label={"Email"} onClick={onClose}
required size="small"
{...register("metadata.email", { required: true })} type="button"
placeholder="lebron@james.com" >
type="email" Cancel
/> </Button>
<TextArea <Button
label={"Personal Message"} variant="primary"
rows={7} type="submit"
placeholder="Something nice to someone special" size="small"
{...register("metadata.personal_message")} disabled={isSubmitting || !isDirty}
/> loading={isSubmitting}
>
Create and send
</Button>
</div> </div>
</div> </Modal.Footer>
</Modal.Content> </form>
<Modal.Footer>
<div className="gap-x-xsmall flex w-full justify-end">
<Button
variant="ghost"
onClick={onDismiss}
size="small"
className="w-[112px]"
>
Cancel
</Button>
<Button
variant="primary"
type="submit"
onClick={handleSubmit(onSubmit)}
size="small"
className="w-[112px]"
disabled={isSubmitting}
loading={isSubmitting}
>
Create & Send
</Button>
</div>
</Modal.Footer>
</Modal.Body> </Modal.Body>
</Modal> </Modal>
) )
@@ -1,111 +1,137 @@
import { AdminPostGiftCardsGiftCardReq, Region } from "@medusajs/medusa" import { GiftCard } from "@medusajs/medusa"
import { useMemo } from "react" import { useAdminUpdateGiftCard } from "medusa-react"
import { Controller, useForm } from "react-hook-form" import { useEffect } from "react"
import { useForm } from "react-hook-form"
import GiftCardEndsAtForm, {
GiftCardEndsAtFormType,
} from "../../../components/forms/gift-card/gift-card-ends-at-form"
import GiftCardRegionForm, {
GiftCardRegionFormType,
} from "../../../components/forms/gift-card/gift-card-region-form"
import Button from "../../../components/fundamentals/button" import Button from "../../../components/fundamentals/button"
import Modal from "../../../components/molecules/modal" import Modal from "../../../components/molecules/modal"
import Select from "../../../components/molecules/select" import useNotification from "../../../hooks/use-notification"
import { Option } from "../../../types/shared" import { getErrorMessage } from "../../../utils/error-messages"
import { nestedForm } from "../../../utils/nested-form"
type EditGiftCardModalProps = { type EditGiftCardModalProps = {
handleClose: () => void onClose: () => void
handleSave: (update: AdminPostGiftCardsGiftCardReq) => void open: boolean
updating: boolean giftCard: GiftCard
regions: Region[] | undefined
region: Region
} }
type EditGiftCardModalFormData = { type EditGiftCardFormType = {
region: Option region: GiftCardRegionFormType
ends_at: GiftCardEndsAtFormType
} }
const EditGiftCardModal = ({ const EditGiftCardModal = ({
handleClose, open,
handleSave, onClose,
updating, giftCard,
regions,
region,
}: EditGiftCardModalProps) => { }: EditGiftCardModalProps) => {
const { control, handleSubmit } = useForm<EditGiftCardModalFormData>({ const form = useForm<EditGiftCardFormType>({
defaultValues: { defaultValues: getDefaultValues(giftCard),
region: { })
value: region.id, const {
label: region.name, handleSubmit,
reset,
formState: { isDirty },
} = form
const { mutate, isLoading } = useAdminUpdateGiftCard(giftCard.id)
const notification = useNotification()
const onSubmit = handleSubmit((data) => {
mutate(
{
region_id: data.region.region_id.value,
ends_at: data.ends_at.ends_at,
}, },
}, {
onSuccess: () => {
notification(
"Updated Gift card",
"Gift card was succesfully updated",
"success"
)
onClose()
},
onError: (err) => {
notification(
"Failed to update Gift card",
getErrorMessage(err),
"error"
)
},
}
)
}) })
const onSubmit = (data: EditGiftCardModalFormData) => { useEffect(() => {
handleSave({ region_id: data.region.value }) if (open) {
} reset(getDefaultValues(giftCard))
}
const regionOptions: Option[] = useMemo(() => { }, [open, reset, giftCard])
return (
regions?.map((r) => ({
label: r.name,
value: r.id,
})) || []
)
}, [regions])
return ( return (
<Modal handleClose={handleClose} isLargeModal={true}> <Modal open={open} handleClose={onClose}>
<form onSubmit={handleSubmit(onSubmit)}> <Modal.Body>
<Modal.Body isLargeModal={true}> <Modal.Header handleClose={onClose}>
<Modal.Header handleClose={handleClose}> <h1 className="inter-xlarge-semibold">Edit Gift Card</h1>
<span className="inter-xlarge-semibold"> </Modal.Header>
Edit Gift Card Details <form onSubmit={onSubmit}>
</span>
</Modal.Header>
<Modal.Content> <Modal.Content>
{/* TODO: Missing backend support for updating code <div className="gap-y-xlarge flex flex-col">
<InputField <div>
label="Code" <h2 className="inter-base-semibold mb-base">Details</h2>
name="code" <GiftCardRegionForm form={nestedForm(form, "region")} />
value={code} </div>
onChange={({ currentTarget }) => setCode(currentTarget.value)} <GiftCardEndsAtForm form={nestedForm(form, "ends_at")} />
className="mb-4" </div>
/> */}
<Controller
control={control}
name="region"
render={({ field: { value, onChange } }) => {
return (
<Select
label="Region"
options={regionOptions}
value={value}
onChange={onChange}
/>
)
}}
/>
</Modal.Content> </Modal.Content>
<Modal.Footer> <Modal.Footer>
<div className="flex w-full justify-end"> <div className="gap-x-xsmall flex w-full justify-end">
<Button <Button
variant="ghost" variant="secondary"
size="small" size="small"
onClick={handleClose} onClick={onClose}
className="mr-2"
type="button" type="button"
> >
Cancel Cancel
</Button> </Button>
<Button <Button
loading={updating}
disabled={updating}
variant="primary" variant="primary"
className="min-w-[100px]"
size="small" size="small"
type="submit" type="submit"
disabled={isLoading || !isDirty}
loading={isLoading}
> >
Save Save and close
</Button> </Button>
</div> </div>
</Modal.Footer> </Modal.Footer>
</Modal.Body> </form>
</form> </Modal.Body>
</Modal> </Modal>
) )
} }
const getDefaultValues = (giftCard: GiftCard): EditGiftCardFormType => {
return {
region: {
region_id: {
label: giftCard.region.name,
value: giftCard.region.id,
currency_code: giftCard.region.currency_code,
},
},
ends_at: {
ends_at: giftCard.ends_at,
},
}
}
export default EditGiftCardModal export default EditGiftCardModal
@@ -1,23 +1,15 @@
import { AdminPostGiftCardsGiftCardReq } from "@medusajs/medusa" import { useAdminGiftCard, useAdminUpdateGiftCard } from "medusa-react"
import {
useAdminGiftCard,
useAdminRegions,
useAdminUpdateGiftCard,
} from "medusa-react"
import moment from "moment" import moment from "moment"
import { useState } from "react"
import { useParams } from "react-router-dom" import { useParams } from "react-router-dom"
import BackButton from "../../../components/atoms/back-button"
import Spinner from "../../../components/atoms/spinner" import Spinner from "../../../components/atoms/spinner"
import Badge from "../../../components/fundamentals/badge"
import DollarSignIcon from "../../../components/fundamentals/icons/dollar-sign-icon" import DollarSignIcon from "../../../components/fundamentals/icons/dollar-sign-icon"
import EditIcon from "../../../components/fundamentals/icons/edit-icon" import EditIcon from "../../../components/fundamentals/icons/edit-icon"
import PublishIcon from "../../../components/fundamentals/icons/publish-icon"
import UnpublishIcon from "../../../components/fundamentals/icons/unpublish-icon"
import Breadcrumb from "../../../components/molecules/breadcrumb"
import StatusSelector from "../../../components/molecules/status-selector" import StatusSelector from "../../../components/molecules/status-selector"
import BodyCard from "../../../components/organisms/body-card" import BodyCard from "../../../components/organisms/body-card"
import RawJSON from "../../../components/organisms/raw-json" import RawJSON from "../../../components/organisms/raw-json"
import useNotification from "../../../hooks/use-notification" import useNotification from "../../../hooks/use-notification"
import useToggleState from "../../../hooks/use-toggle-state"
import { getErrorMessage } from "../../../utils/error-messages" import { getErrorMessage } from "../../../utils/error-messages"
import { formatAmountWithSymbol } from "../../../utils/prices" import { formatAmountWithSymbol } from "../../../utils/prices"
import EditGiftCardModal from "./edit-gift-card-modal" import EditGiftCardModal from "./edit-gift-card-modal"
@@ -29,45 +21,46 @@ const GiftCardDetails = () => {
const { gift_card: giftCard, isLoading } = useAdminGiftCard(id!, { const { gift_card: giftCard, isLoading } = useAdminGiftCard(id!, {
enabled: !!id, enabled: !!id,
}) })
const { regions } = useAdminRegions()
const updateGiftCard = useAdminUpdateGiftCard(giftCard?.id!) const updateGiftCard = useAdminUpdateGiftCard(giftCard?.id!)
const notification = useNotification() const notification = useNotification()
const [showUpdateBalance, setShowUpdateBalance] = useState(false) const {
const [showEdit, setShowEdit] = useState(false) state: editState,
open: openEdit,
close: closeEdit,
} = useToggleState()
const {
state: balanceState,
open: openBalance,
close: closeBalance,
} = useToggleState()
const actions = [ const actions = [
{ {
label: "Edit", label: "Edit details",
onClick: () => setShowEdit(true), onClick: openEdit,
icon: <EditIcon size={20} />, icon: <EditIcon size={20} />,
}, },
{
label: `${giftCard?.is_disabled ? "Activate" : "Disable"}`,
onClick: () => handleUpdate({ is_disabled: !giftCard?.is_disabled }),
icon: giftCard?.is_disabled ? (
<PublishIcon size={20} />
) : (
<UnpublishIcon size={20} />
),
},
{ {
label: "Update balance", label: "Update balance",
onClick: () => setShowUpdateBalance(true), onClick: openBalance,
icon: <DollarSignIcon size={20} />, icon: <DollarSignIcon size={20} />,
}, },
] ]
const handleUpdate = (data: AdminPostGiftCardsGiftCardReq) => { const updateStatus = (data: { is_disabled?: boolean }) => {
updateGiftCard.mutate( updateGiftCard.mutate(
{ ...data }, { is_disabled: data.is_disabled },
{ {
onSuccess: () => { onSuccess: () => {
notification("Success", "Succesfully updated Gift Card", "success") notification(
setShowEdit(false) "Updated status",
setShowUpdateBalance(false) "Succesfully updated the status of the Gift Card",
"success"
)
}, },
onError: (err) => notification("Error", getErrorMessage(err), "error"), onError: (err) => notification("Error", getErrorMessage(err), "error"),
} }
@@ -76,10 +69,10 @@ const GiftCardDetails = () => {
return ( return (
<div> <div>
<Breadcrumb <BackButton
currentPage={"Gift Card Details"} label="Back to Gift Cards"
previousBreadcrumb={"Gift Cards"} path="/a/gift-cards"
previousRoute="/a/gift-cards" className="mb-xsmall"
/> />
{isLoading || !giftCard ? ( {isLoading || !giftCard ? (
<div className="bg-grey-0 border-grey-20 rounded-rounded py-xlarge flex w-full items-center justify-center border"> <div className="bg-grey-0 border-grey-20 rounded-rounded py-xlarge flex w-full items-center justify-center border">
@@ -87,83 +80,89 @@ const GiftCardDetails = () => {
</div> </div>
) : ( ) : (
<> <>
<BodyCard <div className="gap-y-xsmall flex flex-col">
className={"h-auto min-h-0 w-full"} <BodyCard
title={`${giftCard?.code}`} className={"h-auto min-h-0 w-full"}
subtitle={`Gift Card id: ${giftCard?.id}`} title={`${giftCard?.code}`}
status={ status={
<StatusSelector <StatusSelector
isDraft={!!giftCard?.is_disabled} isDraft={!!giftCard?.is_disabled}
activeState={"Active"} activeState={"Active"}
draftState={"Disable"} draftState={"Disable"}
onChange={() => onChange={() =>
handleUpdate({ is_disabled: !giftCard?.is_disabled }) updateStatus({ is_disabled: !giftCard.is_disabled })
} }
/> />
} }
actionables={actions} actionables={actions}
> >
<div className="flex justify-between"> <div className="flex justify-between">
<div className="mt-6 flex space-x-6 divide-x"> <div className="flex space-x-6 divide-x">
<div className="flex flex-col"> <div className="flex flex-col">
<div className="inter-smaller-regular text-grey-50 mb-1"> <div className="inter-smaller-regular text-grey-50 mb-1">
Original amount Original amount
</div>
<div>
{formatAmountWithSymbol({
amount: giftCard.value,
currency: giftCard.region.currency_code,
})}
</div>
</div> </div>
<div> <div className="flex flex-col pl-6">
{formatAmountWithSymbol({ <div className="inter-smaller-regular text-grey-50 mb-1">
amount: giftCard?.value, Balance
currency: giftCard?.region.currency_code, </div>
})} <div>
{formatAmountWithSymbol({
amount: giftCard.balance,
currency: giftCard.region.currency_code,
})}
</div>
</div> </div>
</div> <div className="flex flex-col pl-6">
<div className="flex flex-col pl-6"> <div className="inter-smaller-regular text-grey-50 mb-1">
<div className="inter-smaller-regular text-grey-50 mb-1"> Region
Balance </div>
<div>{giftCard.region.name}</div>
</div> </div>
<div> {giftCard.ends_at && (
{formatAmountWithSymbol({ <div className="flex flex-col pl-6">
amount: giftCard?.balance, <div className="inter-smaller-regular text-grey-50 mb-1">
currency: giftCard?.region.currency_code, Expires on
})} </div>
</div> <div>
</div> {moment(giftCard.ends_at).format("DD MMM YYYY")}
<div className="flex flex-col pl-6"> </div>
<div className="inter-smaller-regular text-grey-50 mb-1"> </div>
Created )}
</div> <div className="flex flex-col pl-6">
<div> <div className="inter-smaller-regular text-grey-50 mb-1">
{moment(giftCard?.created_at).format("DD MMM YYYY")} Created
</div>
<div>
{moment(giftCard.created_at).format("DD MMM YYYY")}
</div>
</div> </div>
</div> </div>
</div> </div>
<div className="flex items-end"> </BodyCard>
<Badge variant="default">{giftCard?.region?.name}</Badge> <RawJSON data={giftCard} title="Raw gift card" />
</div>
</div>
</BodyCard>
<div className="mt-large">
<RawJSON data={giftCard} title="Raw gift card" rootName="product" />
</div> </div>
<UpdateBalanceModal
giftCard={giftCard}
onClose={closeBalance}
open={balanceState}
/>
<EditGiftCardModal
onClose={closeEdit}
open={editState}
giftCard={giftCard}
/>
</> </>
)} )}
{showUpdateBalance && giftCard && (
<UpdateBalanceModal
giftCard={giftCard}
currencyCode={giftCard?.region.currency_code}
handleClose={() => setShowUpdateBalance(false)}
handleSave={handleUpdate}
updating={updateGiftCard.isLoading}
/>
)}
{showEdit && giftCard && (
<EditGiftCardModal
handleClose={() => setShowEdit(false)}
handleSave={handleUpdate}
regions={regions}
region={giftCard.region}
updating={updateGiftCard.isLoading}
/>
)}
</div> </div>
) )
} }
@@ -1,109 +1,109 @@
import { AdminPostGiftCardsGiftCardReq, GiftCard } from "@medusajs/medusa" import { GiftCard } from "@medusajs/medusa"
import clsx from "clsx" import { useAdminUpdateGiftCard } from "medusa-react"
import { Controller, useForm, useWatch } from "react-hook-form" import { useEffect } from "react"
import Tooltip from "../../../components/atoms/tooltip" import { useForm } from "react-hook-form"
import GiftCardBalanceForm, {
GiftCardBalanceFormType,
} from "../../../components/forms/gift-card/gift-card-balance-form"
import Button from "../../../components/fundamentals/button" import Button from "../../../components/fundamentals/button"
import Modal from "../../../components/molecules/modal" import Modal from "../../../components/molecules/modal"
import CurrencyInput from "../../../components/organisms/currency-input" import useNotification from "../../../hooks/use-notification"
import { getErrorMessage } from "../../../utils/error-messages"
import { nestedForm } from "../../../utils/nested-form"
type UpdateBalanceModalProps = { type UpdateBalanceModalProps = {
handleClose: () => void open: boolean
handleSave: (update: AdminPostGiftCardsGiftCardReq) => void onClose: () => void
currencyCode: string
giftCard: GiftCard giftCard: GiftCard
updating: boolean
} }
type UpdateBalanceModalFormData = { type UpdateBalanceModalFormData = {
balance: number balance: GiftCardBalanceFormType
} }
const UpdateBalanceModal = ({ const UpdateBalanceModal = ({
handleClose, open,
handleSave, onClose,
currencyCode,
giftCard, giftCard,
updating,
}: UpdateBalanceModalProps) => { }: UpdateBalanceModalProps) => {
const { control, handleSubmit } = useForm<UpdateBalanceModalFormData>({ const form = useForm<UpdateBalanceModalFormData>({
defaultValues: { defaultValues: getDefaultValues(giftCard),
balance: giftCard.balance,
},
}) })
const balance = useWatch({ const {
control, handleSubmit,
name: "balance", reset,
formState: { isDirty },
} = form
const { mutate, isLoading } = useAdminUpdateGiftCard(giftCard.id)
const notification = useNotification()
const onSubmit = handleSubmit((data) => {
mutate(
{
balance: data.balance.amount,
},
{
onSuccess: () => {
notification(
"Balance updated",
"Gift card balance was updated",
"success"
)
onClose()
},
onError: (err) => {
notification(
"Failed to update balance",
getErrorMessage(err),
"error"
)
},
}
)
}) })
useEffect(() => {
if (open) {
reset(getDefaultValues(giftCard))
}
}, [open, reset, giftCard])
return ( return (
<Modal handleClose={handleClose}> <Modal open={open} handleClose={onClose}>
<Modal.Body> <Modal.Body>
<form onSubmit={handleSubmit(handleSave)}> <Modal.Header handleClose={onClose}>
<Modal.Header handleClose={handleClose}> <h1 className="inter-xlarge-semibold">Update Balance</h1>
<span className="inter-xlarge-semibold">Update Balance</span> </Modal.Header>
<span <form onSubmit={onSubmit}>
className={clsx(
"inter-small-regular mt-2xsmall transition-display text-rose-50 delay-75",
{
hidden: !(balance > giftCard.value),
}
)}
>
Balance can't be updated to a value that is greater than the
original amount
</span>
</Modal.Header>
<Modal.Content> <Modal.Content>
<CurrencyInput.Root <GiftCardBalanceForm
readOnly form={nestedForm(form, "balance")}
currentCurrency={currencyCode} currencyCode={giftCard.region.currency_code}
size="small" originalAmount={giftCard.value}
> />
<Controller
control={control}
name="balance"
rules={{
required: true,
}}
render={({ field: { value, onChange } }) => {
return (
<CurrencyInput.Amount
amount={value}
label="Price"
onChange={onChange}
/>
)
}}
/>
</CurrencyInput.Root>
</Modal.Content> </Modal.Content>
<Modal.Footer> <Modal.Footer>
<div className="flex w-full justify-end"> <div className="gap-x-xsmall flex w-full justify-end">
<Button <Button
variant="ghost" variant="secondary"
size="small" size="small"
onClick={handleClose} onClick={onClose}
className="mr-2"
type="button" type="button"
> >
Cancel Cancel
</Button> </Button>
<Button <Button
loading={updating}
variant="primary" variant="primary"
className="min-w-[100px]"
size="small" size="small"
type="submit" type="submit"
disabled={balance > giftCard.value || updating} loading={isLoading}
disabled={isLoading || !isDirty}
> >
{balance > giftCard.value ? ( Save and close
<Tooltip content="Balance is above original value">
Save
</Tooltip>
) : (
"Save"
)}
</Button> </Button>
</div> </div>
</Modal.Footer> </Modal.Footer>
@@ -112,4 +112,13 @@ const UpdateBalanceModal = ({
</Modal> </Modal>
) )
} }
const getDefaultValues = (giftCard: GiftCard): UpdateBalanceModalFormData => {
return {
balance: {
amount: giftCard.balance,
},
}
}
export default UpdateBalanceModal export default UpdateBalanceModal
@@ -1,3 +1,4 @@
import { Product } from "@medusajs/medusa"
import { import {
useAdminDeleteProduct, useAdminDeleteProduct,
useAdminProducts, useAdminProducts,
@@ -7,6 +8,7 @@ import {
import { useMemo, useState } from "react" import { useMemo, useState } from "react"
import { useNavigate } from "react-router-dom" import { useNavigate } from "react-router-dom"
import PageDescription from "../../components/atoms/page-description" import PageDescription from "../../components/atoms/page-description"
import Spacer from "../../components/atoms/spacer"
import Spinner from "../../components/atoms/spinner" import Spinner from "../../components/atoms/spinner"
import PlusIcon from "../../components/fundamentals/icons/plus-icon" import PlusIcon from "../../components/fundamentals/icons/plus-icon"
import BannerCard from "../../components/molecules/banner-card" import BannerCard from "../../components/molecules/banner-card"
@@ -15,6 +17,7 @@ import DeletePrompt from "../../components/organisms/delete-prompt"
import GiftCardBanner from "../../components/organisms/gift-card-banner" import GiftCardBanner from "../../components/organisms/gift-card-banner"
import GiftCardTable from "../../components/templates/gift-card-table" import GiftCardTable from "../../components/templates/gift-card-table"
import useNotification from "../../hooks/use-notification" import useNotification from "../../hooks/use-notification"
import useToggleState from "../../hooks/use-toggle-state"
import { ProductStatus } from "../../types/shared" import { ProductStatus } from "../../types/shared"
import { getErrorMessage } from "../../utils/error-messages" import { getErrorMessage } from "../../utils/error-messages"
import CustomGiftcard from "./custom-giftcard" import CustomGiftcard from "./custom-giftcard"
@@ -26,8 +29,6 @@ const Overview = () => {
}) })
const { store } = useAdminStore() const { store } = useAdminStore()
const [showCreate, setShowCreate] = useState(false) const [showCreate, setShowCreate] = useState(false)
const [showCreateCustom, setShowCreateCustom] = useState(false)
const [showDelete, setShowDelete] = useState(false)
const giftCard = products?.[0] const giftCard = products?.[0]
@@ -36,6 +37,20 @@ const Overview = () => {
const updateGiftCard = useAdminUpdateProduct(giftCard?.id!) const updateGiftCard = useAdminUpdateProduct(giftCard?.id!)
const deleteGiftCard = useAdminDeleteProduct(giftCard?.id!) const deleteGiftCard = useAdminDeleteProduct(giftCard?.id!)
const {
state: stateCustom,
close: closeCustom,
open: openCustom,
} = useToggleState()
const { state: stateNew, close: closeNew, open: openNew } = useToggleState()
const {
state: stateDelete,
close: closeDelete,
open: openDelete,
} = useToggleState()
const onUpdate = () => { const onUpdate = () => {
let status: ProductStatus = ProductStatus.PUBLISHED let status: ProductStatus = ProductStatus.PUBLISHED
if (giftCard?.status === "published") { if (giftCard?.status === "published") {
@@ -43,7 +58,6 @@ const Overview = () => {
} }
updateGiftCard.mutate( updateGiftCard.mutate(
// @ts-ignore
{ status }, { status },
{ {
onSuccess: () => onSuccess: () =>
@@ -64,7 +78,7 @@ const Overview = () => {
const actionables = [ const actionables = [
{ {
label: "Custom Gift Card", label: "Custom Gift Card",
onClick: () => setShowCreateCustom(true), onClick: openCustom,
icon: <PlusIcon size={20} />, icon: <PlusIcon size={20} />,
}, },
] ]
@@ -74,69 +88,70 @@ const Overview = () => {
return null return null
} }
return { ...giftCard, defaultCurrency: store.default_currency_code } return {
...(giftCard as Product),
defaultCurrency: store.default_currency_code,
}
}, [giftCard, store]) }, [giftCard, store])
return ( return (
<> <>
<div className="pb-xlarge flex h-full grow flex-col"> <div className="flex flex-col">
<PageDescription <PageDescription
title="Gift Cards" title="Gift Cards"
subtitle="Manage the Gift Cards of your Medusa store" subtitle="Manage the Gift Cards of your Medusa store"
/> />
{!isLoading ? ( {!isLoading ? (
<> <div className="gap-y-xsmall flex flex-col">
<div className="mb-base"> {giftCardWithCurrency ? (
{giftCardWithCurrency ? ( <GiftCardBanner
<GiftCardBanner {...giftCardWithCurrency}
{...giftCardWithCurrency} onDelete={openDelete}
onDelete={() => setShowDelete(true)} onEdit={() => navigate("/a/gift-cards/manage")}
onEdit={() => navigate("/a/gift-cards/manage")} onUnpublish={onUpdate}
onUnpublish={onUpdate} />
/> ) : (
) : ( <BannerCard title="Are you ready to sell your first Gift Card?">
<BannerCard title="Are you ready to sell your first Gift Card?"> <BannerCard.Description
<BannerCard.Description cta={{
cta={{ label: "Create Gift Card",
label: "Create Gift Card", onClick: () => setShowCreate(true),
onClick: () => setShowCreate(true), }}
}} >
> No Gift Card has been added yet.
No Gift Card has been added yet. </BannerCard.Description>
</BannerCard.Description> </BannerCard>
</BannerCard> )}
)}
</div> <BodyCard
<div className="flex w-full grow flex-col"> title="History"
<BodyCard subtitle="See the history of purchased Gift Cards"
title="History" actionables={actionables}
subtitle="See the history of purchased Gift Cards" >
actionables={actionables} <GiftCardTable />
className="h-fit" </BodyCard>
> </div>
<GiftCardTable />
</BodyCard>
</div>
</>
) : ( ) : (
<div className="rounded-rounded border-grey-20 flex h-44 w-full items-center justify-center border"> <div className="rounded-rounded border-grey-20 flex h-44 w-full items-center justify-center border">
<Spinner variant="secondary" size="large" /> <Spinner variant="secondary" size="large" />
</div> </div>
)} )}
</div> </div>
{showCreateCustom && (
<CustomGiftcard onDismiss={() => setShowCreateCustom(false)} /> <CustomGiftcard onClose={closeCustom} open={stateCustom} />
)}
{showCreate && <NewGiftCard onClose={() => setShowCreate(!showCreate)} />} {showCreate && <NewGiftCard onClose={() => setShowCreate(!showCreate)} />}
{showDelete && (
{stateDelete && (
<DeletePrompt <DeletePrompt
handleClose={() => setShowDelete(!showDelete)} handleClose={closeDelete}
onDelete={async () => onDelete()} onDelete={async () => onDelete()}
successText="Successfully deleted Gift Card" successText="Successfully deleted Gift Card"
confirmText="Yes, delete" confirmText="Yes, delete"
heading="Delete Gift Card" heading="Delete Gift Card"
/> />
)} )}
<Spacer />
</> </>
) )
} }
@@ -1,3 +1,4 @@
import Spacer from "../../../components/atoms/spacer"
import BodyCard from "../../../components/organisms/body-card" import BodyCard from "../../../components/organisms/body-card"
import InventoryTable from "../../../components/templates/inventory-table" import InventoryTable from "../../../components/templates/inventory-table"
import InventoryPageTableHeader from "../header" import InventoryPageTableHeader from "../header"
@@ -12,6 +13,7 @@ const InventoryView = () => {
> >
<InventoryTable /> <InventoryTable />
</BodyCard> </BodyCard>
<Spacer />
</div> </div>
</div> </div>
) )
@@ -30,6 +30,7 @@ import moment from "moment"
import { useEffect, useMemo, useState } from "react" import { useEffect, useMemo, useState } from "react"
import { useHotkeys } from "react-hotkeys-hook" import { useHotkeys } from "react-hotkeys-hook"
import Avatar from "../../../components/atoms/avatar" import Avatar from "../../../components/atoms/avatar"
import BackButton from "../../../components/atoms/back-button"
import Spinner from "../../../components/atoms/spinner" import Spinner from "../../../components/atoms/spinner"
import Tooltip from "../../../components/atoms/tooltip" import Tooltip from "../../../components/atoms/tooltip"
import Button from "../../../components/fundamentals/button" import Button from "../../../components/fundamentals/button"
@@ -42,7 +43,6 @@ import MailIcon from "../../../components/fundamentals/icons/mail-icon"
import RefreshIcon from "../../../components/fundamentals/icons/refresh-icon" import RefreshIcon from "../../../components/fundamentals/icons/refresh-icon"
import TruckIcon from "../../../components/fundamentals/icons/truck-icon" import TruckIcon from "../../../components/fundamentals/icons/truck-icon"
import { ActionType } from "../../../components/molecules/actionables" import { ActionType } from "../../../components/molecules/actionables"
import Breadcrumb from "../../../components/molecules/breadcrumb"
import JSONView from "../../../components/molecules/json-view" import JSONView from "../../../components/molecules/json-view"
import BodyCard from "../../../components/organisms/body-card" import BodyCard from "../../../components/organisms/body-card"
import RawJSON from "../../../components/organisms/raw-json" import RawJSON from "../../../components/organisms/raw-json"
@@ -285,10 +285,10 @@ const OrderDetails = () => {
return ( return (
<div> <div>
<OrderEditProvider orderId={id!}> <OrderEditProvider orderId={id!}>
<Breadcrumb <BackButton
currentPage={"Order Details"} path="/a/orders"
previousBreadcrumb={"Orders"} label="Back to Orders"
previousRoute="/a/orders" className="mb-xsmall"
/> />
{isLoading || !order ? ( {isLoading || !order ? (
<BodyCard className="pt-2xlarge flex w-full items-center justify-center"> <BodyCard className="pt-2xlarge flex w-full items-center justify-center">
@@ -10,6 +10,7 @@ import moment from "moment"
import { useEffect, useState } from "react" import { useEffect, useState } from "react"
import { useNavigate, useParams } from "react-router-dom" import { useNavigate, useParams } from "react-router-dom"
import Avatar from "../../../components/atoms/avatar" import Avatar from "../../../components/atoms/avatar"
import BackButton from "../../../components/atoms/back-button"
import CopyToClipboard from "../../../components/atoms/copy-to-clipboard" import CopyToClipboard from "../../../components/atoms/copy-to-clipboard"
import Spinner from "../../../components/atoms/spinner" import Spinner from "../../../components/atoms/spinner"
import Badge from "../../../components/fundamentals/badge" import Badge from "../../../components/fundamentals/badge"
@@ -19,7 +20,6 @@ import DollarSignIcon from "../../../components/fundamentals/icons/dollar-sign-i
import TruckIcon from "../../../components/fundamentals/icons/truck-icon" import TruckIcon from "../../../components/fundamentals/icons/truck-icon"
import ImagePlaceholder from "../../../components/fundamentals/image-placeholder" import ImagePlaceholder from "../../../components/fundamentals/image-placeholder"
import StatusDot from "../../../components/fundamentals/status-indicator" import StatusDot from "../../../components/fundamentals/status-indicator"
import Breadcrumb from "../../../components/molecules/breadcrumb"
import JSONView from "../../../components/molecules/json-view" import JSONView from "../../../components/molecules/json-view"
import BodyCard from "../../../components/organisms/body-card" import BodyCard from "../../../components/organisms/body-card"
import ConfirmationPrompt from "../../../components/organisms/confirmation-prompt" import ConfirmationPrompt from "../../../components/organisms/confirmation-prompt"
@@ -125,10 +125,10 @@ const DraftOrderDetails = () => {
return ( return (
<div> <div>
<Breadcrumb <BackButton
currentPage={"Draft Order Details"} path="/a/draft-orders"
previousBreadcrumb={"Draft Orders"} label="Back to Draft Orders"
previousRoute="/a/draft-orders" className="mb-xsmall"
/> />
{isLoading || !draft_order ? ( {isLoading || !draft_order ? (
<BodyCard className="pt-2xlarge flex w-full items-center justify-center"> <BodyCard className="pt-2xlarge flex w-full items-center justify-center">
@@ -1,5 +1,6 @@
import { useMemo, useState } from "react" import { useMemo, useState } from "react"
import { Route, Routes, useNavigate } from "react-router-dom" import { Route, Routes, useNavigate } from "react-router-dom"
import Spacer from "../../../components/atoms/spacer"
import PlusIcon from "../../../components/fundamentals/icons/plus-icon" import PlusIcon from "../../../components/fundamentals/icons/plus-icon"
import BodyCard from "../../../components/organisms/body-card" import BodyCard from "../../../components/organisms/body-card"
@@ -47,6 +48,7 @@ const DraftOrderIndex = () => {
> >
<DraftOrderTable /> <DraftOrderTable />
</BodyCard> </BodyCard>
<Spacer />
</div> </div>
{showNewOrder && ( {showNewOrder && (
<NewOrderFormProvider> <NewOrderFormProvider>
@@ -1,19 +1,20 @@
import { Route, Routes, useNavigate } from "react-router-dom"
import { useMemo, useState } from "react" import { useMemo, useState } from "react"
import { Route, Routes, useNavigate } from "react-router-dom"
import BodyCard from "../../components/organisms/body-card" import { useAdminCreateBatchJob } from "medusa-react"
import Spacer from "../../components/atoms/spacer"
import Button from "../../components/fundamentals/button" import Button from "../../components/fundamentals/button"
import Details from "./details"
import ExportIcon from "../../components/fundamentals/icons/export-icon" import ExportIcon from "../../components/fundamentals/icons/export-icon"
import BodyCard from "../../components/organisms/body-card"
import TableViewHeader from "../../components/organisms/custom-table-header"
import ExportModal from "../../components/organisms/export-modal" import ExportModal from "../../components/organisms/export-modal"
import OrderTable from "../../components/templates/order-table" import OrderTable from "../../components/templates/order-table"
import TableViewHeader from "../../components/organisms/custom-table-header"
import { getErrorMessage } from "../../utils/error-messages"
import { transformFiltersAsExportContext } from "./utils"
import { useAdminCreateBatchJob } from "medusa-react"
import useNotification from "../../hooks/use-notification" import useNotification from "../../hooks/use-notification"
import { usePolling } from "../../providers/polling-provider"
import useToggleState from "../../hooks/use-toggle-state" import useToggleState from "../../hooks/use-toggle-state"
import { usePolling } from "../../providers/polling-provider"
import { getErrorMessage } from "../../utils/error-messages"
import Details from "./details"
import { transformFiltersAsExportContext } from "./utils"
const VIEWS = ["orders", "drafts"] const VIEWS = ["orders", "drafts"]
@@ -37,6 +38,7 @@ const OrderIndex = () => {
const actions = useMemo(() => { const actions = useMemo(() => {
return [ return [
<Button <Button
key="export"
variant="secondary" variant="secondary"
size="small" size="small"
onClick={() => openExportModal()} onClick={() => openExportModal()}
@@ -90,6 +92,7 @@ const OrderIndex = () => {
> >
<OrderTable setContextFilters={setContextFilters} /> <OrderTable setContextFilters={setContextFilters} />
</BodyCard> </BodyCard>
<Spacer />
</div> </div>
</div> </div>
{exportModalOpen && ( {exportModalOpen && (
@@ -1,6 +1,6 @@
import { useAdminPriceList } from "medusa-react" import { useAdminPriceList } from "medusa-react"
import { useParams } from "react-router-dom" import { useParams } from "react-router-dom"
import Breadcrumb from "../../../components/molecules/breadcrumb" import BackButton from "../../../components/atoms/back-button"
import RawJSON from "../../../components/organisms/raw-json" import RawJSON from "../../../components/organisms/raw-json"
import { mapPriceListToFormValues } from "../pricing-form/form/mappers" import { mapPriceListToFormValues } from "../pricing-form/form/mappers"
import { PriceListFormProvider } from "../pricing-form/form/pricing-form-context" import { PriceListFormProvider } from "../pricing-form/form/pricing-form-context"
@@ -13,25 +13,21 @@ const PricingDetails = () => {
const { price_list, isLoading } = useAdminPriceList(id!) const { price_list, isLoading } = useAdminPriceList(id!)
return ( return (
<div className="pb-xlarge"> <div className="pb-large">
<Breadcrumb <BackButton
currentPage="Edit price list" label="Back to Pricing"
previousBreadcrumb="Pricing" path="/a/pricing"
previousRoute="/a/pricing" className="mb-xsmall"
/> />
{!isLoading && price_list ? ( {!isLoading && price_list ? (
<PriceListFormProvider priceList={mapPriceListToFormValues(price_list)}> <PriceListFormProvider priceList={mapPriceListToFormValues(price_list)}>
<Header priceList={price_list} /> <div className="gap-y-xsmall flex flex-col">
<div className="mt-4 w-full"> <Header priceList={price_list} />
<PricesDetails id={price_list?.id} /> <PricesDetails id={price_list?.id} />
</div>
<div className="mt-xlarge"> <RawJSON data={price_list} title="Raw price list" />
<RawJSON
data={price_list}
title="Raw price list"
rootName="price_list"
/>
</div> </div>
</PriceListFormProvider> </PriceListFormProvider>
) : null} ) : null}
@@ -1,4 +1,5 @@
import { Route, Routes, useNavigate } from "react-router-dom" import { Route, Routes, useNavigate } from "react-router-dom"
import Spacer from "../../components/atoms/spacer"
import PlusIcon from "../../components/fundamentals/icons/plus-icon" import PlusIcon from "../../components/fundamentals/icons/plus-icon"
import BodyCard from "../../components/organisms/body-card" import BodyCard from "../../components/organisms/body-card"
import TableViewHeader from "../../components/organisms/custom-table-header" import TableViewHeader from "../../components/organisms/custom-table-header"
@@ -27,6 +28,7 @@ const PricingIndex = () => {
> >
<PricingTable /> <PricingTable />
</BodyCard> </BodyCard>
<Spacer />
</div> </div>
</div> </div>
) )
@@ -3,6 +3,7 @@ import { createContext, useState } from "react"
import { ProductCategory } from "@medusajs/medusa" import { ProductCategory } from "@medusajs/medusa"
import { useAdminProductCategories } from "medusa-react" import { useAdminProductCategories } from "medusa-react"
import Spacer from "../../../components/atoms/spacer"
import BodyCard from "../../../components/organisms/body-card" import BodyCard from "../../../components/organisms/body-card"
import useToggleState from "../../../hooks/use-toggle-state" import useToggleState from "../../../hooks/use-toggle-state"
import ProductCategoriesList from "../components/product-categories-list" import ProductCategoriesList from "../components/product-categories-list"
@@ -99,6 +100,7 @@ function ProductCategoryPage() {
<ProductCategoriesList categories={categories!} /> <ProductCategoriesList categories={categories!} />
)} )}
</BodyCard> </BodyCard>
<Spacer />
{isCreateModalVisible && ( {isCreateModalVisible && (
<CreateProductCategory <CreateProductCategory
parentCategory={activeCategory} parentCategory={activeCategory}
@@ -1,20 +1,30 @@
import { AdminPostProductsReq, ProductVariant } from "@medusajs/medusa" import { AdminPostProductsReq, ProductVariant } from "@medusajs/medusa"
import { useAdminCreateProduct, useMedusa } from "medusa-react" import { useAdminCreateProduct, useMedusa } from "medusa-react"
import { useForm, useWatch } from "react-hook-form" import { useForm, useWatch } from "react-hook-form"
import CustomsForm, { CustomsFormType } from "../../../components/forms/product/customs-form" import CustomsForm, {
CustomsFormType,
} from "../../../components/forms/product/customs-form"
import DimensionsForm, { import DimensionsForm, {
DimensionsFormType DimensionsFormType,
} from "../../../components/forms/product/dimensions-form" } from "../../../components/forms/product/dimensions-form"
import DiscountableForm, { import DiscountableForm, {
DiscountableFormType DiscountableFormType,
} from "../../../components/forms/product/discountable-form" } from "../../../components/forms/product/discountable-form"
import GeneralForm, { GeneralFormType } from "../../../components/forms/product/general-form" import GeneralForm, {
import MediaForm, { MediaFormType } from "../../../components/forms/product/media-form" GeneralFormType,
import OrganizeForm, { OrganizeFormType } from "../../../components/forms/product/organize-form" } from "../../../components/forms/product/general-form"
import ThumbnailForm, { ThumbnailFormType } from "../../../components/forms/product/thumbnail-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 { FormImage, ProductStatus } from "../../../types/shared"
import AddSalesChannelsForm, { import AddSalesChannelsForm, {
AddSalesChannelsFormType AddSalesChannelsFormType,
} from "./add-sales-channels" } from "./add-sales-channels"
import AddVariantsForm, { AddVariantsFormType } from "./add-variants" import AddVariantsForm, { AddVariantsFormType } from "./add-variants"
@@ -247,8 +257,8 @@ const NewProduct = ({ onClose }: Props) => {
</div> </div>
</div> </div>
</FocusModal.Header> </FocusModal.Header>
<FocusModal.Main className="no-scrollbar flex w-full justify-center"> <FocusModal.Main className="no-scrollbar flex w-full justify-center py-16">
<div className="small:w-4/5 medium:w-7/12 large:w-6/12 my-16 max-w-[700px]"> <div className="small:w-4/5 medium:w-7/12 large:w-6/12 max-w-[700px]">
<Accordion defaultValue={["general"]} type="multiple"> <Accordion defaultValue={["general"]} type="multiple">
<Accordion.Item <Accordion.Item
value={"general"} value={"general"}
@@ -357,14 +367,14 @@ const createPayload = (
mid_code: data.customs.mid_code || undefined, mid_code: data.customs.mid_code || undefined,
type: data.organize.type type: data.organize.type
? { ? {
value: data.organize.type.label, value: data.organize.type.label,
id: data.organize.type.value, id: data.organize.type.value,
} }
: undefined, : undefined,
tags: data.organize.tags tags: data.organize.tags
? data.organize.tags.map((t) => ({ ? data.organize.tags.map((t) => ({
value: t, value: t,
})) }))
: undefined, : undefined,
categories: data.organize.categories?.length categories: data.organize.categories?.length
? data.organize.categories.map((id) => ({ id })) ? data.organize.categories.map((id) => ({ id }))
@@ -435,6 +445,7 @@ const createBlank = (): NewProductForm => {
images: [], images: [],
}, },
organize: { organize: {
categories: null,
collection: null, collection: null,
tags: null, tags: null,
type: null, type: null,
@@ -2,6 +2,7 @@ import { useAdminCreateBatchJob, useAdminCreateCollection } from "medusa-react"
import { useEffect, useState } from "react" import { useEffect, useState } from "react"
import { useLocation, useNavigate } from "react-router-dom" import { useLocation, useNavigate } from "react-router-dom"
import Fade from "../../../components/atoms/fade-wrapper" import Fade from "../../../components/atoms/fade-wrapper"
import Spacer from "../../../components/atoms/spacer"
import Button from "../../../components/fundamentals/button" import Button from "../../../components/fundamentals/button"
import ExportIcon from "../../../components/fundamentals/icons/export-icon" import ExportIcon from "../../../components/fundamentals/icons/export-icon"
import PlusIcon from "../../../components/fundamentals/icons/plus-icon" import PlusIcon from "../../../components/fundamentals/icons/plus-icon"
@@ -14,10 +15,10 @@ import CollectionsTable from "../../../components/templates/collections-table"
import ProductTable from "../../../components/templates/product-table" import ProductTable from "../../../components/templates/product-table"
import useNotification from "../../../hooks/use-notification" import useNotification from "../../../hooks/use-notification"
import useToggleState from "../../../hooks/use-toggle-state" import useToggleState from "../../../hooks/use-toggle-state"
import { usePolling } from "../../../providers/polling-provider"
import { getErrorMessage } from "../../../utils/error-messages" import { getErrorMessage } from "../../../utils/error-messages"
import ImportProducts from "../batch-job/import" import ImportProducts from "../batch-job/import"
import NewProduct from "../new" import NewProduct from "../new"
import { usePolling } from "../../../providers/polling-provider"
const VIEWS = ["products", "collections"] const VIEWS = ["products", "collections"]
@@ -178,8 +179,10 @@ const Overview = () => {
> >
<CurrentView /> <CurrentView />
</BodyCard> </BodyCard>
<Spacer />
</div> </div>
</div> </div>
{showNewCollection && ( {showNewCollection && (
<AddCollectionModal <AddCollectionModal
onClose={() => setShowNewCollection(!showNewCollection)} onClose={() => setShowNewCollection(!showNewCollection)}
@@ -6,21 +6,22 @@ import {
useAdminCreatePublishableApiKey, useAdminCreatePublishableApiKey,
} from "medusa-react" } from "medusa-react"
import Breadcrumb from "../../../components/molecules/breadcrumb" import BackButton from "../../../components/atoms/back-button"
import BodyCard from "../../../components/organisms/body-card"
import FocusModal from "../../../components/molecules/modal/focus-modal"
import Fade from "../../../components/atoms/fade-wrapper" import Fade from "../../../components/atoms/fade-wrapper"
import useToggleState from "../../../hooks/use-toggle-state" import Spacer from "../../../components/atoms/spacer"
import Button from "../../../components/fundamentals/button" import Button from "../../../components/fundamentals/button"
import ChannelsIcon from "../../../components/fundamentals/icons/channels-icon"
import CrossIcon from "../../../components/fundamentals/icons/cross-icon" import CrossIcon from "../../../components/fundamentals/icons/cross-icon"
import InputField from "../../../components/molecules/input" import InputField from "../../../components/molecules/input"
import FocusModal from "../../../components/molecules/modal/focus-modal"
import SalesChannelsSummary from "../../../components/molecules/sales-channels-summary"
import BodyCard from "../../../components/organisms/body-card"
import useNotification from "../../../hooks/use-notification" import useNotification from "../../../hooks/use-notification"
import PublishableApiKeysTable from "../tables/publishable-api-keys-table" import useToggleState from "../../../hooks/use-toggle-state"
import AddSalesChannelsSideModal from "../modals/add-sales-channels"
import DetailsModal from "../modals/details" import DetailsModal from "../modals/details"
import ManageSalesChannelsSideModal from "../modals/manage-sales-channels" import ManageSalesChannelsSideModal from "../modals/manage-sales-channels"
import ChannelsIcon from "../../../components/fundamentals/icons/channels-icon" import PublishableApiKeysTable from "../tables/publishable-api-keys-table"
import SalesChannelsSummary from "../../../components/molecules/sales-channels-summary"
import AddSalesChannelsSideModal from "../modals/add-sales-channels"
type AddSalesChannelsSectionProps = { type AddSalesChannelsSectionProps = {
setSelectedChannels: (arg: any) => void setSelectedChannels: (arg: any) => void
@@ -229,10 +230,10 @@ function Index() {
return ( return (
<div> <div>
<Breadcrumb <BackButton
currentPage="Publishable API Keys" label="Back to settings"
previousBreadcrumb="Settings" path="/a/settings"
previousRoute="/a/settings" className="mb-xsmall"
/> />
<BodyCard <BodyCard
title="Publishable API keys" title="Publishable API keys"
@@ -256,6 +257,7 @@ function Index() {
close={_closeChannelsModal} close={_closeChannelsModal}
/> />
</BodyCard> </BodyCard>
<Spacer />
</div> </div>
) )
} }
@@ -1,5 +1,5 @@
import clsx from "clsx" import clsx from "clsx"
import { useEffect, useRef, useState, useMemo } from "react" import { useEffect, useMemo, useRef, useState } from "react"
import { SalesChannel } from "@medusajs/medusa" import { SalesChannel } from "@medusajs/medusa"
import { import {
@@ -9,27 +9,27 @@ import {
useAdminUpdateSalesChannel, useAdminUpdateSalesChannel,
} from "medusa-react" } from "medusa-react"
import EditSalesChannel from "../form/edit-sales-channel" import { useNavigate, useParams } from "react-router-dom"
import AddSalesChannelModal from "../form/add-sales-channel" import BackButton from "../../../components/atoms/back-button"
import Fade from "../../../components/atoms/fade-wrapper"
import Spacer from "../../../components/atoms/spacer"
import CrossIcon from "../../../components/fundamentals/icons/cross-icon"
import EditIcon from "../../../components/fundamentals/icons/edit-icon"
import PlusIcon from "../../../components/fundamentals/icons/plus-icon"
import SearchIcon from "../../../components/fundamentals/icons/search-icon"
import TrashIcon from "../../../components/fundamentals/icons/trash-icon"
import Actionables, { import Actionables, {
ActionType, ActionType,
} from "../../../components/molecules/actionables" } from "../../../components/molecules/actionables"
import PlusIcon from "../../../components/fundamentals/icons/plus-icon" import StatusSelector from "../../../components/molecules/status-selector"
import EditIcon from "../../../components/fundamentals/icons/edit-icon" import useImperativeDialog from "../../../hooks/use-imperative-dialog"
import TrashIcon from "../../../components/fundamentals/icons/trash-icon" import useToggleState from "../../../hooks/use-toggle-state"
import SearchIcon from "../../../components/fundamentals/icons/search-icon" import AddSalesChannelModal from "../form/add-sales-channel"
import EditSalesChannel from "../form/edit-sales-channel"
import { import {
SalesChannelProductsSelectModal, SalesChannelProductsSelectModal,
SalesChannelProductsTable, SalesChannelProductsTable,
} from "../tables/product" } from "../tables/product"
import CrossIcon from "../../../components/fundamentals/icons/cross-icon"
import StatusSelector from "../../../components/molecules/status-selector"
import TwoSplitPane from "../../../components/templates/two-split-pane"
import Fade from "../../../components/atoms/fade-wrapper"
import Breadcrumb from "../../../components/molecules/breadcrumb"
import useToggleState from "../../../hooks/use-toggle-state"
import { useNavigate, useParams } from "react-router-dom"
import useImperativeDialog from "../../../hooks/use-imperative-dialog"
type ListIndicatorProps = { isActive: boolean } type ListIndicatorProps = { isActive: boolean }
@@ -101,10 +101,7 @@ function SalesChannelTile(props: SalesChannelTileProps) {
<h3 className="text-grey-90 mb-1 font-semibold leading-5"> <h3 className="text-grey-90 mb-1 font-semibold leading-5">
{salesChannel.name} {salesChannel.name}
</h3> </h3>
<span <span className="text-small text-grey-50 ">
title={salesChannel.description}
className="text-small text-grey-50 "
>
{salesChannel.description} {salesChannel.description}
</span> </span>
</div> </div>
@@ -210,7 +207,7 @@ function SalesChannelsList(props: SalesChannelsListProps) {
} = props } = props
return ( return (
<div className="bg-grey-0 border-grey-20 col-span-1 h-[968px] rounded-lg border px-8 py-6"> <div className="bg-grey-0 border-grey-20 col-span-1 grow rounded-lg border px-8 py-6">
<SalesChannelsHeader <SalesChannelsHeader
filterText={filterText} filterText={filterText}
setFilterText={setFilterText} setFilterText={setFilterText}
@@ -338,7 +335,7 @@ function SalesChannelDetails(props: SalesChannelDetailsProps) {
useToggleState(false) useToggleState(false)
return ( return (
<div className="rounded-rounded bg-grey-0 border-grey-20 col-span-2 h-[968px] border px-8 py-6"> <div className="rounded-rounded bg-grey-0 border-grey-20 col-span-2 col-span-2 h-fit border px-8 py-6">
<SalesChannelDetailsHeader <SalesChannelDetailsHeader
isDefault={isDefault} isDefault={isDefault}
resetDetails={resetDetails} resetDetails={resetDetails}
@@ -449,13 +446,13 @@ function Details() {
return ( return (
<div> <div>
<Breadcrumb <BackButton
currentPage={"Sales channels"} path="/a/settings"
previousBreadcrumb={"Settings"} label="Back to settings"
previousRoute="/a/settings" className="mb-xsmall"
/> />
<TwoSplitPane threeCols> <div className="gap-x-xsmall grid grid-cols-3">
<SalesChannelsList <SalesChannelsList
filterText={filterText} filterText={filterText}
setFilterText={setFilterText} setFilterText={setFilterText}
@@ -477,7 +474,8 @@ function Details() {
resetDetails={resetDetails} resetDetails={resetDetails}
/> />
)} )}
</TwoSplitPane> </div>
<Spacer />
<Fade isVisible={showCreateModal} isFullScreen={true}> <Fade isVisible={showCreateModal} isFullScreen={true}>
<AddSalesChannelModal onClose={closeCreateModal} /> <AddSalesChannelModal onClose={closeCreateModal} />
@@ -2,7 +2,7 @@ import { Store } from "@medusajs/medusa"
import { useAdminStore, useAdminUpdateStore } from "medusa-react" import { useAdminStore, useAdminUpdateStore } from "medusa-react"
import { useEffect } from "react" import { useEffect } from "react"
import { useForm } from "react-hook-form" import { useForm } from "react-hook-form"
import BreadCrumb from "../../components/molecules/breadcrumb" import BackButton from "../../components/atoms/back-button"
import Input from "../../components/molecules/input" import Input from "../../components/molecules/input"
import BodyCard from "../../components/organisms/body-card" import BodyCard from "../../components/organisms/body-card"
import useNotification from "../../hooks/use-notification" import useNotification from "../../hooks/use-notification"
@@ -64,10 +64,10 @@ const AccountDetails = () => {
return ( return (
<form className="flex-col py-5"> <form className="flex-col py-5">
<div className="max-w-[632px]"> <div className="max-w-[632px]">
<BreadCrumb <BackButton
previousRoute="/a/settings/" path="/a/settings/"
previousBreadcrumb="Settings" label="Back to settings"
currentPage="Store Details" className="mb-xsmall"
/> />
<BodyCard <BodyCard
events={[ events={[
@@ -76,37 +76,41 @@ const AccountDetails = () => {
type: "button", type: "button",
onClick: handleSubmit(onSubmit), onClick: handleSubmit(onSubmit),
}, },
{ label: "Cancel changes", type: "button", onClick: handleCancel }, { label: "Cancel", type: "button", onClick: handleCancel },
]} ]}
title="Store Details" title="Store Details"
subtitle="Manage your business details" subtitle="Manage your business details"
> >
<h6 className="mt-large inter-base-semibold">General</h6> <div className="gap-y-xlarge mb-large flex flex-col">
<Input <div>
className="mt-base" <h2 className="inter-base-semibold mb-base">General</h2>
label="Store name" <Input
{...register("name")} label="Store name"
placeholder="Medusa Store" {...register("name")}
/> placeholder="Medusa Store"
<h6 className="mt-2xlarge inter-base-semibold">Advanced settings</h6> />
<Input </div>
className="mt-base" <div>
label="Swap link template" <h2 className="inter-base-semibold mb-base">Advanced settings</h2>
{...register("swap_link_template")} <Input
placeholder="https://acme.inc/swap={swap_id}" label="Swap link template"
/> {...register("swap_link_template")}
<Input placeholder="https://acme.inc/swap={swap_id}"
className="mt-base" />
label="Draft order link template" <Input
{...register("payment_link_template")} className="mt-base"
placeholder="https://acme.inc/payment={payment_id}" label="Draft order link template"
/> {...register("payment_link_template")}
<Input placeholder="https://acme.inc/payment={payment_id}"
className="mt-base" />
label="Invite link template" <Input
{...register("invite_link_template")} className="mt-base"
placeholder="https://acme-admin.inc/invite?token={invite_token}" label="Invite link template"
/> {...register("invite_link_template")}
placeholder="https://acme-admin.inc/invite?token={invite_token}"
/>
</div>
</div>
</BodyCard> </BodyCard>
</div> </div>
</form> </form>
@@ -9,7 +9,7 @@ const Regions = () => {
return ( return (
<div className="gap-y-xsmall flex h-full flex-col"> <div className="gap-y-xsmall flex h-full flex-col">
<BackButton label="Back to Settings" path="/a/settings" /> <BackButton label="Back to settings" path="/a/settings" />
<div className="medium:grid-cols-3 gap-xsmall pb-xlarge grid grid-cols-1"> <div className="medium:grid-cols-3 gap-xsmall pb-xlarge grid grid-cols-1">
<div className="h-full w-full"> <div className="h-full w-full">
<RegionOverview id={regId} /> <RegionOverview id={regId} />
@@ -104,7 +104,7 @@ const ReturnReasonDetail = ({ reason }: ReturnReasonDetailsProps) => {
onClick: handleSubmit(onSave), onClick: handleSubmit(onSave),
}, },
{ {
label: "Cancel changes", label: "Cancel",
onClick: handleCancel, onClick: handleCancel,
}, },
]} ]}
@@ -1,8 +1,8 @@
import { useAdminReturnReasons } from "medusa-react" import { useAdminReturnReasons } from "medusa-react"
import { useState } from "react" import { useState } from "react"
import BackButton from "../../../components/atoms/back-button"
import Spinner from "../../../components/atoms/spinner" import Spinner from "../../../components/atoms/spinner"
import PlusIcon from "../../../components/fundamentals/icons/plus-icon" import PlusIcon from "../../../components/fundamentals/icons/plus-icon"
import BreadCrumb from "../../../components/molecules/breadcrumb"
import BodyCard from "../../../components/organisms/body-card" import BodyCard from "../../../components/organisms/body-card"
import RadioGroup from "../../../components/organisms/radio-group" import RadioGroup from "../../../components/organisms/radio-group"
import TwoSplitPane from "../../../components/templates/two-split-pane" import TwoSplitPane from "../../../components/templates/two-split-pane"
@@ -30,10 +30,10 @@ const ReturnReasons = () => {
return ( return (
<div> <div>
<BreadCrumb <BackButton
previousRoute="/a/settings" path="/a/settings"
previousBreadcrumb="Settings" label="Back to settings"
currentPage="Return Reasons" className="mb-xsmall"
/> />
<TwoSplitPane> <TwoSplitPane>
<BodyCard <BodyCard
@@ -65,6 +65,7 @@ const ReturnReasons = () => {
> >
{return_reasons?.map((reason) => ( {return_reasons?.map((reason) => (
<RadioGroup.Item <RadioGroup.Item
key={reason.id}
label={reason.label} label={reason.label}
description={reason.description} description={reason.description}
className="mt-xsmall" className="mt-xsmall"
@@ -1,9 +1,9 @@
import { useAdminRegions } from "medusa-react" import { useAdminRegions } from "medusa-react"
import { useEffect } from "react" import { useEffect } from "react"
import { useNavigate, useSearchParams } from "react-router-dom" import { useNavigate, useSearchParams } from "react-router-dom"
import BackButton from "../../../components/atoms/back-button"
import Spinner from "../../../components/atoms/spinner" import Spinner from "../../../components/atoms/spinner"
import GearIcon from "../../../components/fundamentals/icons/gear-icon" import GearIcon from "../../../components/fundamentals/icons/gear-icon"
import BreadCrumb from "../../../components/molecules/breadcrumb"
import BodyCard from "../../../components/organisms/body-card" import BodyCard from "../../../components/organisms/body-card"
import RadioGroup from "../../../components/organisms/radio-group" import RadioGroup from "../../../components/organisms/radio-group"
import TwoSplitPane from "../../../components/templates/two-split-pane" import TwoSplitPane from "../../../components/templates/two-split-pane"
@@ -26,10 +26,10 @@ const Taxes = () => {
return ( return (
<> <>
<div> <div>
<BreadCrumb <BackButton
previousRoute="/a/settings" path="/a/settings"
previousBreadcrumb="Settings" label="Back to settings"
currentPage="Taxes" className="mb-xsmall"
/> />
<TwoSplitPane threeCols> <TwoSplitPane threeCols>
<BodyCard <BodyCard
@@ -1,10 +1,10 @@
import React, { useEffect, useState } from "react" import React, { useEffect, useState } from "react"
import BreadCrumb from "../../../components/molecules/breadcrumb" import BackButton from "../../../components/atoms/back-button"
import Medusa from "../../../services/api" import PlusIcon from "../../../components/fundamentals/icons/plus-icon"
import BodyCard from "../../../components/organisms/body-card" import BodyCard from "../../../components/organisms/body-card"
import InviteModal from "../../../components/organisms/invite-modal" import InviteModal from "../../../components/organisms/invite-modal"
import PlusIcon from "../../../components/fundamentals/icons/plus-icon"
import UserTable from "../../../components/templates/user-table" import UserTable from "../../../components/templates/user-table"
import Medusa from "../../../services/api"
const Users: React.FC = () => { const Users: React.FC = () => {
const [users, setUsers] = useState([]) const [users, setUsers] = useState([])
@@ -46,28 +46,27 @@ const Users: React.FC = () => {
return ( return (
<div className="flex h-full flex-col"> <div className="flex h-full flex-col">
<div className="flex w-full grow flex-col"> <div className="flex w-full grow flex-col">
<BreadCrumb <BackButton
previousRoute="/a/settings" path="/a/settings"
previousBreadcrumb="Settings" label="Back to settings"
currentPage="The Team" className="mb-xsmall"
/> />
<BodyCard <BodyCard
title="The Team" title="The Team"
subtitle="Manage users of your Medusa Store" subtitle="Manage users of your Medusa Store"
actionables={actionables} actionables={actionables}
> >
<div className="flex grow flex-col pt-2"> <div className="flex grow flex-col justify-between">
<UserTable <UserTable
users={users} users={users}
invites={invites} invites={invites}
triggerRefetch={triggerRefetch} triggerRefetch={triggerRefetch}
/> />
<p className="inter-small-regular text-grey-50">
{users.length} member
{users.length === 1 ? "" : "s"}
</p>
</div> </div>
<div className="inter-small-regular text-grey-50">
{users.length} member
{users.length === 1 ? "" : "s"}
</div>
{showInviteModal && ( {showInviteModal && (
<InviteModal <InviteModal
handleClose={() => { handleClose={() => {
@@ -48,6 +48,10 @@ const FormValidator = {
`${name} must be less than or equal to ${getNormalizedAmount(currency)}.` `${name} must be less than or equal to ${getNormalizedAmount(currency)}.`
) )
}, },
email: (name: string) => ({
value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
message: `${name} must be a valid email address.`,
}),
} }
/** /**
@@ -1,6 +1,7 @@
import { import {
AdminGiftCardsDeleteRes, AdminGiftCardsDeleteRes,
AdminGiftCardsRes, AdminGiftCardsRes,
AdminPostGiftCardsGiftCardReq,
AdminPostGiftCardsReq, AdminPostGiftCardsReq,
} from "@medusajs/medusa" } from "@medusajs/medusa"
import { Response } from "@medusajs/medusa-js" import { Response } from "@medusajs/medusa-js"
@@ -33,14 +34,14 @@ export const useAdminUpdateGiftCard = (
options?: UseMutationOptions< options?: UseMutationOptions<
Response<AdminGiftCardsRes>, Response<AdminGiftCardsRes>,
Error, Error,
AdminPostGiftCardsReq AdminPostGiftCardsGiftCardReq
> >
) => { ) => {
const { client } = useMedusa() const { client } = useMedusa()
const queryClient = useQueryClient() const queryClient = useQueryClient()
return useMutation( return useMutation(
(payload: AdminPostGiftCardsReq) => (payload: AdminPostGiftCardsGiftCardReq) =>
client.admin.giftCards.update(id, payload), client.admin.giftCards.update(id, payload),
buildOptions( buildOptions(
queryClient, queryClient,
@@ -1,10 +1,10 @@
import { IsBoolean, IsDate, IsInt, IsOptional, IsString } from "class-validator" import { IsBoolean, IsDate, IsInt, IsOptional, IsString } from "class-validator"
import { defaultAdminGiftCardFields, defaultAdminGiftCardRelations } from "." import { defaultAdminGiftCardFields, defaultAdminGiftCardRelations } from "."
import { GiftCardService } from "../../../../services"
import { Type } from "class-transformer" import { Type } from "class-transformer"
import { validator } from "../../../../utils/validator"
import { EntityManager } from "typeorm" import { EntityManager } from "typeorm"
import { GiftCardService } from "../../../../services"
import { validator } from "../../../../utils/validator"
/** /**
* @oas [post] /admin/gift-cards/{id} * @oas [post] /admin/gift-cards/{id}
@@ -123,7 +123,7 @@ export class AdminPostGiftCardsGiftCardReq {
@IsOptional() @IsOptional()
@IsDate() @IsDate()
@Type(() => Date) @Type(() => Date)
ends_at?: Date ends_at?: Date | null
@IsOptional() @IsOptional()
@IsString() @IsString()
+1 -1
View File
@@ -11,7 +11,7 @@ export type CreateGiftCardInput = {
export type UpdateGiftCardInput = { export type UpdateGiftCardInput = {
balance?: number balance?: number
ends_at?: Date ends_at?: Date | null
is_disabled?: boolean is_disabled?: boolean
region_id?: string region_id?: string
metadata?: Record<string, unknown> metadata?: Record<string, unknown>