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
2023-04-02 19:04:32 +02:00
committed by GitHub
parent a5ad6c0542
commit 788ddc0f43
51 changed files with 997 additions and 709 deletions

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.

View File

@@ -60,6 +60,7 @@ const DatePicker: React.FC<DateTimePickerProps> = ({
"shadow-input border-violet-60": isOpen,
"border-grey-20": !isOpen,
})}
type="button"
>
<InputContainer className="shadown-none border-0 focus-within:shadow-none">
<div className="text-grey-50 flex w-full justify-between pr-0.5">

View File

@@ -53,6 +53,7 @@ const TimePicker: React.FC<DateTimePickerProps> = ({
"shadow-input border-violet-60": isOpen,
"border-grey-20": !isOpen,
})}
type="button"
>
<InputContainer className="shadown-none border-0 focus-within:shadow-none">
<div className="text-grey-50 flex w-full justify-between pr-0.5">

View File

@@ -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 })}
</button>
</div>

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -1,22 +1,23 @@
import React from "react"
import React, { PropsWithChildren } from "react"
import Actionables, { ActionType } from "../../molecules/actionables"
type BannerCardProps = {
type BannerCardProps = PropsWithChildren<{
actions?: ActionType[]
title: string
thumbnail: string | null
} & React.RefAttributes<HTMLDivElement>
thumbnail?: string | null
}> &
React.RefAttributes<HTMLDivElement>
type BannerCardDescriptionProps = {
type BannerCardDescriptionProps = PropsWithChildren<{
cta?: {
label: string
onClick: (e: React.MouseEvent<HTMLButtonElement>) => void
}
}
}>
const BannerCard: React.FC<BannerCardProps> & {
Description: React.FC<BannerCardDescriptionProps>
Footer: React.FC
Footer: React.FC<PropsWithChildren>
} = ({ title, thumbnail, actions, children }) => {
return (
<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>
}

View File

@@ -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

View File

@@ -64,7 +64,7 @@ const BodyCard: React.FC<BodyCardProps> = ({
"border-grey-20 border-b border-solid": setBorders,
})}
>
<div className="flex items-center justify-between">
<div className="flex items-start justify-between">
<div>
{customHeader ? (
<div>{customHeader}</div>
@@ -112,8 +112,8 @@ const BodyCard: React.FC<BodyCardProps> = ({
<Button
key={i}
onClick={event.onClick}
className="first:ml-xsmall min-w-[130px] justify-center"
variant={i === 0 ? "primary" : "ghost"}
className="first:ml-xsmall justify-center"
variant={i === 0 ? "primary" : "secondary"}
size={"small"}
type={event.type}
>

View File

@@ -1,9 +1,9 @@
import React, { useState } from "react"
import useNotification from "../../hooks/use-notification"
import { getErrorMessage } from "../../utils/error-messages"
import Button from "../fundamentals/button"
import Modal from "../molecules/modal"
import { getErrorMessage } from "../../utils/error-messages"
import useNotification from "../../hooks/use-notification"
type DeletePromptProps = {
heading?: string
@@ -54,10 +54,10 @@ const DeletePrompt: React.FC<DeletePromptProps> = ({
</div>
</Modal.Content>
<Modal.Footer>
<div className="flex h-8 w-full justify-end">
<div className="gap-x-xsmall flex h-8 w-full justify-end">
<Button
variant="ghost"
className="text-small min-w-24 mr-2 justify-center"
variant="secondary"
className="justify-center"
size="small"
onClick={handleClose}
>
@@ -66,7 +66,7 @@ const DeletePrompt: React.FC<DeletePromptProps> = ({
<Button
loading={isLoading}
size="small"
className="text-small w-24 justify-center"
className="justify-center"
variant="nuclear"
onClick={handleSubmit}
disabled={isLoading}

View File

@@ -1,5 +1,4 @@
import React, { useMemo } from "react"
import { currencies } from "../../../utils/currencies"
import { normalizeAmount } from "../../../utils/prices"
import EditIcon from "../../fundamentals/icons/edit-icon"
import TrashIcon from "../../fundamentals/icons/trash-icon"
@@ -75,7 +74,7 @@ const GiftCardBanner: React.FC<GiftCardBannerProps> = ({
)} ${defaultCurrency.toUpperCase()}`
})
.filter(Boolean)
}, [variants, defaultCurrency, currencies])
}, [variants, defaultCurrency])
return (
<BannerCard title={title} thumbnail={thumbnail} actions={actions}>

View File

@@ -7,7 +7,7 @@ import { getErrorMessage } from "../../../utils/error-messages"
import Button from "../../fundamentals/button"
import InputField from "../../molecules/input"
import Modal from "../../molecules/modal"
import Select from "../../molecules/select"
import { NextSelect } from "../../molecules/select/next-select"
type InviteModalProps = {
handleClose: () => void
@@ -68,10 +68,13 @@ const InviteModal: React.FC<InviteModalProps> = ({ handleClose }) => {
name="role"
control={control}
defaultValue={{ label: "Member", value: "member" }}
render={({ field: { value, onChange } }) => {
render={({ field: { value, onChange, onBlur, ref } }) => {
return (
<Select
<NextSelect
label="Role"
placeholder="Select role"
onBlur={onBlur}
ref={ref}
onChange={onChange}
options={roleOptions}
value={value}

View File

@@ -27,7 +27,7 @@ function RawJSON(props: RawJSONProps) {
}
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">
<JSONView data={data} />
</div>

View File

@@ -1,5 +1,6 @@
import React from "react"
import PageDescription from "../atoms/page-description"
import Spacer from "../atoms/spacer"
const SettingsOverview: React.FC<React.PropsWithChildren> = ({ children }) => {
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">
{children}
</div>
<Spacer />
</div>
)
}

View File

@@ -38,7 +38,7 @@ const TwoSplitPane: React.FC<TwoSplitPaneProps> = ({
{Children.map(children, (child, i) => {
return (
<div
className={clsx("h-full w-full", {
className={clsx("w-full", {
"col-span-2": threeCols && i === 1,
})}
key={i}

View File

@@ -107,7 +107,7 @@ const CollectionDetails = () => {
return (
<>
<div className="!pb-xlarge flex flex-col">
<div className="flex flex-col">
<BackButton
className="mb-xsmall"
path="/a/products?view=collections"

View File

@@ -45,7 +45,7 @@ const CustomerDetail = () => {
path="/a/customers"
className="mb-xsmall"
/>
<div className="gap-y-small flex flex-col">
<div className="gap-y-xsmall flex flex-col">
<Section>
<div className="flex w-full items-start justify-between">
<div className="gap-x-base flex w-full items-center">

View File

@@ -1,4 +1,5 @@
import { Route, Routes } from "react-router-dom"
import Spacer from "../../components/atoms/spacer"
import BodyCard from "../../components/organisms/body-card"
import CustomerTable from "../../components/templates/customer-table"
import Details from "./details"
@@ -7,15 +8,14 @@ import CustomersPageTableHeader from "./header"
const CustomerIndex = () => {
return (
<div className="flex h-full grow flex-col">
<div className="flex w-full grow flex-col">
<BodyCard
customHeader={<CustomersPageTableHeader activeView="customers" />}
className="h-fit"
>
<CustomerTable />
</BodyCard>
</div>
<div>
<BodyCard
customHeader={<CustomersPageTableHeader activeView="customers" />}
className="h-fit"
>
<CustomerTable />
</BodyCard>
<Spacer />
</div>
)
}

View File

@@ -22,54 +22,56 @@ const DiscountDetailsConditions: React.FC<DiscountDetailsConditionsProps> = ({
return (
<ConditionsProvider discount={discount}>
<BodyCard
title="Conditions"
className="min-h-[200px]"
forceDropdown
actionables={[
{
label: "Add condition",
icon: <PlusIcon size={16} />,
onClick: () => setShow(true),
},
]}
>
{conditions.length ? (
<div
style={{
gridTemplateRows: `repeat(${Math.ceil(
conditions?.length / 2
)}, minmax(0, 1fr))`,
}}
className="gap-y-base gap-x-xlarge grid grid-flow-col grid-cols-2"
>
{conditions.map((condition, i) => (
<NumberedItem
key={i}
title={condition.title}
index={i + 1}
description={condition.description}
actions={condition.actions}
/>
))}
</div>
) : (
<div className="gap-y-small flex flex-1 flex-col items-center justify-center">
<span className="inter-base-regular text-grey-50">
This discount has no conditions
</span>
</div>
<div>
<BodyCard
title="Conditions"
className="min-h-[200px]"
forceDropdown
actionables={[
{
label: "Add condition",
icon: <PlusIcon size={16} />,
onClick: () => setShow(true),
},
]}
>
{conditions.length ? (
<div
style={{
gridTemplateRows: `repeat(${Math.ceil(
conditions?.length / 2
)}, minmax(0, 1fr))`,
}}
className="gap-y-base gap-x-xlarge grid grid-flow-col grid-cols-2"
>
{conditions.map((condition, i) => (
<NumberedItem
key={i}
title={condition.title}
index={i + 1}
description={condition.description}
actions={condition.actions}
/>
))}
</div>
) : (
<div className="gap-y-small flex flex-1 flex-col items-center justify-center">
<span className="inter-base-regular text-grey-50">
This discount has no conditions
</span>
</div>
)}
</BodyCard>
<AddCondition show={show} onClose={() => setShow(false)} />
{selectedCondition && (
<EditConditionsModal
open={!!selectedCondition}
condition={selectedCondition}
discount={discount}
onClose={() => deSelectCondition()}
/>
)}
</BodyCard>
<AddCondition show={show} onClose={() => setShow(false)} />
{selectedCondition && (
<EditConditionsModal
open={!!selectedCondition}
condition={selectedCondition}
discount={discount}
onClose={() => deSelectCondition()}
/>
)}
</div>
</ConditionsProvider>
)
}

View File

@@ -1,8 +1,8 @@
import { useAdminDeleteDiscount, useAdminDiscount } from "medusa-react"
import { useState } from "react"
import { useParams } from "react-router-dom"
import BackButton from "../../../components/atoms/back-button"
import Spinner from "../../../components/atoms/spinner"
import Breadcrumb from "../../../components/molecules/breadcrumb"
import DeletePrompt from "../../../components/organisms/delete-prompt"
import RawJSON from "../../../components/organisms/raw-json"
import useNotification from "../../../hooks/use-notification"
@@ -50,22 +50,22 @@ const Edit = () => {
/>
)}
<Breadcrumb
currentPage="Add Discount"
previousBreadcrumb="Discount"
previousRoute="/a/discounts"
<BackButton
label="Back to Discounts"
path="/a/discounts"
className="mb-xsmall"
/>
{isLoading || !discount ? (
<div className="flex h-full items-center justify-center">
<Spinner variant="secondary" />
</div>
) : (
<div className="flex flex-col gap-y-4">
<div className="gap-y-xsmall flex flex-col">
<DiscountFormProvider>
<General discount={discount} />
<Configurations discount={discount} />
<DiscountDetailsConditions discount={discount} />
<RawJSON data={discount} title="Raw discount" rootName="discount" />
<RawJSON data={discount} title="Raw discount" />
</DiscountFormProvider>
</div>
)}

View File

@@ -1,6 +1,7 @@
import { useState } from "react"
import { Route, Routes } from "react-router-dom"
import Fade from "../../components/atoms/fade-wrapper"
import Spacer from "../../components/atoms/spacer"
import PlusIcon from "../../components/fundamentals/icons/plus-icon"
import BodyCard from "../../components/organisms/body-card"
import TableViewHeader from "../../components/organisms/custom-table-header"
@@ -31,6 +32,7 @@ const DiscountIndex = () => {
>
<DiscountTable />
</BodyCard>
<Spacer />
</div>
<DiscountFormProvider>
<Fade isVisible={isOpen} isFullScreen={true}>

View File

@@ -1,161 +1,136 @@
import { useAdminCreateGiftCard, useAdminRegions } from "medusa-react"
import React, { useEffect, useState } from "react"
import { useForm } from "react-hook-form"
import { useAdminCreateGiftCard } from "medusa-react"
import React, { useEffect } from "react"
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 InputField from "../../components/molecules/input"
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 { getErrorMessage } from "../../utils/error-messages"
import { focusByName } from "../../utils/focus-by-name"
import { validateEmail } from "../../utils/validate-email"
import { nestedForm } from "../../utils/nested-form"
type CustomGiftcardProps = {
onDismiss: () => void
onClose: () => void
open: boolean
}
const CustomGiftcard: React.FC<CustomGiftcardProps> = ({ onDismiss }) => {
const { isLoading, regions } = useAdminRegions()
const [selectedRegion, setSelectedRegion] = useState<any>(null)
const [giftCardAmount, setGiftCardAmount] = useState(0)
type CustomGiftCardFormType = {
region: GiftCardRegionFormType
ends_at: GiftCardEndsAtFormType
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 { mutate, isLoading: isSubmitting } = useAdminCreateGiftCard()
useEffect(() => {
if (!isLoading) {
setSelectedRegion({
value: regions[0],
label: regions[0].name,
})
if (open) {
reset()
}
}, [isLoading])
}, [open, reset])
const onSubmit = (data) => {
if (!giftCardAmount) {
notification("Error", "Please enter an amount", "error")
focusByName("amount")
return
}
if (!validateEmail(data.metadata.email)) {
notification("Error", "Invalid email address", "error")
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()
const onSubmit = handleSubmit((data) => {
mutate(
{
region_id: data.region.region_id.value,
value: data.balance.amount,
ends_at: data.ends_at.ends_at || undefined,
metadata: {
email: data.receiver.email,
personal_message: data.receiver.message,
},
},
onError: (error) => {
notification("Error", getErrorMessage(error), "error")
onDismiss()
},
})
}
{
onSuccess: () => {
notification(
"Created gift card",
"Custom gift card was created successfully",
"success"
)
onClose()
},
onError: (error) => {
notification("Error", getErrorMessage(error), "error")
},
}
)
})
return (
<Modal handleClose={onDismiss}>
<Modal open={open} handleClose={onClose}>
<Modal.Body>
<Modal.Header handleClose={onDismiss}>
<Modal.Header handleClose={onClose}>
<h2 className="inter-xlarge-semibold">Custom Gift Card</h2>
</Modal.Header>
<Modal.Content>
<div className="flex flex-col">
<span className="inter-base-semibold">Value</span>
<div className="gap-x-2xsmall mt-4 flex">
<div className="w-[267px]">
<Select
label={"Region"}
value={selectedRegion}
onChange={(value) => setSelectedRegion(value)}
options={
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}
<form onSubmit={onSubmit}>
<Modal.Content>
<div className="gap-y-xlarge flex flex-col">
<div>
<h2 className="inter-base-semibold mb-base">Details</h2>
<div className="gap-x-xsmall grid grid-cols-2">
<GiftCardRegionForm form={nestedForm(form, "region")} />
<GiftCardBalanceForm
form={nestedForm(form, "balance")}
currencyCode={currencySubscriber}
/>
</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 className="mt-8">
<span className="inter-base-semibold">Receiver</span>
<div className="gap-y-xsmall mt-4 grid grid-cols-1">
<InputField
label={"Email"}
required
{...register("metadata.email", { required: true })}
placeholder="lebron@james.com"
type="email"
/>
<TextArea
label={"Personal Message"}
rows={7}
placeholder="Something nice to someone special"
{...register("metadata.personal_message")}
/>
</Modal.Content>
<Modal.Footer>
<div className="gap-x-xsmall flex w-full justify-end">
<Button
variant="secondary"
onClick={onClose}
size="small"
type="button"
>
Cancel
</Button>
<Button
variant="primary"
type="submit"
size="small"
disabled={isSubmitting || !isDirty}
loading={isSubmitting}
>
Create and send
</Button>
</div>
</div>
</Modal.Content>
<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.Footer>
</form>
</Modal.Body>
</Modal>
)

View File

@@ -1,111 +1,137 @@
import { AdminPostGiftCardsGiftCardReq, Region } from "@medusajs/medusa"
import { useMemo } from "react"
import { Controller, useForm } from "react-hook-form"
import { GiftCard } from "@medusajs/medusa"
import { useAdminUpdateGiftCard } from "medusa-react"
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 Modal from "../../../components/molecules/modal"
import Select from "../../../components/molecules/select"
import { Option } from "../../../types/shared"
import useNotification from "../../../hooks/use-notification"
import { getErrorMessage } from "../../../utils/error-messages"
import { nestedForm } from "../../../utils/nested-form"
type EditGiftCardModalProps = {
handleClose: () => void
handleSave: (update: AdminPostGiftCardsGiftCardReq) => void
updating: boolean
regions: Region[] | undefined
region: Region
onClose: () => void
open: boolean
giftCard: GiftCard
}
type EditGiftCardModalFormData = {
region: Option
type EditGiftCardFormType = {
region: GiftCardRegionFormType
ends_at: GiftCardEndsAtFormType
}
const EditGiftCardModal = ({
handleClose,
handleSave,
updating,
regions,
region,
open,
onClose,
giftCard,
}: EditGiftCardModalProps) => {
const { control, handleSubmit } = useForm<EditGiftCardModalFormData>({
defaultValues: {
region: {
value: region.id,
label: region.name,
const form = useForm<EditGiftCardFormType>({
defaultValues: getDefaultValues(giftCard),
})
const {
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) => {
handleSave({ region_id: data.region.value })
}
const regionOptions: Option[] = useMemo(() => {
return (
regions?.map((r) => ({
label: r.name,
value: r.id,
})) || []
)
}, [regions])
useEffect(() => {
if (open) {
reset(getDefaultValues(giftCard))
}
}, [open, reset, giftCard])
return (
<Modal handleClose={handleClose} isLargeModal={true}>
<form onSubmit={handleSubmit(onSubmit)}>
<Modal.Body isLargeModal={true}>
<Modal.Header handleClose={handleClose}>
<span className="inter-xlarge-semibold">
Edit Gift Card Details
</span>
</Modal.Header>
<Modal open={open} handleClose={onClose}>
<Modal.Body>
<Modal.Header handleClose={onClose}>
<h1 className="inter-xlarge-semibold">Edit Gift Card</h1>
</Modal.Header>
<form onSubmit={onSubmit}>
<Modal.Content>
{/* TODO: Missing backend support for updating code
<InputField
label="Code"
name="code"
value={code}
onChange={({ currentTarget }) => setCode(currentTarget.value)}
className="mb-4"
/> */}
<Controller
control={control}
name="region"
render={({ field: { value, onChange } }) => {
return (
<Select
label="Region"
options={regionOptions}
value={value}
onChange={onChange}
/>
)
}}
/>
<div className="gap-y-xlarge flex flex-col">
<div>
<h2 className="inter-base-semibold mb-base">Details</h2>
<GiftCardRegionForm form={nestedForm(form, "region")} />
</div>
<GiftCardEndsAtForm form={nestedForm(form, "ends_at")} />
</div>
</Modal.Content>
<Modal.Footer>
<div className="flex w-full justify-end">
<div className="gap-x-xsmall flex w-full justify-end">
<Button
variant="ghost"
variant="secondary"
size="small"
onClick={handleClose}
className="mr-2"
onClick={onClose}
type="button"
>
Cancel
</Button>
<Button
loading={updating}
disabled={updating}
variant="primary"
className="min-w-[100px]"
size="small"
type="submit"
disabled={isLoading || !isDirty}
loading={isLoading}
>
Save
Save and close
</Button>
</div>
</Modal.Footer>
</Modal.Body>
</form>
</form>
</Modal.Body>
</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

View File

@@ -1,23 +1,15 @@
import { AdminPostGiftCardsGiftCardReq } from "@medusajs/medusa"
import {
useAdminGiftCard,
useAdminRegions,
useAdminUpdateGiftCard,
} from "medusa-react"
import { useAdminGiftCard, useAdminUpdateGiftCard } from "medusa-react"
import moment from "moment"
import { useState } from "react"
import { useParams } from "react-router-dom"
import BackButton from "../../../components/atoms/back-button"
import Spinner from "../../../components/atoms/spinner"
import Badge from "../../../components/fundamentals/badge"
import DollarSignIcon from "../../../components/fundamentals/icons/dollar-sign-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 BodyCard from "../../../components/organisms/body-card"
import RawJSON from "../../../components/organisms/raw-json"
import useNotification from "../../../hooks/use-notification"
import useToggleState from "../../../hooks/use-toggle-state"
import { getErrorMessage } from "../../../utils/error-messages"
import { formatAmountWithSymbol } from "../../../utils/prices"
import EditGiftCardModal from "./edit-gift-card-modal"
@@ -29,45 +21,46 @@ const GiftCardDetails = () => {
const { gift_card: giftCard, isLoading } = useAdminGiftCard(id!, {
enabled: !!id,
})
const { regions } = useAdminRegions()
const updateGiftCard = useAdminUpdateGiftCard(giftCard?.id!)
const notification = useNotification()
const [showUpdateBalance, setShowUpdateBalance] = useState(false)
const [showEdit, setShowEdit] = useState(false)
const {
state: editState,
open: openEdit,
close: closeEdit,
} = useToggleState()
const {
state: balanceState,
open: openBalance,
close: closeBalance,
} = useToggleState()
const actions = [
{
label: "Edit",
onClick: () => setShowEdit(true),
label: "Edit details",
onClick: openEdit,
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",
onClick: () => setShowUpdateBalance(true),
onClick: openBalance,
icon: <DollarSignIcon size={20} />,
},
]
const handleUpdate = (data: AdminPostGiftCardsGiftCardReq) => {
const updateStatus = (data: { is_disabled?: boolean }) => {
updateGiftCard.mutate(
{ ...data },
{ is_disabled: data.is_disabled },
{
onSuccess: () => {
notification("Success", "Succesfully updated Gift Card", "success")
setShowEdit(false)
setShowUpdateBalance(false)
notification(
"Updated status",
"Succesfully updated the status of the Gift Card",
"success"
)
},
onError: (err) => notification("Error", getErrorMessage(err), "error"),
}
@@ -76,10 +69,10 @@ const GiftCardDetails = () => {
return (
<div>
<Breadcrumb
currentPage={"Gift Card Details"}
previousBreadcrumb={"Gift Cards"}
previousRoute="/a/gift-cards"
<BackButton
label="Back to Gift Cards"
path="/a/gift-cards"
className="mb-xsmall"
/>
{isLoading || !giftCard ? (
<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>
) : (
<>
<BodyCard
className={"h-auto min-h-0 w-full"}
title={`${giftCard?.code}`}
subtitle={`Gift Card id: ${giftCard?.id}`}
status={
<StatusSelector
isDraft={!!giftCard?.is_disabled}
activeState={"Active"}
draftState={"Disable"}
onChange={() =>
handleUpdate({ is_disabled: !giftCard?.is_disabled })
}
/>
}
actionables={actions}
>
<div className="flex justify-between">
<div className="mt-6 flex space-x-6 divide-x">
<div className="flex flex-col">
<div className="inter-smaller-regular text-grey-50 mb-1">
Original amount
<div className="gap-y-xsmall flex flex-col">
<BodyCard
className={"h-auto min-h-0 w-full"}
title={`${giftCard?.code}`}
status={
<StatusSelector
isDraft={!!giftCard?.is_disabled}
activeState={"Active"}
draftState={"Disable"}
onChange={() =>
updateStatus({ is_disabled: !giftCard.is_disabled })
}
/>
}
actionables={actions}
>
<div className="flex justify-between">
<div className="flex space-x-6 divide-x">
<div className="flex flex-col">
<div className="inter-smaller-regular text-grey-50 mb-1">
Original amount
</div>
<div>
{formatAmountWithSymbol({
amount: giftCard.value,
currency: giftCard.region.currency_code,
})}
</div>
</div>
<div>
{formatAmountWithSymbol({
amount: giftCard?.value,
currency: giftCard?.region.currency_code,
})}
<div className="flex flex-col pl-6">
<div className="inter-smaller-regular text-grey-50 mb-1">
Balance
</div>
<div>
{formatAmountWithSymbol({
amount: giftCard.balance,
currency: giftCard.region.currency_code,
})}
</div>
</div>
</div>
<div className="flex flex-col pl-6">
<div className="inter-smaller-regular text-grey-50 mb-1">
Balance
<div className="flex flex-col pl-6">
<div className="inter-smaller-regular text-grey-50 mb-1">
Region
</div>
<div>{giftCard.region.name}</div>
</div>
<div>
{formatAmountWithSymbol({
amount: giftCard?.balance,
currency: giftCard?.region.currency_code,
})}
</div>
</div>
<div className="flex flex-col pl-6">
<div className="inter-smaller-regular text-grey-50 mb-1">
Created
</div>
<div>
{moment(giftCard?.created_at).format("DD MMM YYYY")}
{giftCard.ends_at && (
<div className="flex flex-col pl-6">
<div className="inter-smaller-regular text-grey-50 mb-1">
Expires on
</div>
<div>
{moment(giftCard.ends_at).format("DD MMM YYYY")}
</div>
</div>
)}
<div className="flex flex-col pl-6">
<div className="inter-smaller-regular text-grey-50 mb-1">
Created
</div>
<div>
{moment(giftCard.created_at).format("DD MMM YYYY")}
</div>
</div>
</div>
</div>
<div className="flex items-end">
<Badge variant="default">{giftCard?.region?.name}</Badge>
</div>
</div>
</BodyCard>
<div className="mt-large">
<RawJSON data={giftCard} title="Raw gift card" rootName="product" />
</BodyCard>
<RawJSON data={giftCard} title="Raw gift card" />
</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>
)
}

View File

@@ -1,109 +1,109 @@
import { AdminPostGiftCardsGiftCardReq, GiftCard } from "@medusajs/medusa"
import clsx from "clsx"
import { Controller, useForm, useWatch } from "react-hook-form"
import Tooltip from "../../../components/atoms/tooltip"
import { GiftCard } from "@medusajs/medusa"
import { useAdminUpdateGiftCard } from "medusa-react"
import { useEffect } from "react"
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 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 = {
handleClose: () => void
handleSave: (update: AdminPostGiftCardsGiftCardReq) => void
currencyCode: string
open: boolean
onClose: () => void
giftCard: GiftCard
updating: boolean
}
type UpdateBalanceModalFormData = {
balance: number
balance: GiftCardBalanceFormType
}
const UpdateBalanceModal = ({
handleClose,
handleSave,
currencyCode,
open,
onClose,
giftCard,
updating,
}: UpdateBalanceModalProps) => {
const { control, handleSubmit } = useForm<UpdateBalanceModalFormData>({
defaultValues: {
balance: giftCard.balance,
},
const form = useForm<UpdateBalanceModalFormData>({
defaultValues: getDefaultValues(giftCard),
})
const balance = useWatch({
control,
name: "balance",
const {
handleSubmit,
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 (
<Modal handleClose={handleClose}>
<Modal open={open} handleClose={onClose}>
<Modal.Body>
<form onSubmit={handleSubmit(handleSave)}>
<Modal.Header handleClose={handleClose}>
<span className="inter-xlarge-semibold">Update Balance</span>
<span
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.Header handleClose={onClose}>
<h1 className="inter-xlarge-semibold">Update Balance</h1>
</Modal.Header>
<form onSubmit={onSubmit}>
<Modal.Content>
<CurrencyInput.Root
readOnly
currentCurrency={currencyCode}
size="small"
>
<Controller
control={control}
name="balance"
rules={{
required: true,
}}
render={({ field: { value, onChange } }) => {
return (
<CurrencyInput.Amount
amount={value}
label="Price"
onChange={onChange}
/>
)
}}
/>
</CurrencyInput.Root>
<GiftCardBalanceForm
form={nestedForm(form, "balance")}
currencyCode={giftCard.region.currency_code}
originalAmount={giftCard.value}
/>
</Modal.Content>
<Modal.Footer>
<div className="flex w-full justify-end">
<div className="gap-x-xsmall flex w-full justify-end">
<Button
variant="ghost"
variant="secondary"
size="small"
onClick={handleClose}
className="mr-2"
onClick={onClose}
type="button"
>
Cancel
</Button>
<Button
loading={updating}
variant="primary"
className="min-w-[100px]"
size="small"
type="submit"
disabled={balance > giftCard.value || updating}
loading={isLoading}
disabled={isLoading || !isDirty}
>
{balance > giftCard.value ? (
<Tooltip content="Balance is above original value">
Save
</Tooltip>
) : (
"Save"
)}
Save and close
</Button>
</div>
</Modal.Footer>
@@ -112,4 +112,13 @@ const UpdateBalanceModal = ({
</Modal>
)
}
const getDefaultValues = (giftCard: GiftCard): UpdateBalanceModalFormData => {
return {
balance: {
amount: giftCard.balance,
},
}
}
export default UpdateBalanceModal

View File

@@ -1,3 +1,4 @@
import { Product } from "@medusajs/medusa"
import {
useAdminDeleteProduct,
useAdminProducts,
@@ -7,6 +8,7 @@ import {
import { useMemo, useState } from "react"
import { useNavigate } from "react-router-dom"
import PageDescription from "../../components/atoms/page-description"
import Spacer from "../../components/atoms/spacer"
import Spinner from "../../components/atoms/spinner"
import PlusIcon from "../../components/fundamentals/icons/plus-icon"
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 GiftCardTable from "../../components/templates/gift-card-table"
import useNotification from "../../hooks/use-notification"
import useToggleState from "../../hooks/use-toggle-state"
import { ProductStatus } from "../../types/shared"
import { getErrorMessage } from "../../utils/error-messages"
import CustomGiftcard from "./custom-giftcard"
@@ -26,8 +29,6 @@ const Overview = () => {
})
const { store } = useAdminStore()
const [showCreate, setShowCreate] = useState(false)
const [showCreateCustom, setShowCreateCustom] = useState(false)
const [showDelete, setShowDelete] = useState(false)
const giftCard = products?.[0]
@@ -36,6 +37,20 @@ const Overview = () => {
const updateGiftCard = useAdminUpdateProduct(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 = () => {
let status: ProductStatus = ProductStatus.PUBLISHED
if (giftCard?.status === "published") {
@@ -43,7 +58,6 @@ const Overview = () => {
}
updateGiftCard.mutate(
// @ts-ignore
{ status },
{
onSuccess: () =>
@@ -64,7 +78,7 @@ const Overview = () => {
const actionables = [
{
label: "Custom Gift Card",
onClick: () => setShowCreateCustom(true),
onClick: openCustom,
icon: <PlusIcon size={20} />,
},
]
@@ -74,69 +88,70 @@ const Overview = () => {
return null
}
return { ...giftCard, defaultCurrency: store.default_currency_code }
return {
...(giftCard as Product),
defaultCurrency: store.default_currency_code,
}
}, [giftCard, store])
return (
<>
<div className="pb-xlarge flex h-full grow flex-col">
<div className="flex flex-col">
<PageDescription
title="Gift Cards"
subtitle="Manage the Gift Cards of your Medusa store"
/>
{!isLoading ? (
<>
<div className="mb-base">
{giftCardWithCurrency ? (
<GiftCardBanner
{...giftCardWithCurrency}
onDelete={() => setShowDelete(true)}
onEdit={() => navigate("/a/gift-cards/manage")}
onUnpublish={onUpdate}
/>
) : (
<BannerCard title="Are you ready to sell your first Gift Card?">
<BannerCard.Description
cta={{
label: "Create Gift Card",
onClick: () => setShowCreate(true),
}}
>
No Gift Card has been added yet.
</BannerCard.Description>
</BannerCard>
)}
</div>
<div className="flex w-full grow flex-col">
<BodyCard
title="History"
subtitle="See the history of purchased Gift Cards"
actionables={actionables}
className="h-fit"
>
<GiftCardTable />
</BodyCard>
</div>
</>
<div className="gap-y-xsmall flex flex-col">
{giftCardWithCurrency ? (
<GiftCardBanner
{...giftCardWithCurrency}
onDelete={openDelete}
onEdit={() => navigate("/a/gift-cards/manage")}
onUnpublish={onUpdate}
/>
) : (
<BannerCard title="Are you ready to sell your first Gift Card?">
<BannerCard.Description
cta={{
label: "Create Gift Card",
onClick: () => setShowCreate(true),
}}
>
No Gift Card has been added yet.
</BannerCard.Description>
</BannerCard>
)}
<BodyCard
title="History"
subtitle="See the history of purchased Gift Cards"
actionables={actionables}
>
<GiftCardTable />
</BodyCard>
</div>
) : (
<div className="rounded-rounded border-grey-20 flex h-44 w-full items-center justify-center border">
<Spinner variant="secondary" size="large" />
</div>
)}
</div>
{showCreateCustom && (
<CustomGiftcard onDismiss={() => setShowCreateCustom(false)} />
)}
<CustomGiftcard onClose={closeCustom} open={stateCustom} />
{showCreate && <NewGiftCard onClose={() => setShowCreate(!showCreate)} />}
{showDelete && (
{stateDelete && (
<DeletePrompt
handleClose={() => setShowDelete(!showDelete)}
handleClose={closeDelete}
onDelete={async () => onDelete()}
successText="Successfully deleted Gift Card"
confirmText="Yes, delete"
heading="Delete Gift Card"
/>
)}
<Spacer />
</>
)
}

View File

@@ -1,3 +1,4 @@
import Spacer from "../../../components/atoms/spacer"
import BodyCard from "../../../components/organisms/body-card"
import InventoryTable from "../../../components/templates/inventory-table"
import InventoryPageTableHeader from "../header"
@@ -12,6 +13,7 @@ const InventoryView = () => {
>
<InventoryTable />
</BodyCard>
<Spacer />
</div>
</div>
)

View File

@@ -30,6 +30,7 @@ import moment from "moment"
import { useEffect, useMemo, useState } from "react"
import { useHotkeys } from "react-hotkeys-hook"
import Avatar from "../../../components/atoms/avatar"
import BackButton from "../../../components/atoms/back-button"
import Spinner from "../../../components/atoms/spinner"
import Tooltip from "../../../components/atoms/tooltip"
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 TruckIcon from "../../../components/fundamentals/icons/truck-icon"
import { ActionType } from "../../../components/molecules/actionables"
import Breadcrumb from "../../../components/molecules/breadcrumb"
import JSONView from "../../../components/molecules/json-view"
import BodyCard from "../../../components/organisms/body-card"
import RawJSON from "../../../components/organisms/raw-json"
@@ -285,10 +285,10 @@ const OrderDetails = () => {
return (
<div>
<OrderEditProvider orderId={id!}>
<Breadcrumb
currentPage={"Order Details"}
previousBreadcrumb={"Orders"}
previousRoute="/a/orders"
<BackButton
path="/a/orders"
label="Back to Orders"
className="mb-xsmall"
/>
{isLoading || !order ? (
<BodyCard className="pt-2xlarge flex w-full items-center justify-center">

View File

@@ -10,6 +10,7 @@ import moment from "moment"
import { useEffect, useState } from "react"
import { useNavigate, useParams } from "react-router-dom"
import Avatar from "../../../components/atoms/avatar"
import BackButton from "../../../components/atoms/back-button"
import CopyToClipboard from "../../../components/atoms/copy-to-clipboard"
import Spinner from "../../../components/atoms/spinner"
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 ImagePlaceholder from "../../../components/fundamentals/image-placeholder"
import StatusDot from "../../../components/fundamentals/status-indicator"
import Breadcrumb from "../../../components/molecules/breadcrumb"
import JSONView from "../../../components/molecules/json-view"
import BodyCard from "../../../components/organisms/body-card"
import ConfirmationPrompt from "../../../components/organisms/confirmation-prompt"
@@ -125,10 +125,10 @@ const DraftOrderDetails = () => {
return (
<div>
<Breadcrumb
currentPage={"Draft Order Details"}
previousBreadcrumb={"Draft Orders"}
previousRoute="/a/draft-orders"
<BackButton
path="/a/draft-orders"
label="Back to Draft Orders"
className="mb-xsmall"
/>
{isLoading || !draft_order ? (
<BodyCard className="pt-2xlarge flex w-full items-center justify-center">

View File

@@ -1,5 +1,6 @@
import { useMemo, useState } from "react"
import { Route, Routes, useNavigate } from "react-router-dom"
import Spacer from "../../../components/atoms/spacer"
import PlusIcon from "../../../components/fundamentals/icons/plus-icon"
import BodyCard from "../../../components/organisms/body-card"
@@ -47,6 +48,7 @@ const DraftOrderIndex = () => {
>
<DraftOrderTable />
</BodyCard>
<Spacer />
</div>
{showNewOrder && (
<NewOrderFormProvider>

View File

@@ -1,19 +1,20 @@
import { Route, Routes, useNavigate } from "react-router-dom"
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 Details from "./details"
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 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 { usePolling } from "../../providers/polling-provider"
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"]
@@ -37,6 +38,7 @@ const OrderIndex = () => {
const actions = useMemo(() => {
return [
<Button
key="export"
variant="secondary"
size="small"
onClick={() => openExportModal()}
@@ -90,6 +92,7 @@ const OrderIndex = () => {
>
<OrderTable setContextFilters={setContextFilters} />
</BodyCard>
<Spacer />
</div>
</div>
{exportModalOpen && (

View File

@@ -1,6 +1,6 @@
import { useAdminPriceList } from "medusa-react"
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 { mapPriceListToFormValues } from "../pricing-form/form/mappers"
import { PriceListFormProvider } from "../pricing-form/form/pricing-form-context"
@@ -13,25 +13,21 @@ const PricingDetails = () => {
const { price_list, isLoading } = useAdminPriceList(id!)
return (
<div className="pb-xlarge">
<Breadcrumb
currentPage="Edit price list"
previousBreadcrumb="Pricing"
previousRoute="/a/pricing"
<div className="pb-large">
<BackButton
label="Back to Pricing"
path="/a/pricing"
className="mb-xsmall"
/>
{!isLoading && price_list ? (
<PriceListFormProvider priceList={mapPriceListToFormValues(price_list)}>
<Header priceList={price_list} />
<div className="mt-4 w-full">
<div className="gap-y-xsmall flex flex-col">
<Header priceList={price_list} />
<PricesDetails id={price_list?.id} />
</div>
<div className="mt-xlarge">
<RawJSON
data={price_list}
title="Raw price list"
rootName="price_list"
/>
<RawJSON data={price_list} title="Raw price list" />
</div>
</PriceListFormProvider>
) : null}

View File

@@ -1,4 +1,5 @@
import { Route, Routes, useNavigate } from "react-router-dom"
import Spacer from "../../components/atoms/spacer"
import PlusIcon from "../../components/fundamentals/icons/plus-icon"
import BodyCard from "../../components/organisms/body-card"
import TableViewHeader from "../../components/organisms/custom-table-header"
@@ -27,6 +28,7 @@ const PricingIndex = () => {
>
<PricingTable />
</BodyCard>
<Spacer />
</div>
</div>
)

View File

@@ -3,6 +3,7 @@ import { createContext, useState } from "react"
import { ProductCategory } from "@medusajs/medusa"
import { useAdminProductCategories } from "medusa-react"
import Spacer from "../../../components/atoms/spacer"
import BodyCard from "../../../components/organisms/body-card"
import useToggleState from "../../../hooks/use-toggle-state"
import ProductCategoriesList from "../components/product-categories-list"
@@ -99,6 +100,7 @@ function ProductCategoryPage() {
<ProductCategoriesList categories={categories!} />
)}
</BodyCard>
<Spacer />
{isCreateModalVisible && (
<CreateProductCategory
parentCategory={activeCategory}

View File

@@ -1,20 +1,30 @@
import { AdminPostProductsReq, ProductVariant } from "@medusajs/medusa"
import { useAdminCreateProduct, useMedusa } from "medusa-react"
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, {
DimensionsFormType
DimensionsFormType,
} from "../../../components/forms/product/dimensions-form"
import DiscountableForm, {
DiscountableFormType
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 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
AddSalesChannelsFormType,
} from "./add-sales-channels"
import AddVariantsForm, { AddVariantsFormType } from "./add-variants"
@@ -247,8 +257,8 @@ const NewProduct = ({ onClose }: Props) => {
</div>
</div>
</FocusModal.Header>
<FocusModal.Main className="no-scrollbar flex w-full justify-center">
<div className="small:w-4/5 medium:w-7/12 large:w-6/12 my-16 max-w-[700px]">
<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 max-w-[700px]">
<Accordion defaultValue={["general"]} type="multiple">
<Accordion.Item
value={"general"}
@@ -357,14 +367,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 }))
@@ -435,6 +445,7 @@ const createBlank = (): NewProductForm => {
images: [],
},
organize: {
categories: null,
collection: null,
tags: null,
type: null,

View File

@@ -2,6 +2,7 @@ import { useAdminCreateBatchJob, useAdminCreateCollection } from "medusa-react"
import { useEffect, useState } from "react"
import { useLocation, useNavigate } from "react-router-dom"
import Fade from "../../../components/atoms/fade-wrapper"
import Spacer from "../../../components/atoms/spacer"
import Button from "../../../components/fundamentals/button"
import ExportIcon from "../../../components/fundamentals/icons/export-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 useNotification from "../../../hooks/use-notification"
import useToggleState from "../../../hooks/use-toggle-state"
import { usePolling } from "../../../providers/polling-provider"
import { getErrorMessage } from "../../../utils/error-messages"
import ImportProducts from "../batch-job/import"
import NewProduct from "../new"
import { usePolling } from "../../../providers/polling-provider"
const VIEWS = ["products", "collections"]
@@ -178,8 +179,10 @@ const Overview = () => {
>
<CurrentView />
</BodyCard>
<Spacer />
</div>
</div>
{showNewCollection && (
<AddCollectionModal
onClose={() => setShowNewCollection(!showNewCollection)}

View File

@@ -6,21 +6,22 @@ import {
useAdminCreatePublishableApiKey,
} from "medusa-react"
import Breadcrumb from "../../../components/molecules/breadcrumb"
import BodyCard from "../../../components/organisms/body-card"
import FocusModal from "../../../components/molecules/modal/focus-modal"
import BackButton from "../../../components/atoms/back-button"
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 ChannelsIcon from "../../../components/fundamentals/icons/channels-icon"
import CrossIcon from "../../../components/fundamentals/icons/cross-icon"
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 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 ManageSalesChannelsSideModal from "../modals/manage-sales-channels"
import ChannelsIcon from "../../../components/fundamentals/icons/channels-icon"
import SalesChannelsSummary from "../../../components/molecules/sales-channels-summary"
import AddSalesChannelsSideModal from "../modals/add-sales-channels"
import PublishableApiKeysTable from "../tables/publishable-api-keys-table"
type AddSalesChannelsSectionProps = {
setSelectedChannels: (arg: any) => void
@@ -229,10 +230,10 @@ function Index() {
return (
<div>
<Breadcrumb
currentPage="Publishable API Keys"
previousBreadcrumb="Settings"
previousRoute="/a/settings"
<BackButton
label="Back to settings"
path="/a/settings"
className="mb-xsmall"
/>
<BodyCard
title="Publishable API keys"
@@ -256,6 +257,7 @@ function Index() {
close={_closeChannelsModal}
/>
</BodyCard>
<Spacer />
</div>
)
}

View File

@@ -1,5 +1,5 @@
import clsx from "clsx"
import { useEffect, useRef, useState, useMemo } from "react"
import { useEffect, useMemo, useRef, useState } from "react"
import { SalesChannel } from "@medusajs/medusa"
import {
@@ -9,27 +9,27 @@ import {
useAdminUpdateSalesChannel,
} from "medusa-react"
import EditSalesChannel from "../form/edit-sales-channel"
import AddSalesChannelModal from "../form/add-sales-channel"
import { useNavigate, useParams } from "react-router-dom"
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, {
ActionType,
} from "../../../components/molecules/actionables"
import PlusIcon from "../../../components/fundamentals/icons/plus-icon"
import EditIcon from "../../../components/fundamentals/icons/edit-icon"
import TrashIcon from "../../../components/fundamentals/icons/trash-icon"
import SearchIcon from "../../../components/fundamentals/icons/search-icon"
import StatusSelector from "../../../components/molecules/status-selector"
import useImperativeDialog from "../../../hooks/use-imperative-dialog"
import useToggleState from "../../../hooks/use-toggle-state"
import AddSalesChannelModal from "../form/add-sales-channel"
import EditSalesChannel from "../form/edit-sales-channel"
import {
SalesChannelProductsSelectModal,
SalesChannelProductsTable,
} 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 }
@@ -101,10 +101,7 @@ function SalesChannelTile(props: SalesChannelTileProps) {
<h3 className="text-grey-90 mb-1 font-semibold leading-5">
{salesChannel.name}
</h3>
<span
title={salesChannel.description}
className="text-small text-grey-50 "
>
<span className="text-small text-grey-50 ">
{salesChannel.description}
</span>
</div>
@@ -210,7 +207,7 @@ function SalesChannelsList(props: SalesChannelsListProps) {
} = props
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
filterText={filterText}
setFilterText={setFilterText}
@@ -338,7 +335,7 @@ function SalesChannelDetails(props: SalesChannelDetailsProps) {
useToggleState(false)
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
isDefault={isDefault}
resetDetails={resetDetails}
@@ -449,13 +446,13 @@ function Details() {
return (
<div>
<Breadcrumb
currentPage={"Sales channels"}
previousBreadcrumb={"Settings"}
previousRoute="/a/settings"
<BackButton
path="/a/settings"
label="Back to settings"
className="mb-xsmall"
/>
<TwoSplitPane threeCols>
<div className="gap-x-xsmall grid grid-cols-3">
<SalesChannelsList
filterText={filterText}
setFilterText={setFilterText}
@@ -477,7 +474,8 @@ function Details() {
resetDetails={resetDetails}
/>
)}
</TwoSplitPane>
</div>
<Spacer />
<Fade isVisible={showCreateModal} isFullScreen={true}>
<AddSalesChannelModal onClose={closeCreateModal} />

View File

@@ -2,7 +2,7 @@ import { Store } from "@medusajs/medusa"
import { useAdminStore, useAdminUpdateStore } from "medusa-react"
import { useEffect } from "react"
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 BodyCard from "../../components/organisms/body-card"
import useNotification from "../../hooks/use-notification"
@@ -64,10 +64,10 @@ const AccountDetails = () => {
return (
<form className="flex-col py-5">
<div className="max-w-[632px]">
<BreadCrumb
previousRoute="/a/settings/"
previousBreadcrumb="Settings"
currentPage="Store Details"
<BackButton
path="/a/settings/"
label="Back to settings"
className="mb-xsmall"
/>
<BodyCard
events={[
@@ -76,37 +76,41 @@ const AccountDetails = () => {
type: "button",
onClick: handleSubmit(onSubmit),
},
{ label: "Cancel changes", type: "button", onClick: handleCancel },
{ label: "Cancel", type: "button", onClick: handleCancel },
]}
title="Store Details"
subtitle="Manage your business details"
>
<h6 className="mt-large inter-base-semibold">General</h6>
<Input
className="mt-base"
label="Store name"
{...register("name")}
placeholder="Medusa Store"
/>
<h6 className="mt-2xlarge inter-base-semibold">Advanced settings</h6>
<Input
className="mt-base"
label="Swap link template"
{...register("swap_link_template")}
placeholder="https://acme.inc/swap={swap_id}"
/>
<Input
className="mt-base"
label="Draft order link template"
{...register("payment_link_template")}
placeholder="https://acme.inc/payment={payment_id}"
/>
<Input
className="mt-base"
label="Invite link template"
{...register("invite_link_template")}
placeholder="https://acme-admin.inc/invite?token={invite_token}"
/>
<div className="gap-y-xlarge mb-large flex flex-col">
<div>
<h2 className="inter-base-semibold mb-base">General</h2>
<Input
label="Store name"
{...register("name")}
placeholder="Medusa Store"
/>
</div>
<div>
<h2 className="inter-base-semibold mb-base">Advanced settings</h2>
<Input
label="Swap link template"
{...register("swap_link_template")}
placeholder="https://acme.inc/swap={swap_id}"
/>
<Input
className="mt-base"
label="Draft order link template"
{...register("payment_link_template")}
placeholder="https://acme.inc/payment={payment_id}"
/>
<Input
className="mt-base"
label="Invite link template"
{...register("invite_link_template")}
placeholder="https://acme-admin.inc/invite?token={invite_token}"
/>
</div>
</div>
</BodyCard>
</div>
</form>

View File

@@ -9,7 +9,7 @@ const Regions = () => {
return (
<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="h-full w-full">
<RegionOverview id={regId} />

View File

@@ -104,7 +104,7 @@ const ReturnReasonDetail = ({ reason }: ReturnReasonDetailsProps) => {
onClick: handleSubmit(onSave),
},
{
label: "Cancel changes",
label: "Cancel",
onClick: handleCancel,
},
]}

View File

@@ -1,8 +1,8 @@
import { useAdminReturnReasons } from "medusa-react"
import { useState } from "react"
import BackButton from "../../../components/atoms/back-button"
import Spinner from "../../../components/atoms/spinner"
import PlusIcon from "../../../components/fundamentals/icons/plus-icon"
import BreadCrumb from "../../../components/molecules/breadcrumb"
import BodyCard from "../../../components/organisms/body-card"
import RadioGroup from "../../../components/organisms/radio-group"
import TwoSplitPane from "../../../components/templates/two-split-pane"
@@ -30,10 +30,10 @@ const ReturnReasons = () => {
return (
<div>
<BreadCrumb
previousRoute="/a/settings"
previousBreadcrumb="Settings"
currentPage="Return Reasons"
<BackButton
path="/a/settings"
label="Back to settings"
className="mb-xsmall"
/>
<TwoSplitPane>
<BodyCard
@@ -65,6 +65,7 @@ const ReturnReasons = () => {
>
{return_reasons?.map((reason) => (
<RadioGroup.Item
key={reason.id}
label={reason.label}
description={reason.description}
className="mt-xsmall"

View File

@@ -1,9 +1,9 @@
import { useAdminRegions } from "medusa-react"
import { useEffect } from "react"
import { useNavigate, useSearchParams } from "react-router-dom"
import BackButton from "../../../components/atoms/back-button"
import Spinner from "../../../components/atoms/spinner"
import GearIcon from "../../../components/fundamentals/icons/gear-icon"
import BreadCrumb from "../../../components/molecules/breadcrumb"
import BodyCard from "../../../components/organisms/body-card"
import RadioGroup from "../../../components/organisms/radio-group"
import TwoSplitPane from "../../../components/templates/two-split-pane"
@@ -26,10 +26,10 @@ const Taxes = () => {
return (
<>
<div>
<BreadCrumb
previousRoute="/a/settings"
previousBreadcrumb="Settings"
currentPage="Taxes"
<BackButton
path="/a/settings"
label="Back to settings"
className="mb-xsmall"
/>
<TwoSplitPane threeCols>
<BodyCard

View File

@@ -1,10 +1,10 @@
import React, { useEffect, useState } from "react"
import BreadCrumb from "../../../components/molecules/breadcrumb"
import Medusa from "../../../services/api"
import BackButton from "../../../components/atoms/back-button"
import PlusIcon from "../../../components/fundamentals/icons/plus-icon"
import BodyCard from "../../../components/organisms/body-card"
import InviteModal from "../../../components/organisms/invite-modal"
import PlusIcon from "../../../components/fundamentals/icons/plus-icon"
import UserTable from "../../../components/templates/user-table"
import Medusa from "../../../services/api"
const Users: React.FC = () => {
const [users, setUsers] = useState([])
@@ -46,28 +46,27 @@ const Users: React.FC = () => {
return (
<div className="flex h-full flex-col">
<div className="flex w-full grow flex-col">
<BreadCrumb
previousRoute="/a/settings"
previousBreadcrumb="Settings"
currentPage="The Team"
<BackButton
path="/a/settings"
label="Back to settings"
className="mb-xsmall"
/>
<BodyCard
title="The Team"
subtitle="Manage users of your Medusa Store"
actionables={actionables}
>
<div className="flex grow flex-col pt-2">
<div className="flex grow flex-col justify-between">
<UserTable
users={users}
invites={invites}
triggerRefetch={triggerRefetch}
/>
<p className="inter-small-regular text-grey-50">
{users.length} member
{users.length === 1 ? "" : "s"}
</p>
</div>
<div className="inter-small-regular text-grey-50">
{users.length} member
{users.length === 1 ? "" : "s"}
</div>
{showInviteModal && (
<InviteModal
handleClose={() => {

View File

@@ -48,6 +48,10 @@ const FormValidator = {
`${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.`,
}),
}
/**

View File

@@ -1,6 +1,7 @@
import {
AdminGiftCardsDeleteRes,
AdminGiftCardsRes,
AdminPostGiftCardsGiftCardReq,
AdminPostGiftCardsReq,
} from "@medusajs/medusa"
import { Response } from "@medusajs/medusa-js"
@@ -33,14 +34,14 @@ export const useAdminUpdateGiftCard = (
options?: UseMutationOptions<
Response<AdminGiftCardsRes>,
Error,
AdminPostGiftCardsReq
AdminPostGiftCardsGiftCardReq
>
) => {
const { client } = useMedusa()
const queryClient = useQueryClient()
return useMutation(
(payload: AdminPostGiftCardsReq) =>
(payload: AdminPostGiftCardsGiftCardReq) =>
client.admin.giftCards.update(id, payload),
buildOptions(
queryClient,

View File

@@ -1,10 +1,10 @@
import { IsBoolean, IsDate, IsInt, IsOptional, IsString } from "class-validator"
import { defaultAdminGiftCardFields, defaultAdminGiftCardRelations } from "."
import { GiftCardService } from "../../../../services"
import { Type } from "class-transformer"
import { validator } from "../../../../utils/validator"
import { EntityManager } from "typeorm"
import { GiftCardService } from "../../../../services"
import { validator } from "../../../../utils/validator"
/**
* @oas [post] /admin/gift-cards/{id}
@@ -123,7 +123,7 @@ export class AdminPostGiftCardsGiftCardReq {
@IsOptional()
@IsDate()
@Type(() => Date)
ends_at?: Date
ends_at?: Date | null
@IsOptional()
@IsString()

View File

@@ -11,7 +11,7 @@ export type CreateGiftCardInput = {
export type UpdateGiftCardInput = {
balance?: number
ends_at?: Date
ends_at?: Date | null
is_disabled?: boolean
region_id?: string
metadata?: Record<string, unknown>