feat(dashboard): Rework route modals (#6459)

**What**
- Reworks how RouteModals are setup.

**Why**
- With the current implementation it was possible to create a race-condition in the logic that handled displaying a prompt if the user tried to close a modal, while a child form was dirty. The race condition would cause a new prompt to spawn each time the user clicked the screen, making it impossible to dismiss the prompt. This only occurred in a few specific cases.

**How**
- Creates two new components: RouteFocusModal and RouteDrawer. The component shares logic for handling their own open/closed state, and now accept a form prop, that allows the modals to keep track of whether their child form is dirty. This change ensures that race conditions cannot occur, and that the prompt always only renders once when needed.
This commit is contained in:
Kasper Fabricius Kristensen authored and GitHub committed 2024-02-22 09:03:40 +00:00
1 parent add861d205
commit 72a17d6cd7
88 files changed
+1596 -1645

No files matched your search

@@ -83,6 +83,7 @@
"gallery": "Gallery", "gallery": "Gallery",
"titleHint": "Give your product a short and clear title.<0/>50-60 characters is the recommended length for search engines.", "titleHint": "Give your product a short and clear title.<0/>50-60 characters is the recommended length for search engines.",
"descriptionHint": "Give your product a short and clear description.<0/>120-160 characters is the recommended length for search engines.", "descriptionHint": "Give your product a short and clear description.<0/>120-160 characters is the recommended length for search engines.",
"discountableHint": "When unchecked discounts will not be applied to this product.",
"handleTooltip": "The handle is used to reference the product in your storefront. If not specified, the handle will be generated from the product title.", "handleTooltip": "The handle is used to reference the product in your storefront. If not specified, the handle will be generated from the product title.",
"availableInSalesChannels": "Available in <0>{{x}}</0> of <1>{{y}}</1> sales channels", "availableInSalesChannels": "Available in <0>{{x}}</0> of <1>{{y}}</1> sales channels",
"noSalesChannels": "Not available in any sales channels", "noSalesChannels": "Not available in any sales channels",
@@ -249,7 +250,12 @@
"taxInclusiveHint": "When enabled all prices in the region will be tax inclusive.", "taxInclusiveHint": "When enabled all prices in the region will be tax inclusive.",
"providersHint": "The providers that are available in the region.", "providersHint": "The providers that are available in the region.",
"shippingOptions": "Shipping Options", "shippingOptions": "Shipping Options",
"returnShippingOptions": "Return Shipping Options" "returnShippingOptions": "Return Shipping Options",
"return": "Return",
"outbound": "Outbound",
"priceType": "Price Type",
"flatRate": "Flat Rate",
"calculated": "Calculated"
}, },
"locations": { "locations": {
"domain": "Locations", "domain": "Locations",
@@ -399,6 +405,7 @@
"dateIssued": "Date issued", "dateIssued": "Date issued",
"issuedDate": "Issued date", "issuedDate": "Issued date",
"expiryDate": "Expiry date", "expiryDate": "Expiry date",
"price": "Price",
"height": "Height", "height": "Height",
"width": "Width", "width": "Width",
"length": "Length", "length": "Length",
@@ -0,0 +1,3 @@
export { RouteDrawer } from "./route-drawer"
export { RouteFocusModal } from "./route-focus-modal"
export { useRouteModal } from "./route-modal-provider"
@@ -0,0 +1 @@
export * from "./route-drawer"
@@ -0,0 +1,59 @@
import { Drawer } from "@medusajs/ui"
import { PropsWithChildren, useEffect, useState } from "react"
import { useNavigate } from "react-router-dom"
import { RouteForm } from "../route-form"
import { RouteModalProvider } from "../route-modal-provider/route-provider"
type RouteDrawerProps = PropsWithChildren<{
prev?: string
}>
const Root = ({ prev = "..", children }: RouteDrawerProps) => {
const navigate = useNavigate()
const [open, setOpen] = useState(false)
/**
* Open the modal when the component mounts. This
* ensures that the entry animation is played.
*/
useEffect(() => {
setOpen(true)
}, [])
const handleOpenChange = (open: boolean) => {
if (!open) {
document.body.style.pointerEvents = "auto"
navigate(prev, { replace: true })
return
}
setOpen(open)
}
return (
<Drawer open={open} onOpenChange={handleOpenChange}>
<RouteModalProvider prev={prev}>
<Drawer.Content>{children}</Drawer.Content>
</RouteModalProvider>
</Drawer>
)
}
const Header = Drawer.Header
const Body = Drawer.Body
const Footer = Drawer.Footer
const Close = Drawer.Close
const Form = RouteForm
/**
* Drawer that is used to render a form on a separate route.
*
* Typically used for forms editing a resource.
*/
export const RouteDrawer = Object.assign(Root, {
Header,
Body,
Footer,
Close,
Form,
})
@@ -0,0 +1 @@
export * from "./route-focus-modal"
@@ -0,0 +1,58 @@
import { FocusModal } from "@medusajs/ui"
import { PropsWithChildren, useEffect, useState } from "react"
import { useNavigate } from "react-router-dom"
import { RouteForm } from "../route-form"
import { RouteModalProvider } from "../route-modal-provider/route-provider"
type RouteFocusModalProps = PropsWithChildren<{
prev?: string
}>
const Root = ({ prev = "..", children }: RouteFocusModalProps) => {
const navigate = useNavigate()
const [open, setOpen] = useState(false)
/**
* Open the modal when the component mounts. This
* ensures that the entry animation is played.
*/
useEffect(() => {
setOpen(true)
}, [])
const handleOpenChange = (open: boolean) => {
if (!open) {
document.body.style.pointerEvents = "auto"
navigate(prev, { replace: true })
return
}
setOpen(open)
}
return (
<FocusModal open={open} onOpenChange={handleOpenChange}>
<RouteModalProvider prev={prev}>
<FocusModal.Content>{children}</FocusModal.Content>
</RouteModalProvider>
</FocusModal>
)
}
const Header = FocusModal.Header
const Body = FocusModal.Body
const Close = FocusModal.Close
const Form = RouteForm
/**
* FocusModal that is used to render a form on a separate route.
*
* Typically used for forms creating a resource or forms that require
* a lot of space.
*/
export const RouteFocusModal = Object.assign(Root, {
Header,
Body,
Close,
Form,
})
@@ -0,0 +1 @@
export * from "./route-form"
@@ -0,0 +1,63 @@
import { Prompt } from "@medusajs/ui"
import { PropsWithChildren } from "react"
import { FieldValues, UseFormReturn } from "react-hook-form"
import { useTranslation } from "react-i18next"
import { useBlocker } from "react-router-dom"
import { Form } from "../../common/form"
type RouteFormProps<TFieldValues extends FieldValues> = PropsWithChildren<{
form: UseFormReturn<TFieldValues>
}>
export const RouteForm = <TFieldValues extends FieldValues = any>({
form,
children,
}: RouteFormProps<TFieldValues>) => {
const { t } = useTranslation()
const {
formState: { isDirty },
} = form
const blocker = useBlocker(({ currentLocation, nextLocation }) => {
const { isSubmitSuccessful } = nextLocation.state || {}
if (isSubmitSuccessful) {
return false
}
return isDirty && currentLocation.pathname !== nextLocation.pathname
})
const handleCancel = () => {
blocker?.reset?.()
}
const handleContinue = () => {
blocker?.proceed?.()
}
return (
<Form {...form}>
{children}
<Prompt open={blocker.state === "blocked"} variant="confirmation">
<Prompt.Content>
<Prompt.Header>
<Prompt.Title>{t("general.unsavedChangesTitle")}</Prompt.Title>
<Prompt.Description>
{t("general.unsavedChangesDescription")}
</Prompt.Description>
</Prompt.Header>
<Prompt.Footer>
<Prompt.Cancel onClick={handleCancel} type="button">
{t("actions.cancel")}
</Prompt.Cancel>
<Prompt.Action onClick={handleContinue} type="button">
{t("actions.continue")}
</Prompt.Action>
</Prompt.Footer>
</Prompt.Content>
</Prompt>
</Form>
)
}
@@ -0,0 +1 @@
export * from "./route-provider"
@@ -0,0 +1,41 @@
import { PropsWithChildren, createContext, useContext } from "react"
import { useNavigate } from "react-router-dom"
type RouteModalProviderContextType = {
handleSuccess: (path?: string) => void
}
const RouteModalProviderContext =
createContext<RouteModalProviderContextType | null>(null)
export const useRouteModal = () => {
const context = useContext(RouteModalProviderContext)
if (!context) {
throw new Error("useRouteModal must be used within a RouteModalProvider")
}
return context
}
type RouteModalProviderProps = PropsWithChildren<{
prev: string
}>
export const RouteModalProvider = ({
prev,
children,
}: RouteModalProviderProps) => {
const navigate = useNavigate()
const handleSuccess = (path?: string) => {
const to = path || prev
navigate(to, { replace: true, state: { isSubmitSuccessful: true } })
}
return (
<RouteModalProviderContext.Provider value={{ handleSuccess }}>
{children}
</RouteModalProviderContext.Provider>
)
}
@@ -0,0 +1,56 @@
import { Country } from "@medusajs/medusa"
import { Tooltip } from "@medusajs/ui"
import { useTranslation } from "react-i18next"
import { PlaceholderCell } from "../../common/placeholder-cell"
type CountriesCellProps = {
countries?: Country[] | null
}
export const CountriesCell = ({ countries }: CountriesCellProps) => {
const { t } = useTranslation()
if (!countries || countries.length === 0) {
return <PlaceholderCell />
}
const displayValue = countries
.slice(0, 2)
.map((c) => c.display_name)
.join(", ")
const additionalCountries = countries.slice(2).map((c) => c.display_name)
return (
<div className="flex size-full items-center gap-x-1">
<span>{displayValue}</span>
{additionalCountries.length > 0 && (
<Tooltip
content={
<ul>
{additionalCountries.map((c) => (
<li key={c}>{c}</li>
))}
</ul>
}
>
<span>
{t("general.plusCountMore", {
count: additionalCountries.length,
})}
</span>
</Tooltip>
)}
</div>
)
}
export const CountriesHeader = () => {
const { t } = useTranslation()
return (
<div className="flex size-full items-center">
<span>{t("fields.countries")}</span>
</div>
)
}
@@ -0,0 +1 @@
export * from "./countries-cell"
@@ -0,0 +1,58 @@
import { FulfillmentProvider } from "@medusajs/medusa"
import { Tooltip } from "@medusajs/ui"
import { useTranslation } from "react-i18next"
import { PlaceholderCell } from "../../common/placeholder-cell"
type FulfillmentProvidersCellProps = {
fulfillmentProviders?: FulfillmentProvider[] | null
}
export const FulfillmentProvidersCell = ({
fulfillmentProviders,
}: FulfillmentProvidersCellProps) => {
const { t } = useTranslation()
if (!fulfillmentProviders || fulfillmentProviders.length === 0) {
return <PlaceholderCell />
}
const displayValue = fulfillmentProviders
.slice(0, 2)
.map((p) => p.id)
.join(", ")
const additionalProviders = fulfillmentProviders.slice(2).map((c) => c.id)
return (
<div className="flex size-full items-center gap-x-1">
<span>{displayValue}</span>
{additionalProviders.length > 0 && (
<Tooltip
content={
<ul>
{additionalProviders.map((c) => (
<li key={c}>{c}</li>
))}
</ul>
}
>
<span>
{t("general.plusCountMore", {
count: additionalProviders.length,
})}
</span>
</Tooltip>
)}
</div>
)
}
export const FulfillmentProvidersHeader = () => {
const { t } = useTranslation()
return (
<div className="flex size-full items-center">
<span>{t("fields.fulfillmentProviders")}</span>
</div>
)
}
@@ -0,0 +1 @@
export * from "./fulfillment-providers-cell"
@@ -0,0 +1 @@
export * from "./payment-providers-cell"
@@ -0,0 +1,58 @@
import { PaymentProvider } from "@medusajs/medusa"
import { Tooltip } from "@medusajs/ui"
import { useTranslation } from "react-i18next"
import { PlaceholderCell } from "../../common/placeholder-cell"
type PaymentProvidersCellProps = {
paymentProviders?: PaymentProvider[] | null
}
export const PaymentProvidersCell = ({
paymentProviders,
}: PaymentProvidersCellProps) => {
const { t } = useTranslation()
if (!paymentProviders || paymentProviders.length === 0) {
return <PlaceholderCell />
}
const displayValue = paymentProviders
.slice(0, 2)
.map((p) => p.id)
.join(", ")
const additionalProviders = paymentProviders.slice(2).map((c) => c.id)
return (
<div className="flex size-full items-center gap-x-1">
<span>{displayValue}</span>
{additionalProviders.length > 0 && (
<Tooltip
content={
<ul>
{additionalProviders.map((c) => (
<li key={c}>{c}</li>
))}
</ul>
}
>
<span>
{t("general.plusCountMore", {
count: additionalProviders.length,
})}
</span>
</Tooltip>
)}
</div>
)
}
export const PaymentProvidersHeader = () => {
const { t } = useTranslation()
return (
<div className="flex size-full items-center">
<span>{t("fields.paymentProviders")}</span>
</div>
)
}
@@ -0,0 +1 @@
export * from "./region-cell"
@@ -0,0 +1,23 @@
import { useTranslation } from "react-i18next"
type RegionCellProps = {
name: string
}
export const RegionCell = ({ name }: RegionCellProps) => {
return (
<div className="flex h-full w-full items-center overflow-hidden">
<span className="truncate">{name}</span>
</div>
)
}
export const RegionHeader = () => {
const { t } = useTranslation()
return (
<div className="flex h-full w-full items-center">
<span className="truncate">{t("fields.name")}</span>
</div>
)
}
@@ -0,0 +1,50 @@
import { Region } from "@medusajs/medusa"
import { createColumnHelper } from "@tanstack/react-table"
import { useMemo } from "react"
import {
CountriesCell,
CountriesHeader,
} from "../../../components/table/table-cells/region/countries-cell"
import {
FulfillmentProvidersCell,
FulfillmentProvidersHeader,
} from "../../../components/table/table-cells/region/fulfillment-providers-cell"
import {
PaymentProvidersCell,
PaymentProvidersHeader,
} from "../../../components/table/table-cells/region/payment-providers-cell"
import {
RegionCell,
RegionHeader,
} from "../../../components/table/table-cells/region/region-cell"
const columnHelper = createColumnHelper<Region>()
export const useRegionTableColumns = () => {
return useMemo(
() => [
columnHelper.accessor("name", {
header: () => <RegionHeader />,
cell: ({ getValue }) => <RegionCell name={getValue()} />,
}),
columnHelper.accessor("countries", {
header: () => <CountriesHeader />,
cell: ({ getValue }) => <CountriesCell countries={getValue()} />,
}),
columnHelper.accessor("payment_providers", {
header: () => <PaymentProvidersHeader />,
cell: ({ getValue }) => (
<PaymentProvidersCell paymentProviders={getValue()} />
),
}),
columnHelper.accessor("fulfillment_providers", {
header: () => <FulfillmentProvidersHeader />,
cell: ({ getValue }) => (
<FulfillmentProvidersCell fulfillmentProviders={getValue()} />
),
}),
],
[]
)
}
@@ -0,0 +1,19 @@
import { useTranslation } from "react-i18next"
import { Filter } from "../../../components/table/data-table"
export const useRegionTableFilters = () => {
const { t } = useTranslation()
const dateFilters: Filter[] = [
{ label: t("fields.createdAt"), key: "created_at" },
{ label: t("fields.updatedAt"), key: "updated_at" },
].map((f) => ({
key: f.key,
label: f.label,
type: "date",
}))
const filters = [...dateFilters]
return filters
}
@@ -0,0 +1,33 @@
import { AdminGetRegionsParams } from "@medusajs/medusa"
import { useQueryParams } from "../../use-query-params"
type UseRegionTableQueryProps = {
prefix?: string
pageSize?: number
}
export const useRegionTableQuery = ({
prefix,
pageSize = 20,
}: UseRegionTableQueryProps) => {
const queryObject = useQueryParams(
["offset", "q", "order", "created_at", "updated_at"],
prefix
)
const { offset, q, order, created_at, updated_at } = queryObject
const searchParams: AdminGetRegionsParams = {
limit: pageSize,
offset: offset ? Number(offset) : 0,
order,
created_at: created_at ? JSON.parse(created_at) : undefined,
updated_at: updated_at ? JSON.parse(updated_at) : undefined,
q,
}
return {
searchParams,
raw: queryObject,
}
}
@@ -1,57 +0,0 @@
import { usePrompt } from "@medusajs/ui"
import { useEffect, useState } from "react"
import { useTranslation } from "react-i18next"
import { useNavigate } from "react-router-dom"
/**
* Hook for managing the state of route modals.
*/
export const useRouteModalState = (): [
open: boolean,
onOpenChange: (open: boolean, ignore?: boolean) => void,
/**
* Subscribe to the dirty state of the form.
* If the form is dirty, the modal will prompt
* the user before closing.
*/
subscribe: (value: boolean) => void,
] => {
const [open, setOpen] = useState(false)
const [shouldPrompt, subscribe] = useState(false)
const navigate = useNavigate()
const prompt = usePrompt()
const { t } = useTranslation()
const promptValues = {
title: t("general.unsavedChangesTitle"),
description: t("general.unsavedChangesDescription"),
cancelText: t("actions.cancel"),
confirmText: t("actions.continue"),
variant: "confirmation" as const,
}
useEffect(() => {
setOpen(true)
}, [])
const onOpenChange = async (open: boolean, ignore = false) => {
if (!open) {
if (shouldPrompt && !ignore) {
const confirmed = await prompt(promptValues)
if (!confirmed) {
return
}
}
setTimeout(() => {
navigate("..", { replace: true })
}, 200)
}
setOpen(open)
}
return [open, onOpenChange, subscribe]
}
@@ -1,36 +1,26 @@
import { FocusModal } from "@medusajs/ui"
import { useAdminPublishableApiKeySalesChannels } from "medusa-react" import { useAdminPublishableApiKeySalesChannels } from "medusa-react"
import { useParams } from "react-router-dom" import { useParams } from "react-router-dom"
import { useRouteModalState } from "../../../hooks/use-route-modal-state" import { RouteFocusModal } from "../../../components/route-modal"
import { AddSalesChannelsToApiKeyForm } from "./components" import { AddSalesChannelsToApiKeyForm } from "./components"
export const ApiKeyManagementAddSalesChannels = () => { export const ApiKeyManagementAddSalesChannels = () => {
const { id } = useParams() const { id } = useParams()
const [open, onOpenChange, subscribe] = useRouteModalState()
const { sales_channels, isLoading, isError, error } = const { sales_channels, isLoading, isError, error } =
useAdminPublishableApiKeySalesChannels(id!) useAdminPublishableApiKeySalesChannels(id!)
const handleSuccessfulSubmit = () => {
onOpenChange(false, true)
}
if (isError) { if (isError) {
throw error throw error
} }
return ( return (
<FocusModal open={open} onOpenChange={onOpenChange}> <RouteFocusModal>
<FocusModal.Content>
{!isLoading && sales_channels && ( {!isLoading && sales_channels && (
<AddSalesChannelsToApiKeyForm <AddSalesChannelsToApiKeyForm
apiKey={id!} apiKey={id!}
preSelected={sales_channels.map((sc) => sc.id)} preSelected={sales_channels.map((sc) => sc.id)}
onSuccessfulSubmit={handleSuccessfulSubmit}
subscribe={subscribe}
/> />
)} )}
</FocusModal.Content> </RouteFocusModal>
</FocusModal>
) )
} }
@@ -3,7 +3,6 @@ import { SalesChannel } from "@medusajs/medusa"
import { import {
Button, Button,
Checkbox, Checkbox,
FocusModal,
Hint, Hint,
StatusBadge, StatusBadge,
Table, Table,
@@ -26,17 +25,18 @@ import { useEffect, useMemo, useState } from "react"
import { useForm } from "react-hook-form" import { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
import * as zod from "zod" import * as zod from "zod"
import { Form } from "../../../../components/common/form"
import { OrderBy } from "../../../../components/filtering/order-by" import { OrderBy } from "../../../../components/filtering/order-by"
import { Query } from "../../../../components/filtering/query" import { Query } from "../../../../components/filtering/query"
import { LocalizedTablePagination } from "../../../../components/localization/localized-table-pagination" import { LocalizedTablePagination } from "../../../../components/localization/localized-table-pagination"
import {
RouteFocusModal,
useRouteModal,
} from "../../../../components/route-modal"
import { useQueryParams } from "../../../../hooks/use-query-params" import { useQueryParams } from "../../../../hooks/use-query-params"
type AddSalesChannelsToApiKeyFormProps = { type AddSalesChannelsToApiKeyFormProps = {
apiKey: string apiKey: string
preSelected: string[] preSelected: string[]
subscribe: (state: boolean) => void
onSuccessfulSubmit: () => void
} }
const AddSalesChannelsToApiKeySchema = zod.object({ const AddSalesChannelsToApiKeySchema = zod.object({
@@ -48,10 +48,9 @@ const PAGE_SIZE = 50
export const AddSalesChannelsToApiKeyForm = ({ export const AddSalesChannelsToApiKeyForm = ({
apiKey, apiKey,
preSelected, preSelected,
subscribe,
onSuccessfulSubmit,
}: AddSalesChannelsToApiKeyFormProps) => { }: AddSalesChannelsToApiKeyFormProps) => {
const { t } = useTranslation() const { t } = useTranslation()
const { handleSuccess } = useRouteModal()
const form = useForm<zod.infer<typeof AddSalesChannelsToApiKeySchema>>({ const form = useForm<zod.infer<typeof AddSalesChannelsToApiKeySchema>>({
defaultValues: { defaultValues: {
@@ -60,13 +59,7 @@ export const AddSalesChannelsToApiKeyForm = ({
resolver: zodResolver(AddSalesChannelsToApiKeySchema), resolver: zodResolver(AddSalesChannelsToApiKeySchema),
}) })
const { const { setValue } = form
formState: { isDirty },
} = form
useEffect(() => {
subscribe(isDirty)
}, [isDirty])
const { mutateAsync, isLoading: isMutating } = const { mutateAsync, isLoading: isMutating } =
useAdminAddPublishableKeySalesChannelsBatch(apiKey) useAdminAddPublishableKeySalesChannelsBatch(apiKey)
@@ -87,11 +80,15 @@ export const AddSalesChannelsToApiKeyForm = ({
const [rowSelection, setRowSelection] = useState<RowSelectionState>({}) const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
useEffect(() => { useEffect(() => {
form.setValue( setValue(
"sales_channel_ids", "sales_channel_ids",
Object.keys(rowSelection).filter((k) => rowSelection[k]) Object.keys(rowSelection).filter((k) => rowSelection[k]),
{
shouldDirty: true,
shouldTouch: true,
}
) )
}, [rowSelection]) }, [rowSelection, setValue])
const params = useQueryParams(["q", "order"]) const params = useQueryParams(["q", "order"])
const { sales_channels, count } = useAdminSalesChannels( const { sales_channels, count } = useAdminSalesChannels(
@@ -135,36 +132,36 @@ export const AddSalesChannelsToApiKeyForm = ({
}, },
{ {
onSuccess: () => { onSuccess: () => {
onSuccessfulSubmit() handleSuccess()
}, },
} }
) )
}) })
return ( return (
<Form {...form}> <RouteFocusModal.Form form={form}>
<form <form
onSubmit={handleSubmit} onSubmit={handleSubmit}
className="flex h-full flex-col overflow-hidden" className="flex h-full flex-col overflow-hidden"
> >
<FocusModal.Header> <RouteFocusModal.Header>
<div className="flex items-center justify-end gap-x-2"> <div className="flex items-center justify-end gap-x-2">
{form.formState.errors.sales_channel_ids && ( {form.formState.errors.sales_channel_ids && (
<Hint variant="error"> <Hint variant="error">
{form.formState.errors.sales_channel_ids.message} {form.formState.errors.sales_channel_ids.message}
</Hint> </Hint>
)} )}
<FocusModal.Close asChild> <RouteFocusModal.Close asChild>
<Button size="small" variant="secondary"> <Button size="small" variant="secondary">
{t("actions.cancel")} {t("actions.cancel")}
</Button> </Button>
</FocusModal.Close> </RouteFocusModal.Close>
<Button size="small" type="submit" isLoading={isMutating}> <Button size="small" type="submit" isLoading={isMutating}>
{t("actions.save")} {t("actions.save")}
</Button> </Button>
</div> </div>
</FocusModal.Header> </RouteFocusModal.Header>
<FocusModal.Body className="flex h-full w-full flex-col items-center divide-y overflow-y-auto"> <RouteFocusModal.Body className="flex h-full w-full flex-col items-center divide-y overflow-y-auto">
<div className="flex w-full items-center justify-between px-6 py-4"> <div className="flex w-full items-center justify-between px-6 py-4">
<div></div> <div></div>
<div className="flex items-center gap-x-2"> <div className="flex items-center gap-x-2">
@@ -236,9 +233,9 @@ export const AddSalesChannelsToApiKeyForm = ({
pageSize={PAGE_SIZE} pageSize={PAGE_SIZE}
/> />
</div> </div>
</FocusModal.Body> </RouteFocusModal.Body>
</form> </form>
</Form> </RouteFocusModal.Form>
) )
} }
@@ -1,15 +1,10 @@
import { FocusModal } from "@medusajs/ui" import { RouteFocusModal } from "../../../components/route-modal"
import { useRouteModalState } from "../../../hooks/use-route-modal-state"
import { CreatePublishableApiKeyForm } from "./components/create-publishable-api-key-form" import { CreatePublishableApiKeyForm } from "./components/create-publishable-api-key-form"
export const ApiKeyManagementCreate = () => { export const ApiKeyManagementCreate = () => {
const [open, onOpenChange, subscribe] = useRouteModalState()
return ( return (
<FocusModal open={open} onOpenChange={onOpenChange}> <RouteFocusModal>
<FocusModal.Content> <CreatePublishableApiKeyForm />
<CreatePublishableApiKeyForm subscribe={subscribe} /> </RouteFocusModal>
</FocusModal.Content>
</FocusModal>
) )
} }
@@ -1,26 +1,23 @@
import { zodResolver } from "@hookform/resolvers/zod" import { zodResolver } from "@hookform/resolvers/zod"
import { Button, FocusModal, Heading, Input, Text } from "@medusajs/ui" import { Button, Heading, Input, Text } from "@medusajs/ui"
import { useAdminCreatePublishableApiKey } from "medusa-react" import { useAdminCreatePublishableApiKey } from "medusa-react"
import { useForm } from "react-hook-form" import { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
import * as zod from "zod" import * as zod from "zod"
import { useEffect } from "react"
import { useNavigate } from "react-router-dom"
import { Form } from "../../../../../components/common/form" import { Form } from "../../../../../components/common/form"
import {
type CreatePublishableApiKeyFormProps = { RouteFocusModal,
subscribe: (state: boolean) => void useRouteModal,
} } from "../../../../../components/route-modal"
const CreatePublishableApiKeySchema = zod.object({ const CreatePublishableApiKeySchema = zod.object({
title: zod.string().min(1), title: zod.string().min(1),
}) })
export const CreatePublishableApiKeyForm = ({ export const CreatePublishableApiKeyForm = () => {
subscribe, const { t } = useTranslation()
}: CreatePublishableApiKeyFormProps) => { const { handleSuccess } = useRouteModal()
const { mutateAsync, isLoading } = useAdminCreatePublishableApiKey()
const form = useForm<zod.infer<typeof CreatePublishableApiKeySchema>>({ const form = useForm<zod.infer<typeof CreatePublishableApiKeySchema>>({
defaultValues: { defaultValues: {
@@ -29,46 +26,35 @@ export const CreatePublishableApiKeyForm = ({
resolver: zodResolver(CreatePublishableApiKeySchema), resolver: zodResolver(CreatePublishableApiKeySchema),
}) })
const { const { mutateAsync, isLoading } = useAdminCreatePublishableApiKey()
formState: { isDirty },
} = form
useEffect(() => {
subscribe(isDirty)
}, [isDirty])
const { t } = useTranslation()
const navigate = useNavigate()
const handleSubmit = form.handleSubmit(async (values) => { const handleSubmit = form.handleSubmit(async (values) => {
await mutateAsync(values, { await mutateAsync(values, {
onSuccess: ({ publishable_api_key }) => { onSuccess: ({ publishable_api_key }) => {
navigate(`/settings/api-key-management/${publishable_api_key.id}`, { handleSuccess(`/settings/api-key-management/${publishable_api_key.id}`)
replace: true,
})
}, },
}) })
}) })
return ( return (
<Form {...form}> <RouteFocusModal.Form form={form}>
<form <form
className="flex h-full flex-col overflow-hidden" className="flex h-full flex-col overflow-hidden"
onSubmit={handleSubmit} onSubmit={handleSubmit}
> >
<FocusModal.Header> <RouteFocusModal.Header>
<div className="flex items-center justify-end gap-x-2"> <div className="flex items-center justify-end gap-x-2">
<FocusModal.Close asChild> <RouteFocusModal.Close asChild>
<Button size="small" variant="secondary"> <Button size="small" variant="secondary">
{t("actions.cancel")} {t("actions.cancel")}
</Button> </Button>
</FocusModal.Close> </RouteFocusModal.Close>
<Button size="small" type="submit" isLoading={isLoading}> <Button size="small" type="submit" isLoading={isLoading}>
{t("actions.save")} {t("actions.save")}
</Button> </Button>
</div> </div>
</FocusModal.Header> </RouteFocusModal.Header>
<FocusModal.Body className="flex flex-1 flex-col overflow-hidden"> <RouteFocusModal.Body className="flex flex-1 flex-col overflow-hidden">
<div className="flex flex-1 flex-col items-center overflow-y-auto"> <div className="flex flex-1 flex-col items-center overflow-y-auto">
<div className="flex w-full max-w-[720px] flex-col gap-y-8 px-2 py-16"> <div className="flex w-full max-w-[720px] flex-col gap-y-8 px-2 py-16">
<div> <div>
@@ -88,7 +74,7 @@ export const CreatePublishableApiKeyForm = ({
<Form.Item> <Form.Item>
<Form.Label>{t("fields.title")}</Form.Label> <Form.Label>{t("fields.title")}</Form.Label>
<Form.Control> <Form.Control>
<Input size="small" {...field} /> <Input {...field} />
</Form.Control> </Form.Control>
<Form.ErrorMessage /> <Form.ErrorMessage />
</Form.Item> </Form.Item>
@@ -98,8 +84,8 @@ export const CreatePublishableApiKeyForm = ({
</div> </div>
</div> </div>
</div> </div>
</FocusModal.Body> </RouteFocusModal.Body>
</form> </form>
</Form> </RouteFocusModal.Form>
) )
} }
@@ -1,40 +1,29 @@
import { Drawer, Heading } from "@medusajs/ui" import { Heading } from "@medusajs/ui"
import { useAdminPublishableApiKey } from "medusa-react" import { useAdminPublishableApiKey } from "medusa-react"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
import { useParams } from "react-router-dom" import { useParams } from "react-router-dom"
import { useRouteModalState } from "../../../hooks/use-route-modal-state" import { RouteDrawer } from "../../../components/route-modal"
import { EditApiKeyForm } from "./components/edit-api-key-form" import { EditApiKeyForm } from "./components/edit-api-key-form"
export const ApiKeyManagementEdit = () => { export const ApiKeyManagementEdit = () => {
const [open, onOpenChange, subscribe] = useRouteModalState()
const { id } = useParams() const { id } = useParams()
const { t } = useTranslation() const { t } = useTranslation()
const { publishable_api_key, isLoading, isError, error } = const { publishable_api_key, isLoading, isError, error } =
useAdminPublishableApiKey(id!) useAdminPublishableApiKey(id!)
const handleSuccessfulSubmit = () => {
onOpenChange(false, true)
}
if (isError) { if (isError) {
throw error throw error
} }
return ( return (
<Drawer open={open} onOpenChange={onOpenChange}> <RouteDrawer>
<Drawer.Content> <RouteDrawer.Header>
<Drawer.Header>
<Heading>{t("apiKeyManagement.editKey")}</Heading> <Heading>{t("apiKeyManagement.editKey")}</Heading>
</Drawer.Header> </RouteDrawer.Header>
{!isLoading && publishable_api_key && ( {!isLoading && publishable_api_key && (
<EditApiKeyForm <EditApiKeyForm apiKey={publishable_api_key} />
apiKey={publishable_api_key}
onSuccessfulSubmit={handleSuccessfulSubmit}
subscribe={subscribe}
/>
)} )}
</Drawer.Content> </RouteDrawer>
</Drawer>
) )
} }
@@ -1,29 +1,28 @@
import { zodResolver } from "@hookform/resolvers/zod" import { zodResolver } from "@hookform/resolvers/zod"
import type { PublishableApiKey } from "@medusajs/medusa" import type { PublishableApiKey } from "@medusajs/medusa"
import { Button, Drawer, Input } from "@medusajs/ui" import { Button, Input } from "@medusajs/ui"
import { useAdminUpdatePublishableApiKey } from "medusa-react" import { useAdminUpdatePublishableApiKey } from "medusa-react"
import { useEffect } from "react"
import { useForm } from "react-hook-form" import { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
import * as zod from "zod" import * as zod from "zod"
import { Form } from "../../../../../components/common/form" import { Form } from "../../../../../components/common/form"
import {
RouteDrawer,
useRouteModal,
} from "../../../../../components/route-modal"
type EditApiKeyFormProps = { type EditApiKeyFormProps = {
apiKey: PublishableApiKey apiKey: PublishableApiKey
onSuccessfulSubmit: () => void
subscribe: (state: boolean) => void
} }
const EditApiKeySchema = zod.object({ const EditApiKeySchema = zod.object({
title: zod.string().min(1), title: zod.string().min(1),
}) })
export const EditApiKeyForm = ({ export const EditApiKeyForm = ({ apiKey }: EditApiKeyFormProps) => {
apiKey,
onSuccessfulSubmit,
subscribe,
}: EditApiKeyFormProps) => {
const { t } = useTranslation() const { t } = useTranslation()
const { handleSuccess } = useRouteModal()
const form = useForm<zod.infer<typeof EditApiKeySchema>>({ const form = useForm<zod.infer<typeof EditApiKeySchema>>({
defaultValues: { defaultValues: {
@@ -32,28 +31,23 @@ export const EditApiKeyForm = ({
resolver: zodResolver(EditApiKeySchema), resolver: zodResolver(EditApiKeySchema),
}) })
const {
formState: { isDirty },
} = form
useEffect(() => {
subscribe(isDirty)
}, [isDirty])
const { mutateAsync, isLoading } = useAdminUpdatePublishableApiKey(apiKey.id) const { mutateAsync, isLoading } = useAdminUpdatePublishableApiKey(apiKey.id)
const handleSubmit = form.handleSubmit(async (data) => { const handleSubmit = form.handleSubmit(async (data) => {
await mutateAsync(data, { await mutateAsync(data, {
onSuccess: () => { onSuccess: () => {
onSuccessfulSubmit() handleSuccess()
},
onError: (error) => {
console.log(error)
}, },
}) })
}) })
return ( return (
<Form {...form}> <RouteDrawer.Form form={form}>
<form onSubmit={handleSubmit} className="flex flex-1 flex-col"> <form onSubmit={handleSubmit} className="flex flex-1 flex-col">
<Drawer.Body> <RouteDrawer.Body>
<div className="flex flex-col gap-y-4"> <div className="flex flex-col gap-y-4">
<Form.Field <Form.Field
control={form.control} control={form.control}
@@ -71,20 +65,20 @@ export const EditApiKeyForm = ({
}} }}
/> />
</div> </div>
</Drawer.Body> </RouteDrawer.Body>
<Drawer.Footer> <RouteDrawer.Footer>
<div className="flex items-center gap-x-2"> <div className="flex items-center gap-x-2">
<Drawer.Close asChild> <RouteDrawer.Close asChild>
<Button size="small" variant="secondary"> <Button size="small" variant="secondary">
{t("actions.cancel")} {t("actions.cancel")}
</Button> </Button>
</Drawer.Close> </RouteDrawer.Close>
<Button size="small" type="submit" isLoading={isLoading}> <Button size="small" type="submit" isLoading={isLoading}>
{t("actions.save")} {t("actions.save")}
</Button> </Button>
</div> </div>
</Drawer.Footer> </RouteDrawer.Footer>
</form> </form>
</Form> </RouteDrawer.Form>
) )
} }
@@ -1,34 +1,22 @@
import { FocusModal } from "@medusajs/ui"
import { useAdminCollection } from "medusa-react" import { useAdminCollection } from "medusa-react"
import { useParams } from "react-router-dom" import { useParams } from "react-router-dom"
import { useRouteModalState } from "../../../hooks/use-route-modal-state"
import { RouteFocusModal } from "../../../components/route-modal"
import { AddProductsToCollectionForm } from "./components/add-products-to-collection-form" import { AddProductsToCollectionForm } from "./components/add-products-to-collection-form"
export const CollectionAddProducts = () => { export const CollectionAddProducts = () => {
const { id } = useParams() const { id } = useParams()
const { collection, isLoading, isError, error } = useAdminCollection(id!) const { collection, isLoading, isError, error } = useAdminCollection(id!)
const [open, onOpenChange, subscribe] = useRouteModalState()
const handleSuccessfulSubmit = () => {
onOpenChange(false, true)
}
if (isError) { if (isError) {
throw error throw error
} }
return ( return (
<FocusModal open={open} onOpenChange={onOpenChange}> <RouteFocusModal>
<FocusModal.Content>
{!isLoading && collection && ( {!isLoading && collection && (
<AddProductsToCollectionForm <AddProductsToCollectionForm collection={collection} />
collection={collection}
onSuccessfulSubmit={handleSuccessfulSubmit}
subscribe={subscribe}
/>
)} )}
</FocusModal.Content> </RouteFocusModal>
</FocusModal>
) )
} }
@@ -1,14 +1,6 @@
import { zodResolver } from "@hookform/resolvers/zod" import { zodResolver } from "@hookform/resolvers/zod"
import type { Product, ProductCollection } from "@medusajs/medusa" import type { Product, ProductCollection } from "@medusajs/medusa"
import { import { Button, Checkbox, Hint, Table, Tooltip, clx } from "@medusajs/ui"
Button,
Checkbox,
FocusModal,
Hint,
Table,
Tooltip,
clx,
} from "@medusajs/ui"
import { import {
PaginationState, PaginationState,
RowSelectionState, RowSelectionState,
@@ -30,7 +22,6 @@ import {
NoRecords, NoRecords,
NoResults, NoResults,
} from "../../../../../components/common/empty-table-content" } from "../../../../../components/common/empty-table-content"
import { Form } from "../../../../../components/common/form"
import { import {
ProductAvailabilityCell, ProductAvailabilityCell,
ProductCollectionCell, ProductCollectionCell,
@@ -41,14 +32,16 @@ import {
import { OrderBy } from "../../../../../components/filtering/order-by" import { OrderBy } from "../../../../../components/filtering/order-by"
import { Query } from "../../../../../components/filtering/query" import { Query } from "../../../../../components/filtering/query"
import { LocalizedTablePagination } from "../../../../../components/localization/localized-table-pagination" import { LocalizedTablePagination } from "../../../../../components/localization/localized-table-pagination"
import {
RouteFocusModal,
useRouteModal,
} from "../../../../../components/route-modal"
import { useHandleTableScroll } from "../../../../../hooks/use-handle-table-scroll" import { useHandleTableScroll } from "../../../../../hooks/use-handle-table-scroll"
import { useQueryParams } from "../../../../../hooks/use-query-params" import { useQueryParams } from "../../../../../hooks/use-query-params"
import { queryClient } from "../../../../../lib/medusa" import { queryClient } from "../../../../../lib/medusa"
type AddProductsToCollectionFormProps = { type AddProductsToCollectionFormProps = {
collection: ProductCollection collection: ProductCollection
subscribe: (state: boolean) => void
onSuccessfulSubmit: () => void
} }
const AddProductsToSalesChannelSchema = zod.object({ const AddProductsToSalesChannelSchema = zod.object({
@@ -59,10 +52,9 @@ const PAGE_SIZE = 50
export const AddProductsToCollectionForm = ({ export const AddProductsToCollectionForm = ({
collection, collection,
subscribe,
onSuccessfulSubmit,
}: AddProductsToCollectionFormProps) => { }: AddProductsToCollectionFormProps) => {
const { t } = useTranslation() const { t } = useTranslation()
const { handleSuccess } = useRouteModal()
const form = useForm<zod.infer<typeof AddProductsToSalesChannelSchema>>({ const form = useForm<zod.infer<typeof AddProductsToSalesChannelSchema>>({
defaultValues: { defaultValues: {
@@ -71,13 +63,7 @@ export const AddProductsToCollectionForm = ({
resolver: zodResolver(AddProductsToSalesChannelSchema), resolver: zodResolver(AddProductsToSalesChannelSchema),
}) })
const { const { setValue } = form
formState: { isDirty },
} = form
useEffect(() => {
subscribe(isDirty)
}, [isDirty])
const { mutateAsync, isLoading: isMutating } = const { mutateAsync, isLoading: isMutating } =
useAdminAddProductsToCollection(collection.id) useAdminAddProductsToCollection(collection.id)
@@ -98,11 +84,15 @@ export const AddProductsToCollectionForm = ({
const [rowSelection, setRowSelection] = useState<RowSelectionState>({}) const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
useEffect(() => { useEffect(() => {
form.setValue( setValue(
"product_ids", "product_ids",
Object.keys(rowSelection).filter((k) => rowSelection[k]) Object.keys(rowSelection).filter((k) => rowSelection[k]),
{
shouldDirty: true,
shouldTouch: true,
}
) )
}, [rowSelection]) }, [rowSelection, setValue])
const params = useQueryParams(["q", "order"]) const params = useQueryParams(["q", "order"])
@@ -151,7 +141,7 @@ export const AddProductsToCollectionForm = ({
* determine if they are added to the collection or not. * determine if they are added to the collection or not.
*/ */
queryClient.invalidateQueries(adminProductKeys.lists()) queryClient.invalidateQueries(adminProductKeys.lists())
onSuccessfulSubmit() handleSuccess()
}, },
} }
) )
@@ -169,29 +159,29 @@ export const AddProductsToCollectionForm = ({
} }
return ( return (
<Form {...form}> <RouteFocusModal.Form form={form}>
<form <form
onSubmit={handleSubmit} onSubmit={handleSubmit}
className="flex h-full flex-col overflow-hidden" className="flex h-full flex-col overflow-hidden"
> >
<FocusModal.Header> <RouteFocusModal.Header>
<div className="flex items-center justify-end gap-x-2"> <div className="flex items-center justify-end gap-x-2">
{form.formState.errors.product_ids && ( {form.formState.errors.product_ids && (
<Hint variant="error"> <Hint variant="error">
{form.formState.errors.product_ids.message} {form.formState.errors.product_ids.message}
</Hint> </Hint>
)} )}
<FocusModal.Close asChild> <RouteFocusModal.Close asChild>
<Button size="small" variant="secondary"> <Button size="small" variant="secondary">
{t("actions.cancel")} {t("actions.cancel")}
</Button> </Button>
</FocusModal.Close> </RouteFocusModal.Close>
<Button size="small" type="submit" isLoading={isMutating}> <Button size="small" type="submit" isLoading={isMutating}>
{t("actions.save")} {t("actions.save")}
</Button> </Button>
</div> </div>
</FocusModal.Header> </RouteFocusModal.Header>
<FocusModal.Body className="flex h-full w-full flex-col items-center divide-y overflow-y-auto"> <RouteFocusModal.Body className="flex h-full w-full flex-col items-center divide-y overflow-y-auto">
{!noRecords && ( {!noRecords && (
<div className="flex w-full items-center justify-between px-6 py-4"> <div className="flex w-full items-center justify-between px-6 py-4">
<div></div> <div></div>
@@ -291,9 +281,9 @@ export const AddProductsToCollectionForm = ({
{/* TODO: fix this, and add NoRecords as well */} {/* TODO: fix this, and add NoRecords as well */}
</div> </div>
)} )}
</FocusModal.Body> </RouteFocusModal.Body>
</form> </form>
</Form> </RouteFocusModal.Form>
) )
} }
@@ -1,15 +1,10 @@
import { FocusModal } from "@medusajs/ui" import { RouteFocusModal } from "../../../components/route-modal"
import { useRouteModalState } from "../../../hooks/use-route-modal-state"
import { CreateCollectionForm } from "./components/create-collection-form" import { CreateCollectionForm } from "./components/create-collection-form"
export const CollectionCreate = () => { export const CollectionCreate = () => {
const [open, onOpenChange, subscribe] = useRouteModalState()
return ( return (
<FocusModal open={open} onOpenChange={onOpenChange}> <RouteFocusModal>
<FocusModal.Content> <CreateCollectionForm />
<CreateCollectionForm subscribe={subscribe} /> </RouteFocusModal>
</FocusModal.Content>
</FocusModal>
) )
} }
@@ -1,27 +1,24 @@
import { zodResolver } from "@hookform/resolvers/zod" import { zodResolver } from "@hookform/resolvers/zod"
import { Button, FocusModal, Heading, Input, Text } from "@medusajs/ui" import { Button, Heading, Input, Text } from "@medusajs/ui"
import { useAdminCreateCollection } from "medusa-react" import { useAdminCreateCollection } from "medusa-react"
import { useEffect } from "react"
import { useForm } from "react-hook-form" import { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
import { useNavigate } from "react-router-dom"
import * as zod from "zod" import * as zod from "zod"
import { Form } from "../../../../../components/common/form"
type CreateCollectionFormProps = { import { Form } from "../../../../../components/common/form"
subscribe: (state: boolean) => void import {
} RouteFocusModal,
useRouteModal,
} from "../../../../../components/route-modal"
const CreateCollectionSchema = zod.object({ const CreateCollectionSchema = zod.object({
title: zod.string().min(1), title: zod.string().min(1),
handle: zod.string().optional(), handle: zod.string().optional(),
}) })
export const CreateCollectionForm = ({ export const CreateCollectionForm = () => {
subscribe,
}: CreateCollectionFormProps) => {
const { t } = useTranslation() const { t } = useTranslation()
const navigate = useNavigate() const { handleSuccess } = useRouteModal()
const form = useForm<zod.infer<typeof CreateCollectionSchema>>({ const form = useForm<zod.infer<typeof CreateCollectionSchema>>({
defaultValues: { defaultValues: {
@@ -31,34 +28,26 @@ export const CreateCollectionForm = ({
resolver: zodResolver(CreateCollectionSchema), resolver: zodResolver(CreateCollectionSchema),
}) })
const {
formState: { isDirty },
} = form
useEffect(() => {
subscribe(isDirty)
}, [isDirty])
const { mutateAsync, isLoading } = useAdminCreateCollection() const { mutateAsync, isLoading } = useAdminCreateCollection()
const handleSubmit = form.handleSubmit(async (data) => { const handleSubmit = form.handleSubmit(async (data) => {
await mutateAsync(data, { await mutateAsync(data, {
onSuccess: ({ collection }) => { onSuccess: ({ collection }) => {
navigate(`/collections/${collection.id}`) handleSuccess(`/collections/${collection.id}`)
}, },
}) })
}) })
return ( return (
<Form {...form}> <RouteFocusModal.Form form={form}>
<form onSubmit={handleSubmit}> <form onSubmit={handleSubmit}>
<FocusModal.Header> <RouteFocusModal.Header>
<div className="flex items-center justify-end gap-x-2"> <div className="flex items-center justify-end gap-x-2">
<FocusModal.Close asChild> <RouteFocusModal.Close asChild>
<Button size="small" variant="secondary"> <Button size="small" variant="secondary">
{t("actions.cancel")} {t("actions.cancel")}
</Button> </Button>
</FocusModal.Close> </RouteFocusModal.Close>
<Button <Button
size="small" size="small"
variant="primary" variant="primary"
@@ -68,8 +57,8 @@ export const CreateCollectionForm = ({
{t("actions.create")} {t("actions.create")}
</Button> </Button>
</div> </div>
</FocusModal.Header> </RouteFocusModal.Header>
<FocusModal.Body className="flex flex-col items-center py-16"> <RouteFocusModal.Body className="flex flex-col items-center py-16">
<div className="flex w-full max-w-[720px] flex-col gap-y-8"> <div className="flex w-full max-w-[720px] flex-col gap-y-8">
<div> <div>
<Heading>{t("collections.createCollection")}</Heading> <Heading>{t("collections.createCollection")}</Heading>
@@ -131,8 +120,8 @@ export const CreateCollectionForm = ({
/> />
</div> </div>
</div> </div>
</FocusModal.Body> </RouteFocusModal.Body>
</form> </form>
</Form> </RouteFocusModal.Form>
) )
} }
@@ -1,8 +1,8 @@
import { Drawer, Heading } from "@medusajs/ui" import { Heading } from "@medusajs/ui"
import { useAdminCollection } from "medusa-react" import { useAdminCollection } from "medusa-react"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
import { useParams } from "react-router-dom" import { useParams } from "react-router-dom"
import { useRouteModalState } from "../../../hooks/use-route-modal-state" import { RouteDrawer } from "../../../components/route-modal"
import { EditCollectionForm } from "./components/edit-collection-form" import { EditCollectionForm } from "./components/edit-collection-form"
export const CollectionEdit = () => { export const CollectionEdit = () => {
@@ -10,30 +10,18 @@ export const CollectionEdit = () => {
const { t } = useTranslation() const { t } = useTranslation()
const { collection, isLoading, isError, error } = useAdminCollection(id!) const { collection, isLoading, isError, error } = useAdminCollection(id!)
const [open, onOpenChange, subscribe] = useRouteModalState()
const handleSuccessfulSubmit = () => {
onOpenChange(false, true)
}
if (isError) { if (isError) {
throw error throw error
} }
return ( return (
<Drawer open={open} onOpenChange={onOpenChange}> <RouteDrawer>
<Drawer.Content> <RouteDrawer.Header>
<Drawer.Header>
<Heading>{t("collections.editCollection")}</Heading> <Heading>{t("collections.editCollection")}</Heading>
</Drawer.Header> </RouteDrawer.Header>
{!isLoading && collection && ( {!isLoading && collection && (
<EditCollectionForm <EditCollectionForm collection={collection} />
collection={collection}
onSuccessfulSubmit={handleSuccessfulSubmit}
subscribe={subscribe}
/>
)} )}
</Drawer.Content> </RouteDrawer>
</Drawer>
) )
} }
@@ -1,17 +1,19 @@
import { zodResolver } from "@hookform/resolvers/zod" import { zodResolver } from "@hookform/resolvers/zod"
import type { ProductCollection } from "@medusajs/medusa" import type { ProductCollection } from "@medusajs/medusa"
import { Button, Drawer, Input, Text } from "@medusajs/ui" import { Button, Input, Text } from "@medusajs/ui"
import { useAdminUpdateCollection } from "medusa-react" import { useAdminUpdateCollection } from "medusa-react"
import { useEffect } from "react"
import { useForm } from "react-hook-form" import { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
import * as zod from "zod" import * as zod from "zod"
import { Form } from "../../../../../components/common/form" import { Form } from "../../../../../components/common/form"
import {
RouteDrawer,
useRouteModal,
} from "../../../../../components/route-modal"
type EditCollectionFormProps = { type EditCollectionFormProps = {
collection: ProductCollection collection: ProductCollection
subscribe: (state: boolean) => void
onSuccessfulSubmit: () => void
} }
const EditCollectionSchema = zod.object({ const EditCollectionSchema = zod.object({
@@ -19,12 +21,9 @@ const EditCollectionSchema = zod.object({
handle: zod.string().min(1), handle: zod.string().min(1),
}) })
export const EditCollectionForm = ({ export const EditCollectionForm = ({ collection }: EditCollectionFormProps) => {
collection,
onSuccessfulSubmit,
subscribe,
}: EditCollectionFormProps) => {
const { t } = useTranslation() const { t } = useTranslation()
const { handleSuccess } = useRouteModal()
const form = useForm<zod.infer<typeof EditCollectionSchema>>({ const form = useForm<zod.infer<typeof EditCollectionSchema>>({
defaultValues: { defaultValues: {
@@ -34,28 +33,20 @@ export const EditCollectionForm = ({
resolver: zodResolver(EditCollectionSchema), resolver: zodResolver(EditCollectionSchema),
}) })
const {
formState: { isDirty },
} = form
useEffect(() => {
subscribe(isDirty)
}, [isDirty])
const { mutateAsync, isLoading } = useAdminUpdateCollection(collection.id) const { mutateAsync, isLoading } = useAdminUpdateCollection(collection.id)
const handleSubmit = form.handleSubmit(async (data) => { const handleSubmit = form.handleSubmit(async (data) => {
await mutateAsync(data, { await mutateAsync(data, {
onSuccess: () => { onSuccess: () => {
onSuccessfulSubmit() handleSuccess()
}, },
}) })
}) })
return ( return (
<Form {...form}> <RouteDrawer.Form form={form}>
<form onSubmit={handleSubmit} className="flex flex-1 flex-col"> <form onSubmit={handleSubmit} className="flex flex-1 flex-col">
<Drawer.Body> <RouteDrawer.Body>
<div className="flex flex-col gap-y-4"> <div className="flex flex-col gap-y-4">
<Form.Field <Form.Field
control={form.control} control={form.control}
@@ -102,20 +93,20 @@ export const EditCollectionForm = ({
}} }}
/> />
</div> </div>
</Drawer.Body> </RouteDrawer.Body>
<Drawer.Footer> <RouteDrawer.Footer>
<div className="flex items-center gap-x-2"> <div className="flex items-center gap-x-2">
<Drawer.Close asChild> <RouteDrawer.Close asChild>
<Button size="small" variant="secondary"> <Button size="small" variant="secondary">
{t("actions.cancel")} {t("actions.cancel")}
</Button> </Button>
</Drawer.Close> </RouteDrawer.Close>
<Button size="small" type="submit" isLoading={isLoading}> <Button size="small" type="submit" isLoading={isLoading}>
{t("actions.save")} {t("actions.save")}
</Button> </Button>
</div> </div>
</Drawer.Footer> </RouteDrawer.Footer>
</form> </form>
</Form> </RouteDrawer.Form>
) )
} }
@@ -1,14 +1,6 @@
import { zodResolver } from "@hookform/resolvers/zod" import { zodResolver } from "@hookform/resolvers/zod"
import { Customer } from "@medusajs/medusa" import { Customer } from "@medusajs/medusa"
import { import { Button, Checkbox, Hint, Table, Tooltip, clx } from "@medusajs/ui"
Button,
Checkbox,
FocusModal,
Hint,
Table,
Tooltip,
clx,
} from "@medusajs/ui"
import { import {
PaginationState, PaginationState,
RowSelectionState, RowSelectionState,
@@ -25,21 +17,23 @@ import {
import { useEffect, useMemo, useState } from "react" import { useEffect, useMemo, useState } from "react"
import { useForm } from "react-hook-form" import { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
import { useNavigate } from "react-router-dom"
import * as zod from "zod" import * as zod from "zod"
import { import {
NoRecords, NoRecords,
NoResults, NoResults,
} from "../../../../../components/common/empty-table-content" } from "../../../../../components/common/empty-table-content"
import { Form } from "../../../../../components/common/form"
import { Query } from "../../../../../components/filtering/query" import { Query } from "../../../../../components/filtering/query"
import { LocalizedTablePagination } from "../../../../../components/localization/localized-table-pagination" import { LocalizedTablePagination } from "../../../../../components/localization/localized-table-pagination"
import {
RouteFocusModal,
useRouteModal,
} from "../../../../../components/route-modal"
import { useQueryParams } from "../../../../../hooks/use-query-params" import { useQueryParams } from "../../../../../hooks/use-query-params"
import { queryClient } from "../../../../../lib/medusa" import { queryClient } from "../../../../../lib/medusa"
type AddCustomersFormProps = { type AddCustomersFormProps = {
customerGroupId: string customerGroupId: string
subscribe: (state: boolean) => void
} }
const AddCustomersSchema = zod.object({ const AddCustomersSchema = zod.object({
@@ -50,10 +44,9 @@ const PAGE_SIZE = 10
export const AddCustomersForm = ({ export const AddCustomersForm = ({
customerGroupId, customerGroupId,
subscribe,
}: AddCustomersFormProps) => { }: AddCustomersFormProps) => {
const navigate = useNavigate()
const { t } = useTranslation() const { t } = useTranslation()
const { handleSuccess } = useRouteModal()
const form = useForm<zod.infer<typeof AddCustomersSchema>>({ const form = useForm<zod.infer<typeof AddCustomersSchema>>({
defaultValues: { defaultValues: {
@@ -62,13 +55,7 @@ export const AddCustomersForm = ({
resolver: zodResolver(AddCustomersSchema), resolver: zodResolver(AddCustomersSchema),
}) })
const { const { setValue } = form
formState: { isDirty },
} = form
useEffect(() => {
subscribe(isDirty)
}, [isDirty])
const [{ pageIndex, pageSize }, setPagination] = useState<PaginationState>({ const [{ pageIndex, pageSize }, setPagination] = useState<PaginationState>({
pageIndex: 0, pageIndex: 0,
@@ -86,11 +73,15 @@ export const AddCustomersForm = ({
const [rowSelection, setRowSelection] = useState<RowSelectionState>({}) const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
useEffect(() => { useEffect(() => {
form.setValue( setValue(
"customer_ids", "customer_ids",
Object.keys(rowSelection).filter((k) => rowSelection[k]) Object.keys(rowSelection).filter((k) => rowSelection[k]),
{
shouldDirty: true,
shouldTouch: true,
}
) )
}, [rowSelection]) }, [rowSelection, setValue])
const params = useQueryParams(["q"]) const params = useQueryParams(["q"])
const { customers, count, isLoading, isError, error } = useAdminCustomers({ const { customers, count, isLoading, isError, error } = useAdminCustomers({
@@ -131,7 +122,7 @@ export const AddCustomersForm = ({
{ {
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries(adminCustomerKeys.lists()) queryClient.invalidateQueries(adminCustomerKeys.lists())
navigate(`/customer-groups/${customerGroupId}`) handleSuccess(`/customer-groups/${customerGroupId}`)
}, },
} }
) )
@@ -147,23 +138,23 @@ export const AddCustomersForm = ({
} }
return ( return (
<Form {...form}> <RouteFocusModal.Form form={form}>
<form <form
className="flex h-full flex-col overflow-hidden" className="flex h-full flex-col overflow-hidden"
onSubmit={handleSubmit} onSubmit={handleSubmit}
> >
<FocusModal.Header> <RouteFocusModal.Header>
<div className="flex items-center justify-end gap-x-2"> <div className="flex items-center justify-end gap-x-2">
{form.formState.errors.customer_ids && ( {form.formState.errors.customer_ids && (
<Hint variant="error"> <Hint variant="error">
{form.formState.errors.customer_ids.message} {form.formState.errors.customer_ids.message}
</Hint> </Hint>
)} )}
<FocusModal.Close asChild> <RouteFocusModal.Close asChild>
<Button variant="secondary" size="small"> <Button variant="secondary" size="small">
{t("actions.cancel")} {t("actions.cancel")}
</Button> </Button>
</FocusModal.Close> </RouteFocusModal.Close>
<Button <Button
type="submit" type="submit"
variant="primary" variant="primary"
@@ -173,8 +164,8 @@ export const AddCustomersForm = ({
{t("general.add")} {t("general.add")}
</Button> </Button>
</div> </div>
</FocusModal.Header> </RouteFocusModal.Header>
<FocusModal.Body className="flex h-full w-full flex-col items-center divide-y overflow-y-auto"> <RouteFocusModal.Body className="flex h-full w-full flex-col items-center divide-y overflow-y-auto">
{noRecords ? ( {noRecords ? (
<div className="flex w-full flex-1 items-center justify-center"> <div className="flex w-full flex-1 items-center justify-center">
<NoRecords /> <NoRecords />
@@ -259,9 +250,9 @@ export const AddCustomersForm = ({
/> />
</div> </div>
)} )}
</FocusModal.Body> </RouteFocusModal.Body>
</form> </form>
</Form> </RouteFocusModal.Form>
) )
} }
@@ -1,18 +1,14 @@
import { FocusModal } from "@medusajs/ui"
import { useParams } from "react-router-dom" import { useParams } from "react-router-dom"
import { useRouteModalState } from "../../../hooks/use-route-modal-state"
import { RouteFocusModal } from "../../../components/route-modal"
import { AddCustomersForm } from "./components/add-customers-form" import { AddCustomersForm } from "./components/add-customers-form"
export const CustomerGroupAddCustomers = () => { export const CustomerGroupAddCustomers = () => {
const [open, onOpenChange, subscribe] = useRouteModalState()
const { id } = useParams() const { id } = useParams()
return ( return (
<FocusModal open={open} onOpenChange={onOpenChange}> <RouteFocusModal>
<FocusModal.Content> <AddCustomersForm customerGroupId={id!} />
<AddCustomersForm customerGroupId={id!} subscribe={subscribe} /> </RouteFocusModal>
</FocusModal.Content>
</FocusModal>
) )
} }
@@ -1,26 +1,23 @@
import { zodResolver } from "@hookform/resolvers/zod" import { zodResolver } from "@hookform/resolvers/zod"
import { Button, FocusModal, Heading, Input, Text } from "@medusajs/ui" import { Button, Heading, Input, Text } from "@medusajs/ui"
import { useAdminCreateCustomerGroup } from "medusa-react" import { useAdminCreateCustomerGroup } from "medusa-react"
import { useEffect } from "react"
import { useForm } from "react-hook-form" import { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
import { useNavigate } from "react-router-dom"
import * as zod from "zod" import * as zod from "zod"
import { Form } from "../../../../../components/common/form"
type CreateCustomerGroupFormProps = { import { Form } from "../../../../../components/common/form"
subscribe: (state: boolean) => void import {
} RouteFocusModal,
useRouteModal,
} from "../../../../../components/route-modal"
const CreateCustomerGroupSchema = zod.object({ const CreateCustomerGroupSchema = zod.object({
name: zod.string().min(1), name: zod.string().min(1),
}) })
export const CreateCustomerGroupForm = ({ export const CreateCustomerGroupForm = () => {
subscribe,
}: CreateCustomerGroupFormProps) => {
const { t } = useTranslation() const { t } = useTranslation()
const navigate = useNavigate() const { handleSuccess } = useRouteModal()
const form = useForm<zod.infer<typeof CreateCustomerGroupSchema>>({ const form = useForm<zod.infer<typeof CreateCustomerGroupSchema>>({
defaultValues: { defaultValues: {
@@ -29,14 +26,6 @@ export const CreateCustomerGroupForm = ({
resolver: zodResolver(CreateCustomerGroupSchema), resolver: zodResolver(CreateCustomerGroupSchema),
}) })
const {
formState: { isDirty },
} = form
useEffect(() => {
subscribe(isDirty)
}, [isDirty])
const { mutateAsync, isLoading } = useAdminCreateCustomerGroup() const { mutateAsync, isLoading } = useAdminCreateCustomerGroup()
const handleSubmit = form.handleSubmit(async (data) => { const handleSubmit = form.handleSubmit(async (data) => {
@@ -46,22 +35,22 @@ export const CreateCustomerGroupForm = ({
}, },
{ {
onSuccess: ({ customer_group }) => { onSuccess: ({ customer_group }) => {
navigate(`/customer-groups/${customer_group.id}`) handleSuccess(`/customer-groups/${customer_group.id}`)
}, },
} }
) )
}) })
return ( return (
<Form {...form}> <RouteFocusModal.Form form={form}>
<form onSubmit={handleSubmit}> <form onSubmit={handleSubmit}>
<FocusModal.Header> <RouteFocusModal.Header>
<div className="flex items-center justify-end gap-x-2"> <div className="flex items-center justify-end gap-x-2">
<FocusModal.Close asChild> <RouteFocusModal.Close asChild>
<Button variant="secondary" size="small"> <Button variant="secondary" size="small">
{t("actions.cancel")} {t("actions.cancel")}
</Button> </Button>
</FocusModal.Close> </RouteFocusModal.Close>
<Button <Button
type="submit" type="submit"
variant="primary" variant="primary"
@@ -71,8 +60,8 @@ export const CreateCustomerGroupForm = ({
{t("actions.create")} {t("actions.create")}
</Button> </Button>
</div> </div>
</FocusModal.Header> </RouteFocusModal.Header>
<FocusModal.Body className="flex flex-col items-center pt-[72px]"> <RouteFocusModal.Body className="flex flex-col items-center pt-[72px]">
<div className="flex w-full max-w-[720px] flex-col gap-y-8"> <div className="flex w-full max-w-[720px] flex-col gap-y-8">
<div> <div>
<Heading>{t("customerGroups.createCustomerGroup")}</Heading> <Heading>{t("customerGroups.createCustomerGroup")}</Heading>
@@ -98,8 +87,8 @@ export const CreateCustomerGroupForm = ({
/> />
</div> </div>
</div> </div>
</FocusModal.Body> </RouteFocusModal.Body>
</form> </form>
</Form> </RouteFocusModal.Form>
) )
} }
@@ -1,15 +1,10 @@
import { FocusModal } from "@medusajs/ui" import { RouteFocusModal } from "../../../components/route-modal"
import { useRouteModalState } from "../../../hooks/use-route-modal-state"
import { CreateCustomerGroupForm } from "./components/create-customer-group-form" import { CreateCustomerGroupForm } from "./components/create-customer-group-form"
export const CustomerGroupCreate = () => { export const CustomerGroupCreate = () => {
const [open, onOpenChange, subscribe] = useRouteModalState()
return ( return (
<FocusModal open={open} onOpenChange={onOpenChange}> <RouteFocusModal>
<FocusModal.Content> <CreateCustomerGroupForm />
<CreateCustomerGroupForm subscribe={subscribe} /> </RouteFocusModal>
</FocusModal.Content>
</FocusModal>
) )
} }
@@ -1,17 +1,18 @@
import { zodResolver } from "@hookform/resolvers/zod" import { zodResolver } from "@hookform/resolvers/zod"
import { CustomerGroup } from "@medusajs/medusa" import { CustomerGroup } from "@medusajs/medusa"
import { Button, Drawer, Input } from "@medusajs/ui" import { Button, Input } from "@medusajs/ui"
import { useAdminUpdateCustomerGroup } from "medusa-react" import { useAdminUpdateCustomerGroup } from "medusa-react"
import { useEffect } from "react"
import { useForm } from "react-hook-form" import { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
import * as z from "zod" import * as z from "zod"
import { Form } from "../../../../../components/common/form" import { Form } from "../../../../../components/common/form"
import {
RouteDrawer,
useRouteModal,
} from "../../../../../components/route-modal"
type EditCustomerGroupFormProps = { type EditCustomerGroupFormProps = {
group: CustomerGroup group: CustomerGroup
onSuccessfulSubmit: () => void
subscribe: (state: boolean) => void
} }
const EditCustomerGroupSchema = z.object({ const EditCustomerGroupSchema = z.object({
@@ -20,10 +21,9 @@ const EditCustomerGroupSchema = z.object({
export const EditCustomerGroupForm = ({ export const EditCustomerGroupForm = ({
group, group,
onSuccessfulSubmit,
subscribe,
}: EditCustomerGroupFormProps) => { }: EditCustomerGroupFormProps) => {
const { t } = useTranslation() const { t } = useTranslation()
const { handleSuccess } = useRouteModal()
const form = useForm<z.infer<typeof EditCustomerGroupSchema>>({ const form = useForm<z.infer<typeof EditCustomerGroupSchema>>({
defaultValues: { defaultValues: {
@@ -32,31 +32,23 @@ export const EditCustomerGroupForm = ({
resolver: zodResolver(EditCustomerGroupSchema), resolver: zodResolver(EditCustomerGroupSchema),
}) })
const {
formState: { isDirty },
} = form
useEffect(() => {
subscribe(isDirty)
}, [isDirty])
const { mutateAsync, isLoading } = useAdminUpdateCustomerGroup(group.id) const { mutateAsync, isLoading } = useAdminUpdateCustomerGroup(group.id)
const handleSubmit = form.handleSubmit(async (data) => { const handleSubmit = form.handleSubmit(async (data) => {
await mutateAsync(data, { await mutateAsync(data, {
onSuccess: () => { onSuccess: () => {
onSuccessfulSubmit() handleSuccess()
}, },
}) })
}) })
return ( return (
<Form {...form}> <RouteDrawer.Form form={form}>
<form <form
onSubmit={handleSubmit} onSubmit={handleSubmit}
className="flex flex-1 flex-col overflow-hidden" className="flex flex-1 flex-col overflow-hidden"
> >
<Drawer.Body className="flex max-w-full flex-1 flex-col gap-y-8 overflow-y-auto"> <RouteDrawer.Body className="flex max-w-full flex-1 flex-col gap-y-8 overflow-y-auto">
<Form.Field <Form.Field
control={form.control} control={form.control}
name="name" name="name"
@@ -72,20 +64,20 @@ export const EditCustomerGroupForm = ({
) )
}} }}
/> />
</Drawer.Body> </RouteDrawer.Body>
<Drawer.Footer> <RouteDrawer.Footer>
<div className="flex items-center justify-end gap-x-2"> <div className="flex items-center justify-end gap-x-2">
<Drawer.Close asChild> <RouteDrawer.Close asChild>
<Button size="small" variant="secondary"> <Button size="small" variant="secondary">
{t("actions.cancel")} {t("actions.cancel")}
</Button> </Button>
</Drawer.Close> </RouteDrawer.Close>
<Button size="small" type="submit" isLoading={isLoading}> <Button size="small" type="submit" isLoading={isLoading}>
{t("actions.save")} {t("actions.save")}
</Button> </Button>
</div> </div>
</Drawer.Footer> </RouteDrawer.Footer>
</form> </form>
</Form> </RouteDrawer.Form>
) )
} }
@@ -1,13 +1,11 @@
import { Drawer, Heading } from "@medusajs/ui" import { Heading } from "@medusajs/ui"
import { useAdminCustomerGroup } from "medusa-react" import { useAdminCustomerGroup } from "medusa-react"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
import { useParams } from "react-router-dom" import { useParams } from "react-router-dom"
import { useRouteModalState } from "../../../hooks/use-route-modal-state" import { RouteDrawer } from "../../../components/route-modal"
import { EditCustomerGroupForm } from "./components/edit-customer-group-form" import { EditCustomerGroupForm } from "./components/edit-customer-group-form"
export const CustomerGroupEdit = () => { export const CustomerGroupEdit = () => {
const [open, onOpenChange, subscribe] = useRouteModalState()
const { id } = useParams() const { id } = useParams()
const { customer_group, isLoading, isError, error } = useAdminCustomerGroup( const { customer_group, isLoading, isError, error } = useAdminCustomerGroup(
id! id!
@@ -15,28 +13,18 @@ export const CustomerGroupEdit = () => {
const { t } = useTranslation() const { t } = useTranslation()
const handleSuccessfulSubmit = () => {
onOpenChange(false, true)
}
if (isError) { if (isError) {
throw error throw error
} }
return ( return (
<Drawer open={open} onOpenChange={onOpenChange}> <RouteDrawer>
<Drawer.Content> <RouteDrawer.Header>
<Drawer.Header>
<Heading>{t("customerGroups.editCustomerGroup")}</Heading> <Heading>{t("customerGroups.editCustomerGroup")}</Heading>
</Drawer.Header> </RouteDrawer.Header>
{!isLoading && customer_group && ( {!isLoading && customer_group && (
<EditCustomerGroupForm <EditCustomerGroupForm group={customer_group} />
subscribe={subscribe}
onSuccessfulSubmit={handleSuccessfulSubmit}
group={customer_group}
/>
)} )}
</Drawer.Content> </RouteDrawer>
</Drawer>
) )
} }
@@ -1,16 +1,15 @@
import { zodResolver } from "@hookform/resolvers/zod" import { zodResolver } from "@hookform/resolvers/zod"
import { Button, FocusModal, Heading, Input, Text } from "@medusajs/ui" import { Button, Heading, Input, Text } from "@medusajs/ui"
import { useAdminCreateCustomer } from "medusa-react" import { useAdminCreateCustomer } from "medusa-react"
import { useEffect } from "react"
import { useForm } from "react-hook-form" import { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
import { useNavigate } from "react-router-dom"
import * as zod from "zod" import * as zod from "zod"
import { Form } from "../../../../../components/common/form"
type CreateCustomerFormProps = { import { Form } from "../../../../../components/common/form"
subscribe: (state: boolean) => void import {
} RouteFocusModal,
useRouteModal,
} from "../../../../../components/route-modal"
const CreateCustomerSchema = zod const CreateCustomerSchema = zod
.object({ .object({
@@ -31,9 +30,9 @@ const CreateCustomerSchema = zod
} }
}) })
export const CreateCustomerForm = ({ subscribe }: CreateCustomerFormProps) => { export const CreateCustomerForm = () => {
const { t } = useTranslation() const { t } = useTranslation()
const navigate = useNavigate() const { handleSuccess } = useRouteModal()
const form = useForm<zod.infer<typeof CreateCustomerSchema>>({ const form = useForm<zod.infer<typeof CreateCustomerSchema>>({
defaultValues: { defaultValues: {
@@ -46,13 +45,6 @@ export const CreateCustomerForm = ({ subscribe }: CreateCustomerFormProps) => {
}, },
resolver: zodResolver(CreateCustomerSchema), resolver: zodResolver(CreateCustomerSchema),
}) })
const {
formState: { isDirty },
} = form
useEffect(() => {
subscribe(isDirty)
}, [isDirty])
const { mutateAsync, isLoading } = useAdminCreateCustomer() const { mutateAsync, isLoading } = useAdminCreateCustomer()
@@ -67,22 +59,22 @@ export const CreateCustomerForm = ({ subscribe }: CreateCustomerFormProps) => {
}, },
{ {
onSuccess: ({ customer }) => { onSuccess: ({ customer }) => {
navigate(`/customers/${customer.id}`, { replace: true }) handleSuccess(`/customers/${customer.id}`)
}, },
} }
) )
}) })
return ( return (
<Form {...form}> <RouteFocusModal.Form form={form}>
<form onSubmit={handleSubmit}> <form onSubmit={handleSubmit}>
<FocusModal.Header> <RouteFocusModal.Header>
<div className="flex items-center justify-end gap-x-2"> <div className="flex items-center justify-end gap-x-2">
<FocusModal.Close asChild> <RouteFocusModal.Close asChild>
<Button size="small" variant="secondary"> <Button size="small" variant="secondary">
{t("actions.cancel")} {t("actions.cancel")}
</Button> </Button>
</FocusModal.Close> </RouteFocusModal.Close>
<Button <Button
size="small" size="small"
variant="primary" variant="primary"
@@ -92,8 +84,8 @@ export const CreateCustomerForm = ({ subscribe }: CreateCustomerFormProps) => {
{t("actions.create")} {t("actions.create")}
</Button> </Button>
</div> </div>
</FocusModal.Header> </RouteFocusModal.Header>
<FocusModal.Body className="flex flex-col items-center py-16"> <RouteFocusModal.Body className="flex flex-col items-center py-16">
<div className="flex w-full max-w-[720px] flex-col gap-y-8"> <div className="flex w-full max-w-[720px] flex-col gap-y-8">
<div> <div>
<Heading>{t("customers.createCustomer")}</Heading> <Heading>{t("customers.createCustomer")}</Heading>
@@ -214,8 +206,8 @@ export const CreateCustomerForm = ({ subscribe }: CreateCustomerFormProps) => {
</div> </div>
</div> </div>
</div> </div>
</FocusModal.Body> </RouteFocusModal.Body>
</form> </form>
</Form> </RouteFocusModal.Form>
) )
} }
@@ -1,15 +1,10 @@
import { FocusModal } from "@medusajs/ui" import { RouteFocusModal } from "../../../components/route-modal"
import { useRouteModalState } from "../../../hooks/use-route-modal-state"
import { CreateCustomerForm } from "./components/create-customer-form" import { CreateCustomerForm } from "./components/create-customer-form"
export const CustomerCreate = () => { export const CustomerCreate = () => {
const [open, onOpenChange, subscribe] = useRouteModalState()
return ( return (
<FocusModal open={open} onOpenChange={onOpenChange}> <RouteFocusModal>
<FocusModal.Content> <CreateCustomerForm />
<CreateCustomerForm subscribe={subscribe} /> </RouteFocusModal>
</FocusModal.Content>
</FocusModal>
) )
} }
@@ -1,17 +1,19 @@
import { zodResolver } from "@hookform/resolvers/zod" import { zodResolver } from "@hookform/resolvers/zod"
import { Customer } from "@medusajs/medusa" import { Customer } from "@medusajs/medusa"
import { Button, Drawer, Input } from "@medusajs/ui" import { Button, Input } from "@medusajs/ui"
import { useAdminUpdateCustomer } from "medusa-react" import { useAdminUpdateCustomer } from "medusa-react"
import { useEffect } from "react"
import { useForm } from "react-hook-form" import { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
import * as zod from "zod" import * as zod from "zod"
import { Form } from "../../../../../components/common/form" import { Form } from "../../../../../components/common/form"
import {
RouteDrawer,
useRouteModal,
} from "../../../../../components/route-modal"
type EditCustomerFormProps = { type EditCustomerFormProps = {
customer: Customer customer: Customer
subscribe: (state: boolean) => void
onSuccessfulSubmit: () => void
} }
const EditCustomerSchema = zod.object({ const EditCustomerSchema = zod.object({
@@ -21,12 +23,9 @@ const EditCustomerSchema = zod.object({
phone: zod.string().optional(), phone: zod.string().optional(),
}) })
export const EditCustomerForm = ({ export const EditCustomerForm = ({ customer }: EditCustomerFormProps) => {
customer,
subscribe,
onSuccessfulSubmit,
}: EditCustomerFormProps) => {
const { t } = useTranslation() const { t } = useTranslation()
const { handleSuccess } = useRouteModal()
const form = useForm<zod.infer<typeof EditCustomerSchema>>({ const form = useForm<zod.infer<typeof EditCustomerSchema>>({
defaultValues: { defaultValues: {
@@ -38,14 +37,6 @@ export const EditCustomerForm = ({
resolver: zodResolver(EditCustomerSchema), resolver: zodResolver(EditCustomerSchema),
}) })
const {
formState: { isDirty },
} = form
useEffect(() => {
subscribe(isDirty)
}, [isDirty])
const { mutateAsync, isLoading } = useAdminUpdateCustomer(customer.id) const { mutateAsync, isLoading } = useAdminUpdateCustomer(customer.id)
const handleSubmit = form.handleSubmit(async (data) => { const handleSubmit = form.handleSubmit(async (data) => {
@@ -58,16 +49,16 @@ export const EditCustomerForm = ({
}, },
{ {
onSuccess: () => { onSuccess: () => {
onSuccessfulSubmit() handleSuccess()
}, },
} }
) )
}) })
return ( return (
<Form {...form}> <RouteDrawer.Form form={form}>
<form onSubmit={handleSubmit} className="flex flex-1 flex-col"> <form onSubmit={handleSubmit} className="flex flex-1 flex-col">
<Drawer.Body> <RouteDrawer.Body>
<div className="flex flex-col gap-y-4"> <div className="flex flex-col gap-y-4">
<Form.Field <Form.Field
control={form.control} control={form.control}
@@ -130,14 +121,14 @@ export const EditCustomerForm = ({
}} }}
/> />
</div> </div>
</Drawer.Body> </RouteDrawer.Body>
<Drawer.Footer> <RouteDrawer.Footer>
<div className="flex items-center justify-end gap-x-2"> <div className="flex items-center justify-end gap-x-2">
<Drawer.Close asChild> <RouteDrawer.Close asChild>
<Button variant="secondary" size="small"> <Button variant="secondary" size="small">
{t("actions.cancel")} {t("actions.cancel")}
</Button> </Button>
</Drawer.Close> </RouteDrawer.Close>
<Button <Button
isLoading={isLoading} isLoading={isLoading}
type="submit" type="submit"
@@ -147,8 +138,8 @@ export const EditCustomerForm = ({
{t("actions.save")} {t("actions.save")}
</Button> </Button>
</div> </div>
</Drawer.Footer> </RouteDrawer.Footer>
</form> </form>
</Form> </RouteDrawer.Form>
) )
} }
@@ -1,39 +1,26 @@
import { Drawer, Heading } from "@medusajs/ui" import { Heading } from "@medusajs/ui"
import { useAdminCustomer } from "medusa-react" import { useAdminCustomer } from "medusa-react"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
import { useParams } from "react-router-dom" import { useParams } from "react-router-dom"
import { useRouteModalState } from "../../../hooks/use-route-modal-state" import { RouteDrawer } from "../../../components/route-modal"
import { EditCustomerForm } from "./components/edit-customer-form" import { EditCustomerForm } from "./components/edit-customer-form"
export const CustomerEdit = () => { export const CustomerEdit = () => {
const [open, onOpenChange, subscribe] = useRouteModalState()
const { t } = useTranslation() const { t } = useTranslation()
const { id } = useParams() const { id } = useParams()
const { customer, isLoading, isError, error } = useAdminCustomer(id!) const { customer, isLoading, isError, error } = useAdminCustomer(id!)
const handleSuccessfulSubmit = () => {
onOpenChange(false, true)
}
if (isError) { if (isError) {
throw error throw error
} }
return ( return (
<Drawer open={open} onOpenChange={onOpenChange}> <RouteDrawer>
<Drawer.Content> <RouteDrawer.Header>
<Drawer.Header>
<Heading>{t("customers.editCustomer")}</Heading> <Heading>{t("customers.editCustomer")}</Heading>
</Drawer.Header> </RouteDrawer.Header>
{!isLoading && customer && ( {!isLoading && customer && <EditCustomerForm customer={customer} />}
<EditCustomerForm </RouteDrawer>
customer={customer}
onSuccessfulSubmit={handleSuccessfulSubmit}
subscribe={subscribe}
/>
)}
</Drawer.Content>
</Drawer>
) )
} }
@@ -3,7 +3,6 @@ import {
Button, Button,
CurrencyInput, CurrencyInput,
DatePicker, DatePicker,
FocusModal,
Heading, Heading,
Input, Input,
Select, Select,
@@ -14,20 +13,19 @@ import {
} from "@medusajs/ui" } from "@medusajs/ui"
import * as Collapsible from "@radix-ui/react-collapsible" import * as Collapsible from "@radix-ui/react-collapsible"
import { useAdminCreateGiftCard, useAdminRegions } from "medusa-react" import { useAdminCreateGiftCard, useAdminRegions } from "medusa-react"
import { useEffect, useState } from "react" import { useState } from "react"
import { useForm, useWatch } from "react-hook-form" import { useForm, useWatch } from "react-hook-form"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
import * as zod from "zod" import * as zod from "zod"
import { useNavigate } from "react-router-dom"
import { Form } from "../../../../../components/common/form" import { Form } from "../../../../../components/common/form"
import {
RouteFocusModal,
useRouteModal,
} from "../../../../../components/route-modal"
import { currencies } from "../../../../../lib/currencies" import { currencies } from "../../../../../lib/currencies"
import { getDbAmount } from "../../../../../lib/money-amount-helpers" import { getDbAmount } from "../../../../../lib/money-amount-helpers"
type CreateGiftCardFormProps = {
subscribe: (state: boolean) => void
}
const CreateGiftCardSchema = zod.object({ const CreateGiftCardSchema = zod.object({
region_id: zod.string(), region_id: zod.string(),
value: zod.string(), value: zod.string(),
@@ -37,7 +35,7 @@ const CreateGiftCardSchema = zod.object({
personal_message: zod.string().optional(), personal_message: zod.string().optional(),
}) })
export const CreateGiftCardForm = ({ subscribe }: CreateGiftCardFormProps) => { export const CreateGiftCardForm = () => {
const [showDateFields, setShowDateFields] = useState(false) const [showDateFields, setShowDateFields] = useState(false)
const { regions } = useAdminRegions({ const { regions } = useAdminRegions({
@@ -45,7 +43,7 @@ export const CreateGiftCardForm = ({ subscribe }: CreateGiftCardFormProps) => {
fields: "id,name,currency_code", fields: "id,name,currency_code",
}) })
const { t } = useTranslation() const { t } = useTranslation()
const navigate = useNavigate() const { handleSuccess } = useRouteModal()
const form = useForm<zod.infer<typeof CreateGiftCardSchema>>({ const form = useForm<zod.infer<typeof CreateGiftCardSchema>>({
defaultValues: { defaultValues: {
@@ -59,11 +57,7 @@ export const CreateGiftCardForm = ({ subscribe }: CreateGiftCardFormProps) => {
resolver: zodResolver(CreateGiftCardSchema), resolver: zodResolver(CreateGiftCardSchema),
}) })
const { const { setValue, setError } = form
formState: { isDirty },
setValue,
setError,
} = form
const regionId = useWatch({ const regionId = useWatch({
control: form.control, control: form.control,
@@ -75,10 +69,6 @@ export const CreateGiftCardForm = ({ subscribe }: CreateGiftCardFormProps) => {
? currencies[currencyCode.toUpperCase()].symbol_native ? currencies[currencyCode.toUpperCase()].symbol_native
: undefined : undefined
useEffect(() => {
subscribe(isDirty)
}, [isDirty, subscribe])
const { mutateAsync, isLoading } = useAdminCreateGiftCard() const { mutateAsync, isLoading } = useAdminCreateGiftCard()
const handleOpenChange = (open: boolean) => { const handleOpenChange = (open: boolean) => {
@@ -95,7 +85,7 @@ export const CreateGiftCardForm = ({ subscribe }: CreateGiftCardFormProps) => {
if (!currencyCode) { if (!currencyCode) {
setError("region_id", { setError("region_id", {
type: "manual", type: "manual",
message: "Region not found", message: t("giftCards.selectRegionFirst"),
}) })
return return
@@ -114,31 +104,31 @@ export const CreateGiftCardForm = ({ subscribe }: CreateGiftCardFormProps) => {
}, },
{ {
onSuccess: ({ gift_card }) => { onSuccess: ({ gift_card }) => {
navigate(`../${gift_card.id}`, { replace: true }) handleSuccess(`../${gift_card.id}`)
}, },
} }
) )
}) })
return ( return (
<Form {...form}> <RouteFocusModal.Form form={form}>
<form <form
className="flex h-full flex-col overflow-hidden" className="flex h-full flex-col overflow-hidden"
onSubmit={handleSubmit} onSubmit={handleSubmit}
> >
<FocusModal.Header> <RouteFocusModal.Header>
<div className="flex items-center justify-end gap-x-2"> <div className="flex items-center justify-end gap-x-2">
<FocusModal.Close asChild> <RouteFocusModal.Close asChild>
<Button size="small" variant="secondary"> <Button size="small" variant="secondary">
{t("actions.cancel")} {t("actions.cancel")}
</Button> </Button>
</FocusModal.Close> </RouteFocusModal.Close>
<Button size="small" type="submit" isLoading={isLoading}> <Button size="small" type="submit" isLoading={isLoading}>
{t("actions.save")} {t("actions.save")}
</Button> </Button>
</div> </div>
</FocusModal.Header> </RouteFocusModal.Header>
<FocusModal.Body className="flex h-full w-full flex-col items-center overflow-y-auto py-16"> <RouteFocusModal.Body className="flex h-full w-full flex-col items-center overflow-y-auto py-16">
<div className="flex w-full max-w-[720px] flex-col gap-y-8"> <div className="flex w-full max-w-[720px] flex-col gap-y-8">
<div> <div>
<Heading>{t("giftCards.createGiftCard")}</Heading> <Heading>{t("giftCards.createGiftCard")}</Heading>
@@ -304,8 +294,8 @@ export const CreateGiftCardForm = ({ subscribe }: CreateGiftCardFormProps) => {
/> />
</div> </div>
</div> </div>
</FocusModal.Body> </RouteFocusModal.Body>
</form> </form>
</Form> </RouteFocusModal.Form>
) )
} }
@@ -1,15 +1,10 @@
import { FocusModal } from "@medusajs/ui" import { RouteFocusModal } from "../../../components/route-modal"
import { useRouteModalState } from "../../../hooks/use-route-modal-state"
import { CreateGiftCardForm } from "./components/create-gift-card-form" import { CreateGiftCardForm } from "./components/create-gift-card-form"
export const GiftCardCreate = () => { export const GiftCardCreate = () => {
const [open, onOpenChange, subscribe] = useRouteModalState()
return ( return (
<FocusModal open={open} onOpenChange={onOpenChange}> <RouteFocusModal>
<FocusModal.Content> <CreateGiftCardForm />
<CreateGiftCardForm subscribe={subscribe} /> </RouteFocusModal>
</FocusModal.Content>
</FocusModal>
) )
} }
@@ -4,19 +4,22 @@ import {
Button, Button,
CurrencyInput, CurrencyInput,
DatePicker, DatePicker,
Drawer,
Select, Select,
Switch, Switch,
Text, Text,
} from "@medusajs/ui" } from "@medusajs/ui"
import * as Collapsible from "@radix-ui/react-collapsible" import * as Collapsible from "@radix-ui/react-collapsible"
import { useAdminRegions, useAdminUpdateGiftCard } from "medusa-react" import { useAdminRegions, useAdminUpdateGiftCard } from "medusa-react"
import { useEffect, useState } from "react" import { useState } from "react"
import { useForm } from "react-hook-form" import { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
import * as zod from "zod" import * as zod from "zod"
import { Form } from "../../../../../components/common/form" import { Form } from "../../../../../components/common/form"
import {
RouteDrawer,
useRouteModal,
} from "../../../../../components/route-modal"
import { currencies } from "../../../../../lib/currencies" import { currencies } from "../../../../../lib/currencies"
import { isAxiosError } from "../../../../../lib/is-axios-error" import { isAxiosError } from "../../../../../lib/is-axios-error"
import { import {
@@ -26,8 +29,6 @@ import {
type EditGiftCardFormProps = { type EditGiftCardFormProps = {
giftCard: GiftCard giftCard: GiftCard
onSuccessfulSubmit: () => void
subscribe: (state: boolean) => void
} }
const EditGiftCardSchema = zod.object({ const EditGiftCardSchema = zod.object({
@@ -37,13 +38,12 @@ const EditGiftCardSchema = zod.object({
ends_at: zod.date().nullable(), ends_at: zod.date().nullable(),
}) })
export const EditGiftCardForm = ({ export const EditGiftCardForm = ({ giftCard }: EditGiftCardFormProps) => {
giftCard,
onSuccessfulSubmit,
subscribe,
}: EditGiftCardFormProps) => {
const { t } = useTranslation() const { t } = useTranslation()
const { handleSuccess } = useRouteModal()
const [showDateFields, setShowDateFields] = useState(!!giftCard.ends_at) const [showDateFields, setShowDateFields] = useState(!!giftCard.ends_at)
const form = useForm<zod.infer<typeof EditGiftCardSchema>>({ const form = useForm<zod.infer<typeof EditGiftCardSchema>>({
defaultValues: { defaultValues: {
region_id: giftCard.region_id, region_id: giftCard.region_id,
@@ -57,14 +57,7 @@ export const EditGiftCardForm = ({
resolver: zodResolver(EditGiftCardSchema), resolver: zodResolver(EditGiftCardSchema),
}) })
const { const { setValue } = form
formState: { isDirty },
setValue,
} = form
useEffect(() => {
subscribe(isDirty)
}, [isDirty, subscribe])
const { regions } = useAdminRegions({ const { regions } = useAdminRegions({
limit: 1000, limit: 1000,
@@ -117,7 +110,7 @@ export const EditGiftCardForm = ({
}, },
{ {
onSuccess: () => { onSuccess: () => {
onSuccessfulSubmit() handleSuccess()
}, },
onError: (error) => { onError: (error) => {
if (isAxiosError(error)) { if (isAxiosError(error)) {
@@ -132,19 +125,19 @@ export const EditGiftCardForm = ({
}) })
return ( return (
<Form {...form}> <RouteDrawer.Form form={form}>
<form <form
onSubmit={handleSubmit} onSubmit={handleSubmit}
className="flex flex-1 flex-col overflow-hidden" className="flex flex-1 flex-col overflow-hidden"
> >
<Drawer.Body className="flex flex-1 flex-col gap-y-8 overflow-auto"> <RouteDrawer.Body className="flex flex-1 flex-col gap-y-8 overflow-auto">
<Form.Field <Form.Field
control={form.control} control={form.control}
name="balance" name="balance"
render={({ field: { onChange, ...field } }) => { render={({ field: { onChange, ...field } }) => {
return ( return (
<Form.Item> <Form.Item>
<Form.Label>{t("fields.balance")}</Form.Label> <Form.Label>{t("giftCards.balance")}</Form.Label>
<Form.Control> <Form.Control>
<CurrencyInput <CurrencyInput
code={giftCard.region.currency_code.toUpperCase()} code={giftCard.region.currency_code.toUpperCase()}
@@ -258,20 +251,20 @@ export const EditGiftCardForm = ({
) )
}} }}
/> />
</Drawer.Body> </RouteDrawer.Body>
<Drawer.Footer> <RouteDrawer.Footer>
<div className="flex items-center gap-x-2"> <div className="flex items-center gap-x-2">
<Drawer.Close asChild> <RouteDrawer.Close asChild>
<Button size="small" variant="secondary"> <Button size="small" variant="secondary">
{t("actions.cancel")} {t("actions.cancel")}
</Button> </Button>
</Drawer.Close> </RouteDrawer.Close>
<Button size="small" type="submit" isLoading={isLoading}> <Button size="small" type="submit" isLoading={isLoading}>
{t("actions.save")} {t("actions.save")}
</Button> </Button>
</div> </div>
</Drawer.Footer> </RouteDrawer.Footer>
</form> </form>
</Form> </RouteDrawer.Form>
) )
} }
@@ -1,39 +1,26 @@
import { Drawer, Heading } from "@medusajs/ui" import { Heading } from "@medusajs/ui"
import { useAdminGiftCard } from "medusa-react" import { useAdminGiftCard } from "medusa-react"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
import { useParams } from "react-router-dom" import { useParams } from "react-router-dom"
import { useRouteModalState } from "../../../hooks/use-route-modal-state" import { RouteDrawer } from "../../../components/route-modal"
import { EditGiftCardForm } from "./components/edit-gift-card-form" import { EditGiftCardForm } from "./components/edit-gift-card-form"
export const GiftCardEdit = () => { export const GiftCardEdit = () => {
const { id } = useParams() const { id } = useParams()
const { t } = useTranslation() const { t } = useTranslation()
const [open, onOpenChange, subscribe] = useRouteModalState()
const { gift_card, isLoading, isError, error } = useAdminGiftCard(id!) const { gift_card, isLoading, isError, error } = useAdminGiftCard(id!)
const handleSuccessfulSubmit = () => {
onOpenChange(false, true)
}
if (isError) { if (isError) {
throw error throw error
} }
return ( return (
<Drawer open={open} onOpenChange={onOpenChange}> <RouteDrawer>
<Drawer.Content> <RouteDrawer.Header>
<Drawer.Header>
<Heading>{t("giftCards.editGiftCard")}</Heading> <Heading>{t("giftCards.editGiftCard")}</Heading>
</Drawer.Header> </RouteDrawer.Header>
{!isLoading && gift_card && ( {!isLoading && gift_card && <EditGiftCardForm giftCard={gift_card} />}
<EditGiftCardForm </RouteDrawer>
giftCard={gift_card}
onSuccessfulSubmit={handleSuccessfulSubmit}
subscribe={subscribe}
/>
)}
</Drawer.Content>
</Drawer>
) )
} }
@@ -1,15 +1,8 @@
import { FocusModal } from "@medusajs/ui"
import { useAdminAddLocationToSalesChannel } from "medusa-react" import { useAdminAddLocationToSalesChannel } from "medusa-react"
import { useRouteModalState } from "../../../hooks/use-route-modal-state" import { RouteFocusModal } from "../../../components/route-modal"
export const LocationAddSalesChannels = () => { export const LocationAddSalesChannels = () => {
const [open, onOpenChange] = useRouteModalState()
const { mutateAsync } = useAdminAddLocationToSalesChannel() // TODO: We need a batch mutation instead of this to avoid multiple requests const { mutateAsync } = useAdminAddLocationToSalesChannel() // TODO: We need a batch mutation instead of this to avoid multiple requests
return ( return <RouteFocusModal></RouteFocusModal>
<FocusModal open={open} onOpenChange={onOpenChange}>
<FocusModal.Content></FocusModal.Content>
</FocusModal>
)
} }
@@ -1,11 +1,12 @@
import { zodResolver } from "@hookform/resolvers/zod" import { zodResolver } from "@hookform/resolvers/zod"
import { Button, FocusModal, Heading, Input, Text } from "@medusajs/ui" import { Button, Heading, Input, Text } from "@medusajs/ui"
import { useAdminCreateStockLocation } from "medusa-react" import { useAdminCreateStockLocation } from "medusa-react"
import { useForm } from "react-hook-form" import { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
import * as zod from "zod" import * as zod from "zod"
import { CountrySelect } from "../../../../../components/common/country-select" import { CountrySelect } from "../../../../../components/common/country-select"
import { Form } from "../../../../../components/common/form" import { Form } from "../../../../../components/common/form"
import { RouteFocusModal } from "../../../../../components/route-modal"
const CreateLocationSchema = zod.object({ const CreateLocationSchema = zod.object({
name: zod.string().min(1), name: zod.string().min(1),
@@ -22,7 +23,7 @@ const CreateLocationSchema = zod.object({
}) })
export const CreateLocationForm = () => { export const CreateLocationForm = () => {
const { mutateAsync, isLoading } = useAdminCreateStockLocation() const { t } = useTranslation()
const form = useForm<zod.infer<typeof CreateLocationSchema>>({ const form = useForm<zod.infer<typeof CreateLocationSchema>>({
defaultValues: { defaultValues: {
@@ -41,7 +42,7 @@ export const CreateLocationForm = () => {
resolver: zodResolver(CreateLocationSchema), resolver: zodResolver(CreateLocationSchema),
}) })
const { t } = useTranslation() const { mutateAsync, isLoading } = useAdminCreateStockLocation()
const handleSubmit = form.handleSubmit(async (values) => { const handleSubmit = form.handleSubmit(async (values) => {
mutateAsync( mutateAsync(
@@ -56,24 +57,24 @@ export const CreateLocationForm = () => {
}) })
return ( return (
<Form {...form}> <RouteFocusModal.Form form={form}>
<form <form
onSubmit={handleSubmit} onSubmit={handleSubmit}
className="flex h-full flex-col overflow-hidden" className="flex h-full flex-col overflow-hidden"
> >
<FocusModal.Header> <RouteFocusModal.Header>
<div className="flex items-center justify-end gap-x-2"> <div className="flex items-center justify-end gap-x-2">
<FocusModal.Close asChild> <RouteFocusModal.Close asChild>
<Button size="small" variant="secondary"> <Button size="small" variant="secondary">
{t("actions.cancel")} {t("actions.cancel")}
</Button> </Button>
</FocusModal.Close> </RouteFocusModal.Close>
<Button type="submit" size="small" isLoading={isLoading}> <Button type="submit" size="small" isLoading={isLoading}>
{t("actions.save")} {t("actions.save")}
</Button> </Button>
</div> </div>
</FocusModal.Header> </RouteFocusModal.Header>
<FocusModal.Body className="flex flex-1 flex-col overflow-hidden"> <RouteFocusModal.Body className="flex flex-1 flex-col overflow-hidden">
<div className="flex flex-1 flex-col items-center overflow-y-auto"> <div className="flex flex-1 flex-col items-center overflow-y-auto">
<div className="flex w-full max-w-[720px] flex-col gap-y-8 px-2 py-16"> <div className="flex w-full max-w-[720px] flex-col gap-y-8 px-2 py-16">
<div> <div>
@@ -227,8 +228,8 @@ export const CreateLocationForm = () => {
</div> </div>
</div> </div>
</div> </div>
</FocusModal.Body> </RouteFocusModal.Body>
</form> </form>
</Form> </RouteFocusModal.Form>
) )
} }
@@ -1,15 +1,10 @@
import { FocusModal } from "@medusajs/ui" import { RouteFocusModal } from "../../../components/route-modal"
import { useRouteModalState } from "../../../hooks/use-route-modal-state"
import { CreateLocationForm } from "./components/create-location-form" import { CreateLocationForm } from "./components/create-location-form"
export const LocationCreate = () => { export const LocationCreate = () => {
const [open, onOpenChange] = useRouteModalState()
return ( return (
<FocusModal open={open} onOpenChange={onOpenChange}> <RouteFocusModal>
<FocusModal.Content>
<CreateLocationForm /> <CreateLocationForm />
</FocusModal.Content> </RouteFocusModal>
</FocusModal>
) )
} }
@@ -1,12 +1,14 @@
import { zodResolver } from "@hookform/resolvers/zod" import { zodResolver } from "@hookform/resolvers/zod"
import { StockLocationExpandedDTO } from "@medusajs/types" import { StockLocationExpandedDTO } from "@medusajs/types"
import { Button, Drawer, Input } from "@medusajs/ui" import { Button, Input } from "@medusajs/ui"
import { useAdminUpdateStockLocation } from "medusa-react" import { useAdminUpdateStockLocation } from "medusa-react"
import { useForm } from "react-hook-form" import { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
import * as zod from "zod" import * as zod from "zod"
import { CountrySelect } from "../../../../../components/common/country-select" import { CountrySelect } from "../../../../../components/common/country-select"
import { Form } from "../../../../../components/common/form" import { Form } from "../../../../../components/common/form"
import { RouteDrawer } from "../../../../../components/route-modal"
type EditLocationFormProps = { type EditLocationFormProps = {
location: StockLocationExpandedDTO location: StockLocationExpandedDTO
@@ -56,12 +58,12 @@ export const EditLocationForm = ({ location }: EditLocationFormProps) => {
}) })
return ( return (
<Form {...form}> <RouteDrawer.Form form={form}>
<form <form
onSubmit={handleSubmit} onSubmit={handleSubmit}
className="flex flex-1 flex-col overflow-hidden" className="flex flex-1 flex-col overflow-hidden"
> >
<Drawer.Body className="flex flex-col gap-y-8 overflow-y-auto"> <RouteDrawer.Body className="flex flex-col gap-y-8 overflow-y-auto">
<div> <div>
<Form.Field <Form.Field
control={form.control} control={form.control}
@@ -201,18 +203,20 @@ export const EditLocationForm = ({ location }: EditLocationFormProps) => {
}} }}
/> />
</div> </div>
</Drawer.Body> </RouteDrawer.Body>
<Drawer.Footer> <RouteDrawer.Footer>
<div className="flex items-center justify-end gap-x-2"> <div className="flex items-center justify-end gap-x-2">
<Drawer.Close asChild> <RouteDrawer.Close asChild>
<Button size="small" variant="secondary"> <Button size="small" variant="secondary">
{t("actions.cancel")} {t("actions.cancel")}
</Button> </Button>
</Drawer.Close> </RouteDrawer.Close>
<Button size="small">{t("actions.save")}</Button> <Button size="small" type="submit" isLoading={isLoading}>
{t("actions.save")}
</Button>
</div> </div>
</Drawer.Footer> </RouteDrawer.Footer>
</form> </form>
</Form> </RouteDrawer.Form>
) )
} }
@@ -1,12 +1,11 @@
import { Drawer, Heading } from "@medusajs/ui" import { Heading } from "@medusajs/ui"
import { useAdminStockLocations } from "medusa-react" import { useAdminStockLocations } from "medusa-react"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
import { json, useParams } from "react-router-dom" import { useParams } from "react-router-dom"
import { useRouteModalState } from "../../../hooks/use-route-modal-state" import { RouteDrawer } from "../../../components/route-modal"
import { EditLocationForm } from "./components/edit-location-form/edit-location-form" import { EditLocationForm } from "./components/edit-location-form/edit-location-form"
export const LocationEdit = () => { export const LocationEdit = () => {
const [open, onOpenChange] = useRouteModalState()
const { id } = useParams() const { id } = useParams()
const { stock_locations, isLoading, isError, error } = useAdminStockLocations( const { stock_locations, isLoading, isError, error } = useAdminStockLocations(
@@ -24,24 +23,14 @@ export const LocationEdit = () => {
const stock_location = stock_locations?.[0] const stock_location = stock_locations?.[0]
if (!isLoading && !stock_location) {
throw json({ message: "Not found" }, 404)
}
return ( return (
<Drawer open={open} onOpenChange={onOpenChange}> <RouteDrawer>
<Drawer.Content> <RouteDrawer.Header>
<Drawer.Header> <Heading className="capitalize">{t("locations.editLocation")}</Heading>
<Heading className="capitalize"> </RouteDrawer.Header>
{t("locations.editLocation")} {!isLoading && stock_location && (
</Heading>
</Drawer.Header>
{isLoading || !stock_location ? (
<div>Loading...</div>
) : (
<EditLocationForm location={stock_location} /> <EditLocationForm location={stock_location} />
)} )}
</Drawer.Content> </RouteDrawer>
</Drawer>
) )
} }
@@ -1,19 +1,20 @@
import { zodResolver } from "@hookform/resolvers/zod" import { zodResolver } from "@hookform/resolvers/zod"
import { Product } from "@medusajs/medusa" import { Product } from "@medusajs/medusa"
import { Button, Drawer, Input } from "@medusajs/ui" import { Button, Input } from "@medusajs/ui"
import { useAdminUpdateProduct } from "medusa-react" import { useAdminUpdateProduct } from "medusa-react"
import { useEffect } from "react"
import { useForm } from "react-hook-form" import { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
import * as zod from "zod" import * as zod from "zod"
import { CountrySelect } from "../../../../../components/common/country-select" import { CountrySelect } from "../../../../../components/common/country-select"
import { Form } from "../../../../../components/common/form" import { Form } from "../../../../../components/common/form"
import {
RouteDrawer,
useRouteModal,
} from "../../../../../components/route-modal"
type ProductAttributesFormProps = { type ProductAttributesFormProps = {
product: Product product: Product
subscribe: (state: boolean) => void
onSuccessfulSubmit: () => void
} }
const dimension = zod const dimension = zod
@@ -39,9 +40,10 @@ const ProductAttributesSchema = zod.object({
export const ProductAttributesForm = ({ export const ProductAttributesForm = ({
product, product,
subscribe,
onSuccessfulSubmit,
}: ProductAttributesFormProps) => { }: ProductAttributesFormProps) => {
const { t } = useTranslation()
const { handleSuccess } = useRouteModal()
const form = useForm<zod.infer<typeof ProductAttributesSchema>>({ const form = useForm<zod.infer<typeof ProductAttributesSchema>>({
defaultValues: { defaultValues: {
height: product.height ? product.height : null, height: product.height ? product.height : null,
@@ -55,15 +57,6 @@ export const ProductAttributesForm = ({
resolver: zodResolver(ProductAttributesSchema), resolver: zodResolver(ProductAttributesSchema),
}) })
const {
formState: { isDirty },
} = form
useEffect(() => {
subscribe(isDirty)
}, [isDirty, subscribe])
const { t } = useTranslation()
const { mutateAsync, isLoading } = useAdminUpdateProduct(product.id) const { mutateAsync, isLoading } = useAdminUpdateProduct(product.id)
const handleSubmit = form.handleSubmit(async (data) => { const handleSubmit = form.handleSubmit(async (data) => {
@@ -79,16 +72,16 @@ export const ProductAttributesForm = ({
}, },
{ {
onSuccess: () => { onSuccess: () => {
onSuccessfulSubmit() handleSuccess()
}, },
} }
) )
}) })
return ( return (
<Form {...form}> <RouteDrawer.Form form={form}>
<form onSubmit={handleSubmit} className="flex h-full flex-col"> <form onSubmit={handleSubmit} className="flex h-full flex-col">
<Drawer.Body> <RouteDrawer.Body>
<div className="flex h-full flex-col gap-y-8"> <div className="flex h-full flex-col gap-y-8">
<div className="flex flex-col gap-y-4"> <div className="flex flex-col gap-y-4">
<Form.Field <Form.Field
@@ -254,20 +247,20 @@ export const ProductAttributesForm = ({
/> />
</div> </div>
</div> </div>
</Drawer.Body> </RouteDrawer.Body>
<Drawer.Footer> <RouteDrawer.Footer>
<div className="flex items-center justify-end gap-x-2"> <div className="flex items-center justify-end gap-x-2">
<Drawer.Close asChild> <RouteDrawer.Close asChild>
<Button size="small" variant="secondary"> <Button size="small" variant="secondary">
{t("actions.cancel")} {t("actions.cancel")}
</Button> </Button>
</Drawer.Close> </RouteDrawer.Close>
<Button size="small" type="submit" isLoading={isLoading}> <Button size="small" type="submit" isLoading={isLoading}>
{t("actions.save")} {t("actions.save")}
</Button> </Button>
</div> </div>
</Drawer.Footer> </RouteDrawer.Footer>
</form> </form>
</Form> </RouteDrawer.Form>
) )
} }
@@ -1,41 +1,27 @@
import { Drawer, Heading } from "@medusajs/ui" import { Heading } from "@medusajs/ui"
import { useAdminProduct } from "medusa-react" import { useAdminProduct } from "medusa-react"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
import { useParams } from "react-router-dom" import { useParams } from "react-router-dom"
import { useRouteModalState } from "../../../hooks/use-route-modal-state" import { RouteDrawer } from "../../../components/route-modal"
import { ProductAttributesForm } from "./components/product-attributes-form" import { ProductAttributesForm } from "./components/product-attributes-form"
export const ProductAttributes = () => { export const ProductAttributes = () => {
const { id } = useParams() const { id } = useParams()
const [open, onOpenChange, subscribe] = useRouteModalState()
const { t } = useTranslation() const { t } = useTranslation()
const { product, isLoading, isError, error } = useAdminProduct(id!) const { product, isLoading, isError, error } = useAdminProduct(id!)
const handleSuccessfulSubmit = () => {
onOpenChange(false, true)
}
if (isError) { if (isError) {
throw error throw error
} }
return ( return (
<Drawer open={open} onOpenChange={onOpenChange}> <RouteDrawer>
<Drawer.Content> <RouteDrawer.Header>
<Drawer.Header>
<Heading>{t("products.editAttributes")}</Heading> <Heading>{t("products.editAttributes")}</Heading>
</Drawer.Header> </RouteDrawer.Header>
{!isLoading && product && ( {!isLoading && product && <ProductAttributesForm product={product} />}
<ProductAttributesForm </RouteDrawer>
product={product}
onSuccessfulSubmit={handleSuccessfulSubmit}
subscribe={subscribe}
/>
)}
</Drawer.Content>
</Drawer>
) )
} }
@@ -1,18 +1,16 @@
import { zodResolver } from "@hookform/resolvers/zod" import { zodResolver } from "@hookform/resolvers/zod"
import { Button, FocusModal } from "@medusajs/ui" import { Button } from "@medusajs/ui"
import { useAdminCreateProduct } from "medusa-react" import { useAdminCreateProduct } from "medusa-react"
import { useEffect } from "react"
import { UseFormReturn, useForm } from "react-hook-form" import { UseFormReturn, useForm } from "react-hook-form"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
import * as zod from "zod" import * as zod from "zod"
import { Form } from "../../../../../components/common/form" import {
RouteFocusModal,
useRouteModal,
} from "../../../../../components/route-modal"
import { CreateProductDetails } from "./create-product-details" import { CreateProductDetails } from "./create-product-details"
type CreateProductFormProps = {
subscribe: (state: boolean) => void
}
const CreateProductSchema = zod.object({ const CreateProductSchema = zod.object({
title: zod.string(), title: zod.string(),
subtitle: zod.string().optional(), subtitle: zod.string().optional(),
@@ -38,8 +36,10 @@ const CreateProductSchema = zod.object({
type Schema = zod.infer<typeof CreateProductSchema> type Schema = zod.infer<typeof CreateProductSchema>
export type CreateProductFormReturn = UseFormReturn<Schema> export type CreateProductFormReturn = UseFormReturn<Schema>
export const CreateProductForm = ({ subscribe }: CreateProductFormProps) => { export const CreateProductForm = () => {
const { t } = useTranslation() const { t } = useTranslation()
const { handleSuccess } = useRouteModal()
const form = useForm<Schema>({ const form = useForm<Schema>({
defaultValues: { defaultValues: {
title: "", title: "",
@@ -61,14 +61,6 @@ export const CreateProductForm = ({ subscribe }: CreateProductFormProps) => {
resolver: zodResolver(CreateProductSchema), resolver: zodResolver(CreateProductSchema),
}) })
const {
formState: { isDirty },
} = form
useEffect(() => {
subscribe(isDirty)
}, [subscribe, isDirty])
const { mutateAsync, isLoading } = useAdminCreateProduct() const { mutateAsync, isLoading } = useAdminCreateProduct()
const handleSubmit = form.handleSubmit(async (values) => { const handleSubmit = form.handleSubmit(async (values) => {
@@ -83,34 +75,36 @@ export const CreateProductForm = ({ subscribe }: CreateProductFormProps) => {
weight: values.weight ? parseFloat(values.weight) : undefined, weight: values.weight ? parseFloat(values.weight) : undefined,
}, },
{ {
onSuccess: () => {}, onSuccess: ({ product }) => {
handleSuccess(`../${product.id}`)
},
} }
) )
}) })
return ( return (
<Form {...form}> <RouteFocusModal.Form form={form}>
<form onSubmit={handleSubmit} className="flex h-full flex-col"> <form onSubmit={handleSubmit} className="flex h-full flex-col">
<FocusModal.Header> <RouteFocusModal.Header>
<div className="flex items-center justify-end gap-x-2"> <div className="flex items-center justify-end gap-x-2">
<FocusModal.Close asChild> <RouteFocusModal.Close asChild>
<Button variant="secondary" size="small"> <Button variant="secondary" size="small">
{t("actions.cancel")} {t("actions.cancel")}
</Button> </Button>
</FocusModal.Close> </RouteFocusModal.Close>
<Button type="submit" size="small" isLoading={isLoading}> <Button type="submit" size="small" isLoading={isLoading}>
{t("actions.save")} {t("actions.save")}
</Button> </Button>
</div> </div>
</FocusModal.Header> </RouteFocusModal.Header>
<FocusModal.Body className="flex flex-1 flex-col overflow-hidden"> <RouteFocusModal.Body className="flex flex-1 flex-col overflow-hidden">
<div className="flex flex-1 flex-col items-center overflow-y-auto"> <div className="flex flex-1 flex-col items-center overflow-y-auto">
<div className="flex h-full w-full"> <div className="flex h-full w-full">
<CreateProductDetails form={form} /> <CreateProductDetails form={form} />
</div> </div>
</div> </div>
</FocusModal.Body> </RouteFocusModal.Body>
</form> </form>
</Form> </RouteFocusModal.Form>
) )
} }
@@ -1,15 +1,10 @@
import { FocusModal } from "@medusajs/ui" import { RouteFocusModal } from "../../../components/route-modal"
import { useRouteModalState } from "../../../hooks/use-route-modal-state"
import { CreateProductForm } from "./components/create-product-form" import { CreateProductForm } from "./components/create-product-form"
export const ProductCreate = () => { export const ProductCreate = () => {
const [open, onOpenChange, subscribe] = useRouteModalState()
return ( return (
<FocusModal open={open} onOpenChange={onOpenChange}> <RouteFocusModal>
<FocusModal.Content> <CreateProductForm />
<CreateProductForm subscribe={subscribe} /> </RouteFocusModal>
</FocusModal.Content>
</FocusModal>
) )
} }
@@ -1,27 +1,20 @@
import { zodResolver } from "@hookform/resolvers/zod" import { zodResolver } from "@hookform/resolvers/zod"
import { Product } from "@medusajs/medusa" import { Product } from "@medusajs/medusa"
import { ProductStatus } from "@medusajs/types" import { ProductStatus } from "@medusajs/types"
import { import { Button, Input, Select, Switch, Text, Textarea } from "@medusajs/ui"
Button,
Drawer,
Input,
Select,
Switch,
Text,
Textarea,
} from "@medusajs/ui"
import { useAdminUpdateProduct } from "medusa-react" import { useAdminUpdateProduct } from "medusa-react"
import { useEffect } from "react"
import { useForm } from "react-hook-form" import { useForm } from "react-hook-form"
import { Trans, useTranslation } from "react-i18next" import { Trans, useTranslation } from "react-i18next"
import * as zod from "zod" import * as zod from "zod"
import { Form } from "../../../../../components/common/form" import { Form } from "../../../../../components/common/form"
import {
RouteDrawer,
useRouteModal,
} from "../../../../../components/route-modal"
type EditProductFormProps = { type EditProductFormProps = {
product: Product product: Product
subscribe: (state: boolean) => void
onSuccessfulSubmit: () => void
} }
const EditProductSchema = zod.object({ const EditProductSchema = zod.object({
@@ -34,11 +27,10 @@ const EditProductSchema = zod.object({
discountable: zod.boolean(), discountable: zod.boolean(),
}) })
export const EditProductForm = ({ export const EditProductForm = ({ product }: EditProductFormProps) => {
product, const { t } = useTranslation()
subscribe, const { handleSuccess } = useRouteModal()
onSuccessfulSubmit,
}: EditProductFormProps) => {
const form = useForm<zod.infer<typeof EditProductSchema>>({ const form = useForm<zod.infer<typeof EditProductSchema>>({
defaultValues: { defaultValues: {
status: product.status, status: product.status,
@@ -52,15 +44,6 @@ export const EditProductForm = ({
resolver: zodResolver(EditProductSchema), resolver: zodResolver(EditProductSchema),
}) })
const {
formState: { isDirty },
} = form
useEffect(() => {
subscribe(isDirty)
}, [isDirty, subscribe])
const { t } = useTranslation()
const { mutateAsync, isLoading } = useAdminUpdateProduct(product.id) const { mutateAsync, isLoading } = useAdminUpdateProduct(product.id)
const handleSubmit = form.handleSubmit(async (data) => { const handleSubmit = form.handleSubmit(async (data) => {
@@ -71,16 +54,16 @@ export const EditProductForm = ({
}, },
{ {
onSuccess: () => { onSuccess: () => {
onSuccessfulSubmit() handleSuccess()
}, },
} }
) )
}) })
return ( return (
<Form {...form}> <RouteDrawer.Form form={form}>
<form onSubmit={handleSubmit} className="flex h-full flex-col"> <form onSubmit={handleSubmit} className="flex h-full flex-col">
<Drawer.Body> <RouteDrawer.Body>
<div className="flex h-full flex-col gap-y-8"> <div className="flex h-full flex-col gap-y-8">
<div className="flex flex-col gap-y-4"> <div className="flex flex-col gap-y-4">
<Form.Field <Form.Field
@@ -247,8 +230,7 @@ export const EditProductForm = ({
</Form.Control> </Form.Control>
</div> </div>
<Form.Hint className="!mt-1"> <Form.Hint className="!mt-1">
When unchecked discounts will not be applied to this {t("products.discountableHint")}
product.
</Form.Hint> </Form.Hint>
<Form.ErrorMessage /> <Form.ErrorMessage />
</Form.Item> </Form.Item>
@@ -256,20 +238,20 @@ export const EditProductForm = ({
}} }}
/> />
</div> </div>
</Drawer.Body> </RouteDrawer.Body>
<Drawer.Footer> <RouteDrawer.Footer>
<div className="flex items-center justify-end gap-x-2"> <div className="flex items-center justify-end gap-x-2">
<Drawer.Close asChild> <RouteDrawer.Close asChild>
<Button size="small" variant="secondary"> <Button size="small" variant="secondary">
{t("actions.cancel")} {t("actions.cancel")}
</Button> </Button>
</Drawer.Close> </RouteDrawer.Close>
<Button size="small" type="submit" isLoading={isLoading}> <Button size="small" type="submit" isLoading={isLoading}>
{t("actions.save")} {t("actions.save")}
</Button> </Button>
</div> </div>
</Drawer.Footer> </RouteDrawer.Footer>
</form> </form>
</Form> </RouteDrawer.Form>
) )
} }
@@ -1,41 +1,27 @@
import { Drawer, Heading } from "@medusajs/ui" import { Heading } from "@medusajs/ui"
import { useAdminProduct } from "medusa-react" import { useAdminProduct } from "medusa-react"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
import { useParams } from "react-router-dom" import { useParams } from "react-router-dom"
import { useRouteModalState } from "../../../hooks/use-route-modal-state" import { RouteDrawer } from "../../../components/route-modal"
import { EditProductForm } from "./components/edit-product-form" import { EditProductForm } from "./components/edit-product-form"
export const ProductEdit = () => { export const ProductEdit = () => {
const { id } = useParams() const { id } = useParams()
const [open, onOpenChange, subscribe] = useRouteModalState()
const { t } = useTranslation() const { t } = useTranslation()
const { product, isLoading, isError, error } = useAdminProduct(id!) const { product, isLoading, isError, error } = useAdminProduct(id!)
const handleSuccessfulSubmit = () => {
onOpenChange(false, true)
}
if (isError) { if (isError) {
throw error throw error
} }
return ( return (
<Drawer open={open} onOpenChange={onOpenChange}> <RouteDrawer>
<Drawer.Content> <RouteDrawer.Header>
<Drawer.Header>
<Heading>{t("products.editProduct")}</Heading> <Heading>{t("products.editProduct")}</Heading>
</Drawer.Header> </RouteDrawer.Header>
{!isLoading && product && ( {!isLoading && product && <EditProductForm product={product} />}
<EditProductForm </RouteDrawer>
product={product}
onSuccessfulSubmit={handleSuccessfulSubmit}
subscribe={subscribe}
/>
)}
</Drawer.Content>
</Drawer>
) )
} }
@@ -11,14 +11,16 @@ import { Button, IconButton, Kbd, Tooltip } from "@medusajs/ui"
import * as Dialog from "@radix-ui/react-dialog" import * as Dialog from "@radix-ui/react-dialog"
import { Variants, motion } from "framer-motion" import { Variants, motion } from "framer-motion"
import { useAdminProduct } from "medusa-react" import { useAdminProduct } from "medusa-react"
import { useCallback, useEffect, useMemo } from "react" import { useCallback, useEffect, useMemo, useState } from "react"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
import { Link, useParams, useSearchParams } from "react-router-dom" import { Link, useParams, useSearchParams } from "react-router-dom"
import { useRouteModalState } from "../../../hooks/use-route-modal-state"
export const ProductGallery = () => { export const ProductGallery = () => {
const [open, onOpenChange] = useRouteModalState() const [open, setOpen] = useState(false)
useEffect(() => {
setOpen(true)
}, [])
const { id } = useParams() const { id } = useParams()
const [searchParams, setSearchParams] = useSearchParams() const [searchParams, setSearchParams] = useSearchParams()
@@ -78,7 +80,7 @@ export const ProductGallery = () => {
} }
return ( return (
<Dialog.Root modal open={open} onOpenChange={onOpenChange}> <Dialog.Root modal open={open} onOpenChange={setOpen}>
<Dialog.Portal> <Dialog.Portal>
<Dialog.Content className="bg-ui-bg-subtle dark fixed inset-0 grid-rows-[32px_1fr_6px] pb-16 pt-4 outline-none"> <Dialog.Content className="bg-ui-bg-subtle dark fixed inset-0 grid-rows-[32px_1fr_6px] pb-16 pt-4 outline-none">
<div className="flex items-center justify-between px-4"> <div className="flex items-center justify-between px-4">
@@ -1,15 +1,13 @@
import { zodResolver } from "@hookform/resolvers/zod" import { zodResolver } from "@hookform/resolvers/zod"
import { Product } from "@medusajs/medusa" import { Product } from "@medusajs/medusa"
import { Button, Drawer } from "@medusajs/ui" import { Button } from "@medusajs/ui"
import { useForm } from "react-hook-form" import { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
import * as zod from "zod" import * as zod from "zod"
import { Form } from "../../../../../components/common/form" import { RouteDrawer } from "../../../../../components/route-modal"
type EditProductOptionsFormProps = { type EditProductOptionsFormProps = {
product: Product product: Product
handleSuccess: () => void
subscribe: (state: boolean) => void
} }
const EditProductOptionsSchema = zod.object({}) const EditProductOptionsSchema = zod.object({})
@@ -22,22 +20,22 @@ export const EditProductOptionsForm = (props: EditProductOptionsFormProps) => {
}) })
return ( return (
<Form {...form}> <RouteDrawer.Form form={form}>
<form className="flex flex-1 flex-col overflow-hidden"> <form className="flex flex-1 flex-col overflow-hidden">
<Drawer.Body className="flex flex-1 flex-col gap-y-8 overflow-auto"></Drawer.Body> <RouteDrawer.Body className="flex flex-1 flex-col gap-y-8 overflow-auto"></RouteDrawer.Body>
<Drawer.Footer> <RouteDrawer.Footer>
<div className="flex items-center justify-end gap-x-2"> <div className="flex items-center justify-end gap-x-2">
<Drawer.Close asChild> <RouteDrawer.Close asChild>
<Button variant="secondary" size="small"> <Button variant="secondary" size="small">
{t("actions.cancel")} {t("actions.cancel")}
</Button> </Button>
</Drawer.Close> </RouteDrawer.Close>
<Button type="submit" size="small"> <Button type="submit" size="small">
{t("actions.save")} {t("actions.save")}
</Button> </Button>
</div> </div>
</Drawer.Footer> </RouteDrawer.Footer>
</form> </form>
</Form> </RouteDrawer.Form>
) )
} }
@@ -1,39 +1,26 @@
import { Drawer, Heading } from "@medusajs/ui" import { Heading } from "@medusajs/ui"
import { useAdminProduct } from "medusa-react" import { useAdminProduct } from "medusa-react"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
import { useParams } from "react-router-dom" import { useParams } from "react-router-dom"
import { useRouteModalState } from "../../../hooks/use-route-modal-state" import { RouteDrawer } from "../../../components/route-modal"
import { EditProductOptionsForm } from "./components/edit-product-options-form" import { EditProductOptionsForm } from "./components/edit-product-options-form"
export const ProductOptions = () => { export const ProductOptions = () => {
const { id } = useParams() const { id } = useParams()
const { t } = useTranslation() const { t } = useTranslation()
const [open, onOpenChange, subscribe] = useRouteModalState()
const { product, isLoading, isError, error } = useAdminProduct(id!) const { product, isLoading, isError, error } = useAdminProduct(id!)
const handleSuccess = () => {
onOpenChange(false, true)
}
if (isError) { if (isError) {
throw error throw error
} }
return ( return (
<Drawer open={open} onOpenChange={onOpenChange}> <RouteDrawer>
<Drawer.Content> <RouteDrawer.Header>
<Drawer.Header>
<Heading>{t("products.editOptions")}</Heading> <Heading>{t("products.editOptions")}</Heading>
</Drawer.Header> </RouteDrawer.Header>
{!isLoading && product && ( {!isLoading && product && <EditProductOptionsForm product={product} />}
<EditProductOptionsForm </RouteDrawer>
product={product}
handleSuccess={handleSuccess}
subscribe={subscribe}
/>
)}
</Drawer.Content>
</Drawer>
) )
} }
@@ -1,9 +1,17 @@
import { Product, SalesChannel } from "@medusajs/medusa" import { Product, SalesChannel } from "@medusajs/medusa"
import { Button, Checkbox, FocusModal } from "@medusajs/ui" import { Button, Checkbox } from "@medusajs/ui"
import { RowSelectionState, createColumnHelper } from "@tanstack/react-table" import { RowSelectionState, createColumnHelper } from "@tanstack/react-table"
import { useAdminSalesChannels, useAdminUpdateProduct } from "medusa-react" import { useAdminSalesChannels, useAdminUpdateProduct } from "medusa-react"
import { useEffect, useMemo, useState } from "react" import { useEffect, useMemo, useState } from "react"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
import * as zod from "zod"
import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form"
import {
RouteFocusModal,
useRouteModal,
} from "../../../../../components/route-modal"
import { DataTable } from "../../../../../components/table/data-table" import { DataTable } from "../../../../../components/table/data-table"
import { useSalesChannelTableColumns } from "../../../../../hooks/table/columns/use-sales-channel-table-columns" import { useSalesChannelTableColumns } from "../../../../../hooks/table/columns/use-sales-channel-table-columns"
import { useSalesChannelTableFilters } from "../../../../../hooks/table/filters/use-sales-channel-table-filters" import { useSalesChannelTableFilters } from "../../../../../hooks/table/filters/use-sales-channel-table-filters"
@@ -12,18 +20,28 @@ import { useDataTable } from "../../../../../hooks/use-data-table"
type EditSalesChannelsFormProps = { type EditSalesChannelsFormProps = {
product: Product product: Product
subscribe: (state: boolean) => void
onSuccessfulSubmit: () => void
} }
const EditSalesChannelsSchema = zod.object({
sales_channels: zod.array(zod.string()).optional(),
})
const PAGE_SIZE = 50 const PAGE_SIZE = 50
export const EditSalesChannelsForm = ({ export const EditSalesChannelsForm = ({
product, product,
subscribe,
onSuccessfulSubmit,
}: EditSalesChannelsFormProps) => { }: EditSalesChannelsFormProps) => {
const { t } = useTranslation() const { t } = useTranslation()
const { handleSuccess } = useRouteModal()
const form = useForm<zod.infer<typeof EditSalesChannelsSchema>>({
defaultValues: {
sales_channels: product.sales_channels?.map((sc) => sc.id) ?? [],
},
resolver: zodResolver(EditSalesChannelsSchema),
})
const { setValue } = form
const initialState = const initialState =
product.sales_channels?.reduce((acc, curr) => { product.sales_channels?.reduce((acc, curr) => {
@@ -34,13 +52,13 @@ export const EditSalesChannelsForm = ({
const [rowSelection, setRowSelection] = const [rowSelection, setRowSelection] =
useState<RowSelectionState>(initialState) useState<RowSelectionState>(initialState)
const isDirty = Object.entries(initialState).some(
([key, value]) => value !== rowSelection[key]
)
useEffect(() => { useEffect(() => {
subscribe(isDirty) const ids = Object.keys(rowSelection)
}, [isDirty, subscribe]) setValue("sales_channels", ids, {
shouldDirty: true,
shouldTouch: true,
})
}, [rowSelection, setValue])
const { searchParams, raw } = useSalesChannelTableQuery({ const { searchParams, raw } = useSalesChannelTableQuery({
pageSize: PAGE_SIZE, pageSize: PAGE_SIZE,
@@ -76,12 +94,10 @@ export const EditSalesChannelsForm = ({
product.id product.id
) )
const handleSubmit = async () => { const handleSubmit = form.handleSubmit(async (data) => {
const selected = Object.keys(rowSelection).filter((key) => { const arr = data.sales_channels ?? []
return rowSelection[key]
})
const sales_channels = selected.map((id) => { const sales_channels = arr.map((id) => {
return { return {
id, id,
} }
@@ -93,31 +109,32 @@ export const EditSalesChannelsForm = ({
}, },
{ {
onSuccess: () => { onSuccess: () => {
onSuccessfulSubmit() handleSuccess()
}, },
} }
) )
} })
if (isError) { if (isError) {
throw error throw error
} }
return ( return (
<RouteFocusModal.Form form={form}>
<div className="flex h-full flex-col overflow-hidden"> <div className="flex h-full flex-col overflow-hidden">
<FocusModal.Header> <RouteFocusModal.Header>
<div className="flex items-center justify-end gap-x-2"> <div className="flex items-center justify-end gap-x-2">
<FocusModal.Close asChild> <RouteFocusModal.Close asChild>
<Button size="small" variant="secondary"> <Button size="small" variant="secondary">
{t("actions.cancel")} {t("actions.cancel")}
</Button> </Button>
</FocusModal.Close> </RouteFocusModal.Close>
<Button size="small" isLoading={isMutating} onClick={handleSubmit}> <Button size="small" isLoading={isMutating} onClick={handleSubmit}>
{t("actions.save")} {t("actions.save")}
</Button> </Button>
</div> </div>
</FocusModal.Header> </RouteFocusModal.Header>
<FocusModal.Body> <RouteFocusModal.Body>
<DataTable <DataTable
table={table} table={table}
columns={columns} columns={columns}
@@ -131,8 +148,9 @@ export const EditSalesChannelsForm = ({
queryObject={raw} queryObject={raw}
layout="fill" layout="fill"
/> />
</FocusModal.Body> </RouteFocusModal.Body>
</div> </div>
</RouteFocusModal.Form>
) )
} }
@@ -1,35 +1,20 @@
import { FocusModal } from "@medusajs/ui"
import { useAdminProduct } from "medusa-react" import { useAdminProduct } from "medusa-react"
import { useParams } from "react-router-dom" import { useParams } from "react-router-dom"
import { useRouteModalState } from "../../../hooks/use-route-modal-state" import { RouteFocusModal } from "../../../components/route-modal"
import { EditSalesChannelsForm } from "./components/edit-sales-channels-form" import { EditSalesChannelsForm } from "./components/edit-sales-channels-form"
export const ProductSalesChannels = () => { export const ProductSalesChannels = () => {
const { id } = useParams() const { id } = useParams()
const [open, onOpenChange, subscribe] = useRouteModalState()
const { product, isLoading, isError, error } = useAdminProduct(id!) const { product, isLoading, isError, error } = useAdminProduct(id!)
const handleSuccessfulSubmit = () => {
onOpenChange(false, true)
}
if (isError) { if (isError) {
throw error throw error
} }
return ( return (
<FocusModal open={open} onOpenChange={onOpenChange}> <RouteFocusModal>
<FocusModal.Content> {!isLoading && product && <EditSalesChannelsForm product={product} />}
{!isLoading && product && ( </RouteFocusModal>
<EditSalesChannelsForm
product={product}
onSuccessfulSubmit={handleSuccessfulSubmit}
subscribe={subscribe}
/>
)}
</FocusModal.Content>
</FocusModal>
) )
} }
@@ -1,19 +1,22 @@
import { User } from "@medusajs/medusa"
import * as zod from "zod"
import { zodResolver } from "@hookform/resolvers/zod" import { zodResolver } from "@hookform/resolvers/zod"
import { Button, Drawer, Input, Select, Switch } from "@medusajs/ui" import { User } from "@medusajs/medusa"
import { Button, Input, Select, Switch } from "@medusajs/ui"
import { adminAuthKeys, useAdminUpdateUser } from "medusa-react" import { adminAuthKeys, useAdminUpdateUser } from "medusa-react"
import { useForm } from "react-hook-form" import { useForm } from "react-hook-form"
import { Trans, useTranslation } from "react-i18next" import { Trans, useTranslation } from "react-i18next"
import * as zod from "zod"
import { Form } from "../../../../../components/common/form" import { Form } from "../../../../../components/common/form"
import {
RouteDrawer,
useRouteModal,
} from "../../../../../components/route-modal"
import { languages } from "../../../../../i18n/config" import { languages } from "../../../../../i18n/config"
import { queryClient } from "../../../../../lib/medusa" import { queryClient } from "../../../../../lib/medusa"
type EditProfileProps = { type EditProfileProps = {
user: Omit<User, "password_hash"> user: Omit<User, "password_hash">
usageInsights: boolean usageInsights: boolean
onSuccess: () => void
} }
const EditProfileSchema = zod.object({ const EditProfileSchema = zod.object({
@@ -23,13 +26,9 @@ const EditProfileSchema = zod.object({
usage_insights: zod.boolean(), usage_insights: zod.boolean(),
}) })
export const EditProfileForm = ({ export const EditProfileForm = ({ user, usageInsights }: EditProfileProps) => {
user,
usageInsights,
onSuccess,
}: EditProfileProps) => {
const { t, i18n } = useTranslation() const { t, i18n } = useTranslation()
const { mutateAsync, isLoading } = useAdminUpdateUser(user.id) const { handleSuccess } = useRouteModal()
const form = useForm<zod.infer<typeof EditProfileSchema>>({ const form = useForm<zod.infer<typeof EditProfileSchema>>({
defaultValues: { defaultValues: {
@@ -49,6 +48,8 @@ export const EditProfileForm = ({
a.display_name.localeCompare(b.display_name) a.display_name.localeCompare(b.display_name)
) )
const { mutateAsync, isLoading } = useAdminUpdateUser(user.id)
const handleSubmit = form.handleSubmit(async (values) => { const handleSubmit = form.handleSubmit(async (values) => {
await mutateAsync( await mutateAsync(
{ {
@@ -56,12 +57,7 @@ export const EditProfileForm = ({
last_name: values.last_name, last_name: values.last_name,
}, },
{ {
onSuccess: ({ user }) => { onSuccess: () => {
form.reset({
first_name: user.first_name,
last_name: user.last_name,
})
// Invalidate the current user session. // Invalidate the current user session.
queryClient.invalidateQueries(adminAuthKeys.details()) queryClient.invalidateQueries(adminAuthKeys.details())
}, },
@@ -73,13 +69,13 @@ export const EditProfileForm = ({
changeLanguage(values.language) changeLanguage(values.language)
onSuccess() handleSuccess()
}) })
return ( return (
<Form {...form}> <RouteDrawer.Form form={form}>
<form onSubmit={handleSubmit} className="flex flex-1 flex-col"> <form onSubmit={handleSubmit} className="flex flex-1 flex-col">
<Drawer.Body> <RouteDrawer.Body>
<div className="flex flex-col gap-y-8"> <div className="flex flex-col gap-y-8">
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<Form.Field <Form.Field
@@ -122,9 +118,6 @@ export const EditProfileForm = ({
<Form.Control> <Form.Control>
<Select <Select
{...field} {...field}
// open={selectOpen}
// onOpenChange={setSelectOpen}
value={field.value}
onValueChange={field.onChange} onValueChange={field.onChange}
size="small" size="small"
> >
@@ -175,6 +168,7 @@ export const EditProfileForm = ({
i18nKey="profile.userInsightsHint" i18nKey="profile.userInsightsHint"
components={[ components={[
<a <a
key="hint-link"
className="text-ui-fg-interactive hover:text-ui-fg-interactive-hover transition-fg underline" className="text-ui-fg-interactive hover:text-ui-fg-interactive-hover transition-fg underline"
href="https://docs.medusajs.com/usage#admin-analytics" href="https://docs.medusajs.com/usage#admin-analytics"
target="_blank" target="_blank"
@@ -189,20 +183,20 @@ export const EditProfileForm = ({
)} )}
/> />
</div> </div>
</Drawer.Body> </RouteDrawer.Body>
<Drawer.Footer> <RouteDrawer.Footer>
<div className="flex items-center gap-x-2"> <div className="flex items-center gap-x-2">
<Drawer.Close asChild> <RouteDrawer.Close asChild>
<Button size="small" variant="secondary"> <Button size="small" variant="secondary">
{t("actions.cancel")} {t("actions.cancel")}
</Button> </Button>
</Drawer.Close> </RouteDrawer.Close>
<Button size="small" type="submit" isLoading={isLoading}> <Button size="small" type="submit" isLoading={isLoading}>
{t("actions.save")} {t("actions.save")}
</Button> </Button>
</div> </div>
</Drawer.Footer> </RouteDrawer.Footer>
</form> </form>
</Form> </RouteDrawer.Form>
) )
} }
@@ -1,52 +1,26 @@
import { Drawer, Heading } from "@medusajs/ui" import { Heading } from "@medusajs/ui"
import { useAdminGetSession } from "medusa-react" import { useAdminGetSession } from "medusa-react"
import { useEffect, useState } from "react"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
import { useNavigate } from "react-router-dom" import { RouteDrawer } from "../../../components/route-modal"
import { EditProfileForm } from "./components/edit-profile-form/edit-profile-form" import { EditProfileForm } from "./components/edit-profile-form/edit-profile-form"
export const ProfileEdit = () => { export const ProfileEdit = () => {
const [open, setOpen] = useState(false)
const navigate = useNavigate()
const { user, isLoading, isError, error } = useAdminGetSession() const { user, isLoading, isError, error } = useAdminGetSession()
const { t } = useTranslation() const { t } = useTranslation()
useEffect(() => {
setOpen(true)
}, [])
const onOpenChange = (open: boolean) => {
if (!open) {
setTimeout(() => {
navigate(`/settings/profile`, { replace: true })
}, 200)
}
setOpen(open)
}
if (isError) { if (isError) {
throw error throw error
} }
return ( return (
<Drawer open={open} onOpenChange={onOpenChange}> <RouteDrawer>
<Drawer.Content> <RouteDrawer.Header className="capitalize">
<Drawer.Header className="capitalize">
<Heading>{t("profile.editProfile")}</Heading> <Heading>{t("profile.editProfile")}</Heading>
</Drawer.Header> </RouteDrawer.Header>
{isLoading || !user ? ( {!isLoading && user && (
<div>Loading...</div> <EditProfileForm user={user} usageInsights={false} />
) : (
<EditProfileForm
user={user}
usageInsights={false}
onSuccess={() => onOpenChange(false)}
/>
)} )}
</Drawer.Content> </RouteDrawer>
</Drawer>
) )
} }
@@ -1,16 +1,12 @@
import { zodResolver } from "@hookform/resolvers/zod" import { zodResolver } from "@hookform/resolvers/zod"
import { Button, FocusModal, Heading, Input, Switch, Text } from "@medusajs/ui" import { Button, Heading, Input, Select, Switch, Text } from "@medusajs/ui"
import { useAdminCreateRegion } from "medusa-react" import { useAdminCreateRegion, useAdminStore } from "medusa-react"
import { useEffect } from "react"
import { useForm } from "react-hook-form" import { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
import { useNavigate } from "react-router-dom" import { useNavigate } from "react-router-dom"
import * as zod from "zod" import * as zod from "zod"
import { Form } from "../../../../../components/common/form" import { Form } from "../../../../../components/common/form"
import { RouteFocusModal } from "../../../../../components/route-modal/route-focus-modal"
type CreateRegionFormProps = {
subscribe: (state: boolean) => void
}
const CreateRegionSchema = zod.object({ const CreateRegionSchema = zod.object({
name: zod.string().min(1), name: zod.string().min(1),
@@ -23,7 +19,7 @@ const CreateRegionSchema = zod.object({
tax_code: zod.string().optional(), tax_code: zod.string().optional(),
}) })
export const CreateRegionForm = ({ subscribe }: CreateRegionFormProps) => { export const CreateRegionForm = () => {
const form = useForm<zod.infer<typeof CreateRegionSchema>>({ const form = useForm<zod.infer<typeof CreateRegionSchema>>({
defaultValues: { defaultValues: {
name: "", name: "",
@@ -33,23 +29,19 @@ export const CreateRegionForm = ({ subscribe }: CreateRegionFormProps) => {
fulfillment_providers: [], fulfillment_providers: [],
payment_providers: [], payment_providers: [],
tax_code: "", tax_code: "",
tax_rate: 0,
}, },
resolver: zodResolver(CreateRegionSchema), resolver: zodResolver(CreateRegionSchema),
}) })
const {
formState: { isDirty },
} = form
useEffect(() => {
subscribe(isDirty)
}, [isDirty, subscribe])
const { t } = useTranslation() const { t } = useTranslation()
const navigate = useNavigate() const navigate = useNavigate()
const { mutateAsync, isLoading } = useAdminCreateRegion() const { mutateAsync, isLoading } = useAdminCreateRegion()
const { store } = useAdminStore()
const storeCurrencies = store?.currencies ?? []
const handleSubmit = form.handleSubmit(async (values) => { const handleSubmit = form.handleSubmit(async (values) => {
await mutateAsync( await mutateAsync(
{ {
@@ -71,24 +63,24 @@ export const CreateRegionForm = ({ subscribe }: CreateRegionFormProps) => {
}) })
return ( return (
<Form {...form}> <RouteFocusModal.Form form={form}>
<form <form
className="flex h-full flex-col overflow-hidden" className="flex h-full flex-col overflow-hidden"
onSubmit={handleSubmit} onSubmit={handleSubmit}
> >
<FocusModal.Header> <RouteFocusModal.Header>
<div className="flex items-center justify-end gap-x-2"> <div className="flex items-center justify-end gap-x-2">
<FocusModal.Close asChild> <RouteFocusModal.Close asChild>
<Button size="small" variant="secondary"> <Button size="small" variant="secondary">
{t("actions.cancel")} {t("actions.cancel")}
</Button> </Button>
</FocusModal.Close> </RouteFocusModal.Close>
<Button size="small" type="submit" isLoading={isLoading}> <Button size="small" type="submit" isLoading={isLoading}>
{t("actions.save")} {t("actions.save")}
</Button> </Button>
</div> </div>
</FocusModal.Header> </RouteFocusModal.Header>
<FocusModal.Body className="flex h-full w-full flex-col items-center overflow-y-auto py-16"> <RouteFocusModal.Body className="flex h-full w-full flex-col items-center overflow-y-auto py-16">
<div className="flex w-full max-w-[720px] flex-col gap-y-8"> <div className="flex w-full max-w-[720px] flex-col gap-y-8">
<div> <div>
<Heading>{t("regions.createRegion")}</Heading> <Heading>{t("regions.createRegion")}</Heading>
@@ -116,11 +108,27 @@ export const CreateRegionForm = ({ subscribe }: CreateRegionFormProps) => {
<Form.Field <Form.Field
control={form.control} control={form.control}
name="currency_code" name="currency_code"
render={({ field }) => { render={({ field: { onChange, ref, ...field } }) => {
return ( return (
<Form.Item> <Form.Item>
<Form.Label>{t("fields.currency")}</Form.Label> <Form.Label>{t("fields.currency")}</Form.Label>
<Form.Control></Form.Control> <Form.Control>
<Select {...field} onValueChange={onChange}>
<Select.Trigger ref={ref}>
<Select.Value />
</Select.Trigger>
<Select.Content>
{storeCurrencies.map((currency) => (
<Select.Item
value={currency.code}
key={currency.code}
>
{currency.name}
</Select.Item>
))}
</Select.Content>
</Select>
</Form.Control>
<Form.ErrorMessage /> <Form.ErrorMessage />
</Form.Item> </Form.Item>
) )
@@ -198,8 +206,8 @@ export const CreateRegionForm = ({ subscribe }: CreateRegionFormProps) => {
<div className="grid grid-cols-2 gap-4"></div> <div className="grid grid-cols-2 gap-4"></div>
</div> </div>
</div> </div>
</FocusModal.Body> </RouteFocusModal.Body>
</form> </form>
</Form> </RouteFocusModal.Form>
) )
} }
@@ -1,16 +1,10 @@
import { FocusModal } from "@medusajs/ui" import { RouteFocusModal } from "../../../components/route-modal/route-focus-modal"
import { useRouteModalState } from "../../../hooks/use-route-modal-state"
import { CreateRegionForm } from "./components/create-region-form" import { CreateRegionForm } from "./components/create-region-form"
export const RegionCreate = () => { export const RegionCreate = () => {
const [open, onOpenChange, subscribe] = useRouteModalState()
return ( return (
<FocusModal open={open} onOpenChange={onOpenChange}> <RouteFocusModal>
<FocusModal.Content> <CreateRegionForm />
<CreateRegionForm subscribe={subscribe} /> </RouteFocusModal>
</FocusModal.Content>
</FocusModal>
) )
} }
@@ -0,0 +1 @@
export * from "./region-shipping-option-section"
@@ -1,60 +1,48 @@
import { Region, ShippingOption } from "@medusajs/medusa" import { Region } from "@medusajs/medusa"
import { Container, Heading, StatusBadge, Table, clx } from "@medusajs/ui" import { Container, Heading } from "@medusajs/ui"
import {
PaginationState,
RowSelectionState,
createColumnHelper,
flexRender,
getCoreRowModel,
useReactTable,
} from "@tanstack/react-table"
import { useAdminShippingOptions } from "medusa-react" import { useAdminShippingOptions } from "medusa-react"
import { useMemo, useState } from "react"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
import { LocalizedTablePagination } from "../../../../../components/localization/localized-table-pagination"
import { PricedShippingOption } from "@medusajs/medusa/dist/types/pricing"
import { DataTable } from "../../../../../components/table/data-table"
import { useDataTable } from "../../../../../hooks/use-data-table"
import { useShippingOptionColumns } from "./use-shipping-option-table-columns"
import { useShippingOptionTableFilters } from "./use-shipping-option-table-filters"
import { useShippingOptionTableQuery } from "./use-shipping-option-table-query"
type RegionShippingOptionSectionProps = { type RegionShippingOptionSectionProps = {
region: Region region: Region
} }
const PAGE_SIZE = 10
export const RegionShippingOptionSection = ({ export const RegionShippingOptionSection = ({
region, region,
}: RegionShippingOptionSectionProps) => { }: RegionShippingOptionSectionProps) => {
const { searchParams, raw } = useShippingOptionTableQuery({
pageSize: PAGE_SIZE,
regionId: region.id,
})
const { shipping_options, count, isError, error, isLoading } = const { shipping_options, count, isError, error, isLoading } =
useAdminShippingOptions({ useAdminShippingOptions(
region_id: region.id, {
is_return: false, ...searchParams,
}) },
{
const [{ pageIndex, pageSize }, setPagination] = useState<PaginationState>({ keepPreviousData: true,
pageIndex: 0, }
pageSize: count || 0,
})
const pagination = useMemo(
() => ({
pageIndex,
pageSize,
}),
[pageIndex, pageSize]
) )
const [rowSelection, setRowSelection] = useState<RowSelectionState>({}) const filters = useShippingOptionTableFilters()
const columns = useShippingOptionColumns() const columns = useShippingOptionColumns()
const table = useReactTable({ const { table } = useDataTable({
data: shipping_options ?? [], data: (shipping_options ?? []) as unknown as PricedShippingOption[],
columns, columns,
pageCount: count ? 1 : 0, count,
state: { enablePagination: true,
pagination, getRowId: (row) => row.id!,
rowSelection, pageSize: PAGE_SIZE,
},
manualPagination: true,
getCoreRowModel: getCoreRowModel(),
onPaginationChange: setPagination,
onRowSelectionChange: setRowSelection,
}) })
const { t } = useTranslation() const { t } = useTranslation()
@@ -64,91 +52,30 @@ export const RegionShippingOptionSection = ({
} }
return ( return (
<Container className="p-0 divide-y"> <Container className="divide-y p-0">
<div className="px-6 py-4"> <div className="px-6 py-4">
<Heading level="h2">{t("regions.shippingOptions")}</Heading> <Heading level="h2">{t("regions.shippingOptions")}</Heading>
</div> </div>
<Table> <DataTable
<Table.Header> table={table}
{table.getHeaderGroups().map((headerGroup) => { columns={columns}
return ( count={count}
<Table.Row filters={filters}
key={headerGroup.id} orderBy={[
className="[&_th:first-of-type]:w-[1%] [&_th:first-of-type]:whitespace-nowrap [&_th:last-of-type]:w-[1%] [&_th:last-of-type]:whitespace-nowrap" "name",
> "price_type",
{headerGroup.headers.map((header) => { "price_incl_tax",
return ( "is_return",
<Table.HeaderCell key={header.id}> "admin_only",
{flexRender( "created_at",
header.column.columnDef.header, "updated_at",
header.getContext() ]}
)} isLoading={isLoading}
</Table.HeaderCell> rowCount={PAGE_SIZE}
) pagination
})} search
</Table.Row> queryObject={raw}
)
})}
</Table.Header>
<Table.Body className="border-b-0">
{table.getRowModel().rows.map((row) => (
<Table.Row
key={row.id}
className={clx(
"transition-fg [&_td:last-of-type]:w-[1%] [&_td:last-of-type]:whitespace-nowrap",
{
"bg-ui-bg-highlight hover:bg-ui-bg-highlight-hover":
row.getIsSelected(),
}
)}
>
{row.getVisibleCells().map((cell) => (
<Table.Cell key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</Table.Cell>
))}
</Table.Row>
))}
</Table.Body>
</Table>
<LocalizedTablePagination
canNextPage={table.getCanNextPage()}
canPreviousPage={table.getCanPreviousPage()}
nextPage={table.nextPage}
previousPage={table.previousPage}
count={count ?? 0}
pageIndex={pageIndex}
pageCount={table.getPageCount()}
pageSize={count ?? 0}
/> />
</Container> </Container>
) )
} }
const columnHelper = createColumnHelper<ShippingOption>()
const useShippingOptionColumns = () => {
const { t } = useTranslation()
return useMemo(
() => [
columnHelper.accessor("name", {
header: t("fields.name"),
cell: (cell) => cell.getValue(),
}),
columnHelper.accessor("admin_only", {
header: t("fields.availability"),
cell: (cell) => {
const value = cell.getValue()
return (
<StatusBadge color={value ? "blue" : "green"}>
{value ? t("general.admin") : t("general.store")}
</StatusBadge>
)
},
}),
],
[t]
)
}
@@ -0,0 +1,116 @@
import { PricedShippingOption } from "@medusajs/medusa/dist/types/pricing"
import { createColumnHelper } from "@tanstack/react-table"
import { useMemo } from "react"
import { useTranslation } from "react-i18next"
import { MoneyAmountCell } from "../../../../../components/table/table-cells/common/money-amount-cell"
import { PlaceholderCell } from "../../../../../components/table/table-cells/common/placeholder-cell"
import { StatusCell } from "../../../../../components/table/table-cells/common/status-cell"
const columnHelper = createColumnHelper<PricedShippingOption>()
export const useShippingOptionColumns = () => {
const { t } = useTranslation()
return useMemo(
() => [
columnHelper.accessor("name", {
header: t("fields.name"),
cell: ({ getValue }) => (
<div className="flex size-full items-center overflow-hidden">
<span className="truncate">{getValue()}</span>
</div>
),
}),
columnHelper.accessor("price_type", {
header: t("regions.priceType"),
cell: ({ getValue }) => {
const type = getValue()
return (
<StatusCell color={type === "flat_rate" ? "green" : "blue"}>
{type === "flat_rate"
? t("regions.flatRate")
: t("regions.calculated")}
</StatusCell>
)
},
}),
columnHelper.accessor("price_incl_tax", {
header: t("fields.price"),
cell: ({ getValue, row }) => {
const isCalculated = row.original.price_type === "calculated"
if (isCalculated) {
return <PlaceholderCell />
}
const amount = getValue()
const currencyCode = row.original.region!.currency_code
return <MoneyAmountCell currencyCode={currencyCode} amount={amount} />
},
}),
columnHelper.display({
id: "min_amount",
header: "Min.",
cell: ({ row }) => {
const minAmountReq = row.original.requirements?.find(
(r) => r.type === "min_subtotal"
)
if (!minAmountReq) {
return <PlaceholderCell />
}
const amount = minAmountReq.amount
const currencyCode = row.original.region!.currency_code
return <MoneyAmountCell currencyCode={currencyCode} amount={amount} />
},
}),
columnHelper.display({
id: "max_amount",
header: "Max.",
cell: ({ row }) => {
const maxAmountReq = row.original.requirements?.find(
(r) => r.type === "max_subtotal"
)
if (!maxAmountReq) {
return <PlaceholderCell />
}
const amount = maxAmountReq.amount
const currencyCode = row.original.region!.currency_code
return <MoneyAmountCell currencyCode={currencyCode} amount={amount} />
},
}),
columnHelper.accessor("admin_only", {
header: t("fields.availability"),
cell: (cell) => {
const value = cell.getValue()
return (
<StatusCell color={value ? "blue" : "green"}>
{value ? t("general.admin") : t("general.store")}
</StatusCell>
)
},
}),
columnHelper.accessor("is_return", {
header: t("fields.type"),
cell: (cell) => {
const value = cell.getValue()
return (
<StatusCell color={value ? "blue" : "green"}>
{value ? t("regions.return") : t("regions.outbound")}
</StatusCell>
)
},
}),
],
[t]
)
}
@@ -0,0 +1,39 @@
import { useTranslation } from "react-i18next"
import { Filter } from "../../../../../components/table/data-table"
export const useShippingOptionTableFilters = () => {
const { t } = useTranslation()
const isReturnFilter: Filter = {
key: "is_return",
label: t("fields.type"),
type: "select",
options: [
{ label: t("regions.return"), value: "true" },
{ label: t("regions.outbound"), value: "false" },
],
}
const isAdminFilter: Filter = {
key: "admin_only",
label: t("fields.availability"),
type: "select",
options: [
{ label: t("general.admin"), value: "true" },
{ label: t("general.store"), value: "false" },
],
}
const dateFilters: Filter[] = [
{ label: t("fields.createdAt"), key: "created_at" },
{ label: t("fields.updatedAt"), key: "updated_at" },
].map((f) => ({
key: f.key,
label: f.label,
type: "date",
}))
const filters = [isReturnFilter, isAdminFilter, ...dateFilters]
return filters
}
@@ -0,0 +1,48 @@
import { AdminGetShippingOptionsParams } from "@medusajs/medusa"
import { useQueryParams } from "../../../../../hooks/use-query-params"
type UseShippingOptionTableQueryProps = {
regionId: string
isReturn?: boolean
pageSize?: number
prefix?: string
}
export const useShippingOptionTableQuery = ({
regionId,
pageSize = 10,
prefix,
}: UseShippingOptionTableQueryProps) => {
const queryObject = useQueryParams(
[
"offset",
"q",
"order",
"admin_only",
"is_return",
"created_at",
"updated_at",
],
prefix
)
const { offset, order, q, admin_only, is_return, created_at, updated_at } =
queryObject
const searchParams: AdminGetShippingOptionsParams = {
limit: pageSize,
offset: offset ? Number(offset) : 0,
region_id: regionId,
is_return: is_return ? is_return === "true" : undefined,
admin_only: admin_only ? admin_only === "true" : undefined,
q,
order,
created_at: created_at ? JSON.parse(created_at) : undefined,
updated_at: updated_at ? JSON.parse(updated_at) : undefined,
}
return {
searchParams,
raw: queryObject,
}
}
@@ -1,64 +1,43 @@
import { PencilSquare, Trash } from "@medusajs/icons" import { PencilSquare, Trash } from "@medusajs/icons"
import { Region } from "@medusajs/medusa" import { Region } from "@medusajs/medusa"
import { import { Button, Container, Heading, usePrompt } from "@medusajs/ui"
Button, import { createColumnHelper } from "@tanstack/react-table"
Container,
Heading,
Table,
Tooltip,
clx,
usePrompt,
} from "@medusajs/ui"
import {
PaginationState,
createColumnHelper,
flexRender,
getCoreRowModel,
useReactTable,
} from "@tanstack/react-table"
import { useAdminDeleteRegion, useAdminRegions } from "medusa-react" import { useAdminDeleteRegion, useAdminRegions } from "medusa-react"
import { useMemo, useState } from "react" import { useMemo } from "react"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
import { Link, useNavigate } from "react-router-dom" import { Link } from "react-router-dom"
import { ActionMenu } from "../../../../../components/common/action-menu" import { ActionMenu } from "../../../../../components/common/action-menu"
import { LocalizedTablePagination } from "../../../../../components/localization/localized-table-pagination" import { DataTable } from "../../../../../components/table/data-table"
import { useRegionTableColumns } from "../../../../../hooks/table/columns/use-region-table-columns"
import { useRegionTableFilters } from "../../../../../hooks/table/filters/use-region-table-filters"
import { useRegionTableQuery } from "../../../../../hooks/table/query/use-region-table-query"
import { useDataTable } from "../../../../../hooks/use-data-table"
const PAGE_SIZE = 50 const PAGE_SIZE = 20
export const RegionListTable = () => { export const RegionListTable = () => {
const navigate = useNavigate()
const { t } = useTranslation() const { t } = useTranslation()
const [{ pageIndex, pageSize }, setPagination] = useState<PaginationState>({ const { searchParams, raw } = useRegionTableQuery({ pageSize: PAGE_SIZE })
pageIndex: 0, const { regions, count, isLoading, isError, error } = useAdminRegions(
pageSize: PAGE_SIZE, {
}) ...searchParams,
},
const pagination = useMemo( {
() => ({ keepPreviousData: true,
pageIndex, }
pageSize,
}),
[pageIndex, pageSize]
) )
const { regions, count, isLoading, isError, error } = useAdminRegions({ const filters = useRegionTableFilters()
limit: PAGE_SIZE,
offset: pageIndex * PAGE_SIZE,
})
const columns = useColumns() const columns = useColumns()
const table = useReactTable({ const { table } = useDataTable({
data: regions ?? [], data: regions ?? [],
columns, columns,
pageCount: Math.ceil((count ?? 0) / PAGE_SIZE), count,
state: { enablePagination: true,
pagination, getRowId: (row) => row.id,
}, pageSize: PAGE_SIZE,
onPaginationChange: setPagination,
getCoreRowModel: getCoreRowModel(),
manualPagination: true,
}) })
if (isError) { if (isError) {
@@ -66,7 +45,7 @@ export const RegionListTable = () => {
} }
return ( return (
<Container className="p-0"> <Container className="divide-y p-0">
<div className="flex items-center justify-between px-6 py-4"> <div className="flex items-center justify-between px-6 py-4">
<Heading level="h2">{t("regions.domain")}</Heading> <Heading level="h2">{t("regions.domain")}</Heading>
<Link to="/settings/regions/create"> <Link to="/settings/regions/create">
@@ -75,59 +54,18 @@ export const RegionListTable = () => {
</Button> </Button>
</Link> </Link>
</div> </div>
<Table> <DataTable
<Table.Header> table={table}
{table.getHeaderGroups().map((headerGroup) => { columns={columns}
return ( count={count}
<Table.Row rowCount={PAGE_SIZE}
key={headerGroup.id} isLoading={isLoading}
className=" [&_th:last-of-type]:w-[1%] [&_th:last-of-type]:whitespace-nowrap" filters={filters}
> orderBy={["name", "created_at", "updated_at"]}
{headerGroup.headers.map((header) => { navigateTo={(row) => `${row.original.id}`}
return ( pagination
<Table.HeaderCell key={header.id}> search
{flexRender( queryObject={raw}
header.column.columnDef.header,
header.getContext()
)}
</Table.HeaderCell>
)
})}
</Table.Row>
)
})}
</Table.Header>
<Table.Body className="border-b-0">
{table.getRowModel().rows.map((row) => (
<Table.Row
key={row.id}
className={clx(
"transition-fg cursor-pointer [&_td:last-of-type]:w-[1%] [&_td:last-of-type]:whitespace-nowrap",
{
"bg-ui-bg-highlight hover:bg-ui-bg-highlight-hover":
row.getIsSelected(),
}
)}
onClick={() => navigate(`/settings/regions/${row.original.id}`)}
>
{row.getVisibleCells().map((cell) => (
<Table.Cell key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</Table.Cell>
))}
</Table.Row>
))}
</Table.Body>
</Table>
<LocalizedTablePagination
canNextPage={table.getCanNextPage()}
canPreviousPage={table.getCanPreviousPage()}
nextPage={table.nextPage}
previousPage={table.previousPage}
count={count ?? 0}
pageIndex={pageIndex}
pageCount={table.getPageCount()}
pageSize={PAGE_SIZE}
/> />
</Container> </Container>
) )
@@ -187,124 +125,11 @@ const RegionActions = ({ region }: { region: Region }) => {
const columnHelper = createColumnHelper<Region>() const columnHelper = createColumnHelper<Region>()
const useColumns = () => { const useColumns = () => {
const { t } = useTranslation() const base = useRegionTableColumns()
return useMemo( return useMemo(
() => [ () => [
columnHelper.accessor("name", { ...base,
header: t("fields.name"),
cell: (cell) => cell.getValue(),
}),
columnHelper.accessor("countries", {
header: t("fields.countries"),
cell: (cell) => {
const countries = cell.getValue()
const displayValue = countries
.slice(0, 2)
.map((c) => c.display_name)
.join(", ")
const additionalCountries = countries
.slice(2)
.map((c) => c.display_name)
return (
<div className="flex items-center gap-x-1">
<span>{displayValue}</span>
{additionalCountries.length > 0 && (
<Tooltip
content={
<ul>
{additionalCountries.map((c) => (
<li key={c}>{c}</li>
))}
</ul>
}
>
<span>
{t("general.plusCountMore", {
count: additionalCountries.length,
})}
</span>
</Tooltip>
)}
</div>
)
},
}),
columnHelper.accessor("payment_providers", {
header: t("fields.paymentProviders"),
cell: (cell) => {
const providers = cell.getValue()
const displayValue = providers
.slice(0, 2)
.map((p) => p.id)
.join(", ")
const additionalProviders = providers.slice(2).map((c) => c.id)
return (
<div className="flex items-center gap-x-1">
<span>{displayValue}</span>
{additionalProviders.length > 0 && (
<Tooltip
content={
<ul>
{additionalProviders.map((c) => (
<li key={c}>{c}</li>
))}
</ul>
}
>
<span>
{t("general.plusCountMore", {
count: additionalProviders.length,
})}
</span>
</Tooltip>
)}
</div>
)
},
}),
columnHelper.accessor("fulfillment_providers", {
header: t("fields.fulfillmentProviders"),
cell: (cell) => {
const providers = cell.getValue()
const displayValue = providers
.slice(0, 2)
.map((p) => p.id)
.join(", ")
const additionalProviders = providers.slice(2).map((c) => c.id)
return (
<div className="flex items-center gap-x-1">
<span>{displayValue}</span>
{additionalProviders.length > 0 && (
<Tooltip
content={
<ul>
{additionalProviders.map((c) => (
<li key={c}>{c}</li>
))}
</ul>
}
>
<span>
{t("general.plusCountMore", {
count: additionalProviders.length,
})}
</span>
</Tooltip>
)}
</div>
)
},
}),
columnHelper.display({ columnHelper.display({
id: "actions", id: "actions",
cell: ({ row }) => { cell: ({ row }) => {
@@ -312,6 +137,6 @@ const useColumns = () => {
}, },
}), }),
], ],
[t] [base]
) )
} }
@@ -1,14 +1,6 @@
import { zodResolver } from "@hookform/resolvers/zod" import { zodResolver } from "@hookform/resolvers/zod"
import { Product, SalesChannel } from "@medusajs/medusa" import { Product, SalesChannel } from "@medusajs/medusa"
import { import { Button, Checkbox, Hint, Table, Tooltip, clx } from "@medusajs/ui"
Button,
Checkbox,
FocusModal,
Hint,
Table,
Tooltip,
clx,
} from "@medusajs/ui"
import { import {
PaginationState, PaginationState,
RowSelectionState, RowSelectionState,
@@ -26,7 +18,6 @@ import { useEffect, useMemo, useState } from "react"
import { useForm } from "react-hook-form" import { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
import * as zod from "zod" import * as zod from "zod"
import { Form } from "../../../../components/common/form"
import { import {
ProductAvailabilityCell, ProductAvailabilityCell,
ProductCollectionCell, ProductCollectionCell,
@@ -37,13 +28,15 @@ import {
import { OrderBy } from "../../../../components/filtering/order-by" import { OrderBy } from "../../../../components/filtering/order-by"
import { Query } from "../../../../components/filtering/query" import { Query } from "../../../../components/filtering/query"
import { LocalizedTablePagination } from "../../../../components/localization/localized-table-pagination" import { LocalizedTablePagination } from "../../../../components/localization/localized-table-pagination"
import {
RouteFocusModal,
useRouteModal,
} from "../../../../components/route-modal"
import { useQueryParams } from "../../../../hooks/use-query-params" import { useQueryParams } from "../../../../hooks/use-query-params"
import { queryClient } from "../../../../lib/medusa" import { queryClient } from "../../../../lib/medusa"
type AddProductsToSalesChannelFormProps = { type AddProductsToSalesChannelFormProps = {
salesChannel: SalesChannel salesChannel: SalesChannel
subscribe: (state: boolean) => void
onSuccess: () => void
} }
const AddProductsToSalesChannelSchema = zod.object({ const AddProductsToSalesChannelSchema = zod.object({
@@ -54,10 +47,9 @@ const PAGE_SIZE = 50
export const AddProductsToSalesChannelForm = ({ export const AddProductsToSalesChannelForm = ({
salesChannel, salesChannel,
subscribe,
onSuccess,
}: AddProductsToSalesChannelFormProps) => { }: AddProductsToSalesChannelFormProps) => {
const { t } = useTranslation() const { t } = useTranslation()
const { handleSuccess } = useRouteModal()
const form = useForm<zod.infer<typeof AddProductsToSalesChannelSchema>>({ const form = useForm<zod.infer<typeof AddProductsToSalesChannelSchema>>({
defaultValues: { defaultValues: {
@@ -66,13 +58,7 @@ export const AddProductsToSalesChannelForm = ({
resolver: zodResolver(AddProductsToSalesChannelSchema), resolver: zodResolver(AddProductsToSalesChannelSchema),
}) })
const { const { setValue } = form
formState: { isDirty },
} = form
useEffect(() => {
subscribe(isDirty)
}, [isDirty])
const { mutateAsync, isLoading: isMutating } = const { mutateAsync, isLoading: isMutating } =
useAdminAddProductsToSalesChannel(salesChannel.id) useAdminAddProductsToSalesChannel(salesChannel.id)
@@ -93,15 +79,19 @@ export const AddProductsToSalesChannelForm = ({
const [rowSelection, setRowSelection] = useState<RowSelectionState>({}) const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
useEffect(() => { useEffect(() => {
form.setValue( setValue(
"product_ids", "product_ids",
Object.keys(rowSelection).filter((k) => rowSelection[k]) Object.keys(rowSelection).filter((k) => rowSelection[k]),
{
shouldDirty: true,
shouldTouch: true,
}
) )
}, [rowSelection]) }, [rowSelection, setValue])
const params = useQueryParams(["q", "order"]) const params = useQueryParams(["q", "order"])
const { products, count, isLoading } = useAdminProducts( const { products, count } = useAdminProducts(
{ {
expand: "variants,sales_channels", expand: "variants,sales_channels",
...params, ...params,
@@ -148,36 +138,36 @@ export const AddProductsToSalesChannelForm = ({
* determine if they are added to the sales channel or not. * determine if they are added to the sales channel or not.
*/ */
queryClient.invalidateQueries(adminProductKeys.lists()) queryClient.invalidateQueries(adminProductKeys.lists())
onSuccess() handleSuccess()
}, },
} }
) )
}) })
return ( return (
<Form {...form}> <RouteFocusModal.Form form={form}>
<form <form
onSubmit={handleSubmit} onSubmit={handleSubmit}
className="flex h-full flex-col overflow-hidden" className="flex h-full flex-col overflow-hidden"
> >
<FocusModal.Header> <RouteFocusModal.Header>
<div className="flex items-center justify-end gap-x-2"> <div className="flex items-center justify-end gap-x-2">
{form.formState.errors.product_ids && ( {form.formState.errors.product_ids && (
<Hint variant="error"> <Hint variant="error">
{form.formState.errors.product_ids.message} {form.formState.errors.product_ids.message}
</Hint> </Hint>
)} )}
<FocusModal.Close asChild> <RouteFocusModal.Close asChild>
<Button size="small" variant="secondary"> <Button size="small" variant="secondary">
{t("actions.cancel")} {t("actions.cancel")}
</Button> </Button>
</FocusModal.Close> </RouteFocusModal.Close>
<Button size="small" type="submit" isLoading={isMutating}> <Button size="small" type="submit" isLoading={isMutating}>
{t("actions.save")} {t("actions.save")}
</Button> </Button>
</div> </div>
</FocusModal.Header> </RouteFocusModal.Header>
<FocusModal.Body className="flex h-full w-full flex-col items-center divide-y overflow-y-auto"> <RouteFocusModal.Body className="flex h-full w-full flex-col items-center divide-y overflow-y-auto">
<div className="flex w-full items-center justify-between px-6 py-4"> <div className="flex w-full items-center justify-between px-6 py-4">
<div></div> <div></div>
<div className="flex items-center gap-x-2"> <div className="flex items-center gap-x-2">
@@ -251,9 +241,9 @@ export const AddProductsToSalesChannelForm = ({
pageSize={PAGE_SIZE} pageSize={PAGE_SIZE}
/> />
</div> </div>
</FocusModal.Body> </RouteFocusModal.Body>
</form> </form>
</Form> </RouteFocusModal.Form>
) )
} }
@@ -1,36 +1,21 @@
import { FocusModal } from "@medusajs/ui"
import { useAdminSalesChannel } from "medusa-react" import { useAdminSalesChannel } from "medusa-react"
import { useParams } from "react-router-dom" import { useParams } from "react-router-dom"
import { useRouteModalState } from "../../../hooks/use-route-modal-state" import { RouteFocusModal } from "../../../components/route-modal"
import { AddProductsToSalesChannelForm } from "./components" import { AddProductsToSalesChannelForm } from "./components"
export const SalesChannelAddProducts = () => { export const SalesChannelAddProducts = () => {
const { id } = useParams() const { id } = useParams()
const [open, onOpenChange, subscribe] = useRouteModalState()
const { sales_channel, isLoading, isError, error } = useAdminSalesChannel(id!) const { sales_channel, isLoading, isError, error } = useAdminSalesChannel(id!)
const handleSuccess = () => {
onOpenChange(false, true)
}
if (isError) { if (isError) {
throw error throw error
} }
return ( return (
<FocusModal open={open} onOpenChange={onOpenChange}> <RouteFocusModal>
<FocusModal.Content> {!isLoading && sales_channel && (
{isLoading || !sales_channel ? ( <AddProductsToSalesChannelForm salesChannel={sales_channel} />
<div>Loading...</div>
) : (
<AddProductsToSalesChannelForm
salesChannel={sales_channel}
onSuccess={handleSuccess}
subscribe={subscribe}
/>
)} )}
</FocusModal.Content> </RouteFocusModal>
</FocusModal>
) )
} }
@@ -1,25 +1,15 @@
import { zodResolver } from "@hookform/resolvers/zod" import { zodResolver } from "@hookform/resolvers/zod"
import { import { Button, Heading, Input, Switch, Text, Textarea } from "@medusajs/ui"
Button,
FocusModal,
Heading,
Input,
Switch,
Text,
Textarea,
} from "@medusajs/ui"
import { useAdminCreateSalesChannel } from "medusa-react" import { useAdminCreateSalesChannel } from "medusa-react"
import { useForm } from "react-hook-form" import { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
import { useNavigate } from "react-router-dom"
import * as zod from "zod" import * as zod from "zod"
import { useEffect } from "react"
import { Form } from "../../../../../components/common/form" import { Form } from "../../../../../components/common/form"
import {
type CreateSalesChannelFormProps = { RouteFocusModal,
subscribe: (state: boolean) => void useRouteModal,
} } from "../../../../../components/route-modal"
const CreateSalesChannelSchema = zod.object({ const CreateSalesChannelSchema = zod.object({
name: zod.string().min(1), name: zod.string().min(1),
@@ -27,9 +17,10 @@ const CreateSalesChannelSchema = zod.object({
enabled: zod.boolean(), enabled: zod.boolean(),
}) })
export const CreateSalesChannelForm = ({ export const CreateSalesChannelForm = () => {
subscribe, const { t } = useTranslation()
}: CreateSalesChannelFormProps) => { const { handleSuccess } = useRouteModal()
const form = useForm<zod.infer<typeof CreateSalesChannelSchema>>({ const form = useForm<zod.infer<typeof CreateSalesChannelSchema>>({
defaultValues: { defaultValues: {
name: "", name: "",
@@ -40,17 +31,6 @@ export const CreateSalesChannelForm = ({
}) })
const { mutateAsync, isLoading } = useAdminCreateSalesChannel() const { mutateAsync, isLoading } = useAdminCreateSalesChannel()
const {
formState: { isDirty },
} = form
useEffect(() => {
subscribe(isDirty)
}, [isDirty])
const { t } = useTranslation()
const navigate = useNavigate()
const handleSubmit = form.handleSubmit(async (values) => { const handleSubmit = form.handleSubmit(async (values) => {
await mutateAsync( await mutateAsync(
{ {
@@ -60,31 +40,31 @@ export const CreateSalesChannelForm = ({
}, },
{ {
onSuccess: ({ sales_channel }) => { onSuccess: ({ sales_channel }) => {
navigate(`../${sales_channel.id}`) handleSuccess(`../${sales_channel.id}`)
}, },
} }
) )
}) })
return ( return (
<Form {...form}> <RouteFocusModal.Form form={form}>
<form <form
onSubmit={handleSubmit} onSubmit={handleSubmit}
className="flex h-full flex-col overflow-hidden" className="flex h-full flex-col overflow-hidden"
> >
<FocusModal.Header> <RouteFocusModal.Header>
<div className="flex items-center justify-end gap-x-2"> <div className="flex items-center justify-end gap-x-2">
<FocusModal.Close asChild> <RouteFocusModal.Close asChild>
<Button size="small" variant="secondary"> <Button size="small" variant="secondary">
{t("actions.cancel")} {t("actions.cancel")}
</Button> </Button>
</FocusModal.Close> </RouteFocusModal.Close>
<Button size="small" type="submit" isLoading={isLoading}> <Button size="small" type="submit" isLoading={isLoading}>
{t("actions.save")} {t("actions.save")}
</Button> </Button>
</div> </div>
</FocusModal.Header> </RouteFocusModal.Header>
<FocusModal.Body className="flex flex-1 flex-col overflow-hidden"> <RouteFocusModal.Body className="flex flex-1 flex-col overflow-hidden">
<div className="flex flex-1 flex-col items-center overflow-y-auto"> <div className="flex flex-1 flex-col items-center overflow-y-auto">
<div className="flex w-full max-w-[720px] flex-col gap-y-8 px-2 py-16"> <div className="flex w-full max-w-[720px] flex-col gap-y-8 px-2 py-16">
<div> <div>
@@ -153,8 +133,8 @@ export const CreateSalesChannelForm = ({
/> />
</div> </div>
</div> </div>
</FocusModal.Body> </RouteFocusModal.Body>
</form> </form>
</Form> </RouteFocusModal.Form>
) )
} }
@@ -1,15 +1,10 @@
import { FocusModal } from "@medusajs/ui" import { RouteFocusModal } from "../../../components/route-modal"
import { useRouteModalState } from "../../../hooks/use-route-modal-state"
import { CreateSalesChannelForm } from "./components/create-sales-channel-form" import { CreateSalesChannelForm } from "./components/create-sales-channel-form"
export const SalesChannelCreate = () => { export const SalesChannelCreate = () => {
const [open, onOpenChange, subscribe] = useRouteModalState()
return ( return (
<FocusModal open={open} onOpenChange={onOpenChange}> <RouteFocusModal>
<FocusModal.Content> <CreateSalesChannelForm />
<CreateSalesChannelForm subscribe={subscribe} /> </RouteFocusModal>
</FocusModal.Content>
</FocusModal>
) )
} }
@@ -1,18 +1,19 @@
import { zodResolver } from "@hookform/resolvers/zod" import { zodResolver } from "@hookform/resolvers/zod"
import { SalesChannel } from "@medusajs/medusa" import { SalesChannel } from "@medusajs/medusa"
import { Button, Drawer, Input, Switch, Textarea } from "@medusajs/ui" import { Button, Input, Switch, Textarea } from "@medusajs/ui"
import { useAdminUpdateSalesChannel } from "medusa-react" import { useAdminUpdateSalesChannel } from "medusa-react"
import { useForm } from "react-hook-form" import { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
import * as zod from "zod" import * as zod from "zod"
import { useEffect } from "react"
import { Form } from "../../../../../components/common/form" import { Form } from "../../../../../components/common/form"
import {
RouteDrawer,
useRouteModal,
} from "../../../../../components/route-modal"
type EditSalesChannelFormProps = { type EditSalesChannelFormProps = {
salesChannel: SalesChannel salesChannel: SalesChannel
subscribe: (state: boolean) => void
onSuccess: () => void
} }
const EditSalesChannelSchema = zod.object({ const EditSalesChannelSchema = zod.object({
@@ -23,9 +24,10 @@ const EditSalesChannelSchema = zod.object({
export const EditSalesChannelForm = ({ export const EditSalesChannelForm = ({
salesChannel, salesChannel,
subscribe,
onSuccess,
}: EditSalesChannelFormProps) => { }: EditSalesChannelFormProps) => {
const { t } = useTranslation()
const { handleSuccess } = useRouteModal()
const form = useForm<zod.infer<typeof EditSalesChannelSchema>>({ const form = useForm<zod.infer<typeof EditSalesChannelSchema>>({
defaultValues: { defaultValues: {
name: salesChannel.name, name: salesChannel.name,
@@ -35,16 +37,6 @@ export const EditSalesChannelForm = ({
resolver: zodResolver(EditSalesChannelSchema), resolver: zodResolver(EditSalesChannelSchema),
}) })
const {
formState: { isDirty },
} = form
useEffect(() => {
subscribe(isDirty)
}, [isDirty])
const { t } = useTranslation()
const { mutateAsync, isLoading } = useAdminUpdateSalesChannel(salesChannel.id) const { mutateAsync, isLoading } = useAdminUpdateSalesChannel(salesChannel.id)
const handleSubmit = form.handleSubmit(async (values) => { const handleSubmit = form.handleSubmit(async (values) => {
@@ -56,19 +48,19 @@ export const EditSalesChannelForm = ({
}, },
{ {
onSuccess: () => { onSuccess: () => {
onSuccess() handleSuccess()
}, },
} }
) )
}) })
return ( return (
<Form {...form}> <RouteDrawer.Form form={form}>
<form <form
onSubmit={handleSubmit} onSubmit={handleSubmit}
className="flex flex-1 flex-col overflow-hidden" className="flex flex-1 flex-col overflow-hidden"
> >
<Drawer.Body className="flex max-w-full flex-1 flex-col gap-y-8 overflow-y-auto"> <RouteDrawer.Body className="flex max-w-full flex-1 flex-col gap-y-8 overflow-y-auto">
<Form.Field <Form.Field
control={form.control} control={form.control}
name="name" name="name"
@@ -121,20 +113,20 @@ export const EditSalesChannelForm = ({
) )
}} }}
/> />
</Drawer.Body> </RouteDrawer.Body>
<Drawer.Footer> <RouteDrawer.Footer>
<div className="flex items-center justify-end gap-x-2"> <div className="flex items-center justify-end gap-x-2">
<Drawer.Close asChild> <RouteDrawer.Close asChild>
<Button size="small" variant="secondary"> <Button size="small" variant="secondary">
{t("actions.cancel")} {t("actions.cancel")}
</Button> </Button>
</Drawer.Close> </RouteDrawer.Close>
<Button size="small" type="submit" isLoading={isLoading}> <Button size="small" type="submit" isLoading={isLoading}>
{t("actions.save")} {t("actions.save")}
</Button> </Button>
</div> </div>
</Drawer.Footer> </RouteDrawer.Footer>
</form> </form>
</Form> </RouteDrawer.Form>
) )
} }
@@ -1,41 +1,31 @@
import { Drawer, Heading } from "@medusajs/ui" import { Heading } from "@medusajs/ui"
import { useAdminSalesChannel } from "medusa-react" import { useAdminSalesChannel } from "medusa-react"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
import { useParams } from "react-router-dom" import { useParams } from "react-router-dom"
import { useRouteModalState } from "../../../hooks/use-route-modal-state" import { RouteDrawer } from "../../../components/route-modal"
import { EditSalesChannelForm } from "./components/edit-sales-channel-form" import { EditSalesChannelForm } from "./components/edit-sales-channel-form"
export const SalesChannelEdit = () => { export const SalesChannelEdit = () => {
const { id } = useParams() const { id } = useParams()
const { t } = useTranslation() const { t } = useTranslation()
const [open, onOpenChange, subscribe] = useRouteModalState()
const { sales_channel, isLoading, isError, error } = useAdminSalesChannel(id!)
const onSuccess = () => { const { sales_channel, isLoading, isError, error } = useAdminSalesChannel(id!)
onOpenChange(false, true)
}
if (isError) { if (isError) {
throw error throw error
} }
return ( return (
<Drawer open={open} onOpenChange={onOpenChange}> <RouteDrawer>
<Drawer.Content> <RouteDrawer.Header>
<Drawer.Header>
<Heading className="capitalize"> <Heading className="capitalize">
{t("salesChannels.editSalesChannel")} {t("salesChannels.editSalesChannel")}
</Heading> </Heading>
</Drawer.Header> </RouteDrawer.Header>
{!isLoading && sales_channel && ( {!isLoading && sales_channel && (
<EditSalesChannelForm <EditSalesChannelForm salesChannel={sales_channel} />
salesChannel={sales_channel}
subscribe={subscribe}
onSuccess={onSuccess}
/>
)} )}
</Drawer.Content> </RouteDrawer>
</Drawer>
) )
} }
@@ -3,7 +3,6 @@ import {
Badge, Badge,
Button, Button,
Checkbox, Checkbox,
FocusModal,
Hint, Hint,
StatusBadge, StatusBadge,
Table, Table,
@@ -19,11 +18,18 @@ import {
useReactTable, useReactTable,
} from "@tanstack/react-table" } from "@tanstack/react-table"
import { useAdminCurrencies, useAdminUpdateStore } from "medusa-react" import { useAdminCurrencies, useAdminUpdateStore } from "medusa-react"
import { FormEvent, useMemo, useState } from "react" import { useEffect, useMemo, useState } from "react"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
import * as zod from "zod" import * as zod from "zod"
import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form"
import { OrderBy } from "../../../../../components/filtering/order-by" import { OrderBy } from "../../../../../components/filtering/order-by"
import { LocalizedTablePagination } from "../../../../../components/localization/localized-table-pagination" import { LocalizedTablePagination } from "../../../../../components/localization/localized-table-pagination"
import {
RouteFocusModal,
useRouteModal,
} from "../../../../../components/route-modal"
import { useHandleTableScroll } from "../../../../../hooks/use-handle-table-scroll" import { useHandleTableScroll } from "../../../../../hooks/use-handle-table-scroll"
import { useQueryParams } from "../../../../../hooks/use-query-params" import { useQueryParams } from "../../../../../hooks/use-query-params"
@@ -38,9 +44,18 @@ const AddCurrenciesSchema = zod.object({
const PAGE_SIZE = 50 const PAGE_SIZE = 50
export const AddCurrenciesForm = ({ store }: AddCurrenciesFormProps) => { export const AddCurrenciesForm = ({ store }: AddCurrenciesFormProps) => {
const [errorMessage, setErrorMessage] = useState<{ const { t } = useTranslation()
currencies?: string | undefined const { handleSuccess } = useRouteModal()
}>({})
const form = useForm<zod.infer<typeof AddCurrenciesSchema>>({
defaultValues: {
currencies: [],
},
resolver: zodResolver(AddCurrenciesSchema),
})
const { setValue } = form
const [{ pageIndex, pageSize }, setPagination] = useState<PaginationState>({ const [{ pageIndex, pageSize }, setPagination] = useState<PaginationState>({
pageIndex: 0, pageIndex: 0,
pageSize: PAGE_SIZE, pageSize: PAGE_SIZE,
@@ -56,8 +71,16 @@ export const AddCurrenciesForm = ({ store }: AddCurrenciesFormProps) => {
const [rowSelection, setRowSelection] = useState<RowSelectionState>({}) const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
useEffect(() => {
const ids = Object.keys(rowSelection)
setValue("currencies", ids, {
shouldDirty: true,
shouldTouch: true,
})
}, [rowSelection, setValue])
const params = useQueryParams(["order"]) const params = useQueryParams(["order"])
const { currencies, count, isLoading, isError, error } = useAdminCurrencies({ const { currencies, count, isError, error } = useAdminCurrencies({
limit: PAGE_SIZE, limit: PAGE_SIZE,
offset: pageIndex * PAGE_SIZE, offset: pageIndex * PAGE_SIZE,
...params, ...params,
@@ -83,69 +106,59 @@ export const AddCurrenciesForm = ({ store }: AddCurrenciesFormProps) => {
manualPagination: true, manualPagination: true,
}) })
const { t } = useTranslation()
const { mutateAsync, isLoading: isMutating } = useAdminUpdateStore() const { mutateAsync, isLoading: isMutating } = useAdminUpdateStore()
const { handleScroll, isScrolled, tableContainerRef } = useHandleTableScroll() const { handleScroll, isScrolled, tableContainerRef } = useHandleTableScroll()
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => { const handleSubmit = form.handleSubmit(async (data) => {
e.preventDefault()
const ids = Object.keys(rowSelection)
try {
AddCurrenciesSchema.parse({
currencies: ids,
})
setErrorMessage({})
} catch (err) {
if (err instanceof zod.ZodError) {
setErrorMessage(err.flatten().fieldErrors)
}
return
}
const currencies = Array.from( const currencies = Array.from(
new Set([...ids, ...preSelectedRows]) new Set([...data.currencies, ...preSelectedRows])
) as string[] ) as string[]
await mutateAsync({ await mutateAsync(
{
currencies, currencies,
}) },
{
onSuccess: () => {
handleSuccess()
},
} }
)
})
if (isError) { if (isError) {
throw error throw error
} }
return ( return (
<RouteFocusModal.Form form={form}>
<form <form
onSubmit={handleSubmit} onSubmit={handleSubmit}
className="flex h-full flex-col overflow-hidden" className="flex h-full flex-col overflow-hidden"
> >
<FocusModal.Header> <RouteFocusModal.Header>
<div className="flex flex-1 items-center justify-between"> <div className="flex flex-1 items-center justify-between">
<div> <div className="flex items-center">
{errorMessage.currencies && ( {form.formState.errors.currencies && (
<Hint variant="error">{errorMessage.currencies}</Hint> <Hint variant="error">
{form.formState.errors.currencies.message}
</Hint>
)} )}
</div> </div>
<div className="flex items-center justify-end gap-x-2"> <div className="flex items-center justify-end gap-x-2">
<FocusModal.Close asChild> <RouteFocusModal.Close asChild>
<Button size="small" variant="secondary"> <Button size="small" variant="secondary">
{t("actions.cancel")} {t("actions.cancel")}
</Button> </Button>
</FocusModal.Close> </RouteFocusModal.Close>
<Button size="small" type="submit" isLoading={isMutating}> <Button size="small" type="submit" isLoading={isMutating}>
{t("actions.save")} {t("actions.save")}
</Button> </Button>
</div> </div>
</div> </div>
</FocusModal.Header> </RouteFocusModal.Header>
<FocusModal.Body className="flex flex-1 flex-col overflow-hidden"> <RouteFocusModal.Body className="flex flex-1 flex-col overflow-hidden">
<div className="flex items-center justify-between border-b px-6 py-4"> <div className="flex items-center justify-between border-b px-6 py-4">
<div></div> <div></div>
<div className="flex items-center gap-x-2"> <div className="flex items-center gap-x-2">
@@ -227,8 +240,9 @@ export const AddCurrenciesForm = ({ store }: AddCurrenciesFormProps) => {
pageSize={PAGE_SIZE} pageSize={PAGE_SIZE}
/> />
</div> </div>
</FocusModal.Body> </RouteFocusModal.Body>
</form> </form>
</RouteFocusModal.Form>
) )
} }
@@ -1,42 +1,17 @@
import { FocusModal } from "@medusajs/ui"
import { useAdminStore } from "medusa-react" import { useAdminStore } from "medusa-react"
import { useEffect, useState } from "react" import { RouteFocusModal } from "../../../components/route-modal"
import { useNavigate } from "react-router-dom"
import { AddCurrenciesForm } from "./components/add-currencies-form/add-currencies-form" import { AddCurrenciesForm } from "./components/add-currencies-form/add-currencies-form"
export const StoreAddCurrencies = () => { export const StoreAddCurrencies = () => {
const [open, setOpen] = useState(false)
const navigate = useNavigate()
const { store, isLoading, isError, error } = useAdminStore() const { store, isLoading, isError, error } = useAdminStore()
useEffect(() => {
setOpen(true)
}, [])
const onOpenChange = (open: boolean) => {
if (!open) {
setTimeout(() => {
navigate(`/settings/store`, { replace: true })
}, 200)
}
setOpen(open)
}
if (isError) { if (isError) {
throw error throw error
} }
return ( return (
<FocusModal open={open} onOpenChange={onOpenChange}> <RouteFocusModal>
<FocusModal.Content> {!isLoading && store && <AddCurrenciesForm store={store} />}
{isLoading || !store ? ( </RouteFocusModal>
<div>Loading...</div>
) : (
<AddCurrenciesForm store={store} />
)}
</FocusModal.Content>
</FocusModal>
) )
} }
@@ -1,15 +1,18 @@
import { zodResolver } from "@hookform/resolvers/zod" import { zodResolver } from "@hookform/resolvers/zod"
import type { Store } from "@medusajs/medusa" import type { Store } from "@medusajs/medusa"
import { Button, Drawer, Input } from "@medusajs/ui" import { Button, Input } from "@medusajs/ui"
import { useAdminUpdateStore } from "medusa-react" import { useAdminUpdateStore } from "medusa-react"
import { useForm } from "react-hook-form" import { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
import * as zod from "zod" import * as zod from "zod"
import { Form } from "../../../../../components/common/form" import { Form } from "../../../../../components/common/form"
import {
RouteDrawer,
useRouteModal,
} from "../../../../../components/route-modal"
type EditStoreFormProps = { type EditStoreFormProps = {
store: Store store: Store
onSuccess: () => void
} }
const EditStoreSchema = zod.object({ const EditStoreSchema = zod.object({
@@ -22,8 +25,9 @@ const EditStoreSchema = zod.object({
invite_link_template: zod.union([zod.literal(""), zod.string().trim().url()]), invite_link_template: zod.union([zod.literal(""), zod.string().trim().url()]),
}) })
export const EditStoreForm = ({ store, onSuccess }: EditStoreFormProps) => { export const EditStoreForm = ({ store }: EditStoreFormProps) => {
const { mutateAsync, isLoading } = useAdminUpdateStore() const { t } = useTranslation()
const { handleSuccess } = useRouteModal()
const form = useForm<zod.infer<typeof EditStoreSchema>>({ const form = useForm<zod.infer<typeof EditStoreSchema>>({
defaultValues: { defaultValues: {
@@ -35,7 +39,7 @@ export const EditStoreForm = ({ store, onSuccess }: EditStoreFormProps) => {
resolver: zodResolver(EditStoreSchema), resolver: zodResolver(EditStoreSchema),
}) })
const { t } = useTranslation() const { mutateAsync, isLoading } = useAdminUpdateStore()
const handleSubmit = form.handleSubmit(async (values) => { const handleSubmit = form.handleSubmit(async (values) => {
mutateAsync( mutateAsync(
@@ -47,16 +51,16 @@ export const EditStoreForm = ({ store, onSuccess }: EditStoreFormProps) => {
}, },
{ {
onSuccess: () => { onSuccess: () => {
onSuccess() handleSuccess()
}, },
} }
) )
}) })
return ( return (
<Form {...form}> <RouteDrawer.Form form={form}>
<form onSubmit={handleSubmit} className="flex h-full flex-col"> <form onSubmit={handleSubmit} className="flex h-full flex-col">
<Drawer.Body> <RouteDrawer.Body>
<div className="flex flex-col gap-y-8"> <div className="flex flex-col gap-y-8">
<Form.Field <Form.Field
control={form.control} control={form.control}
@@ -123,20 +127,20 @@ export const EditStoreForm = ({ store, onSuccess }: EditStoreFormProps) => {
)} )}
/> />
</div> </div>
</Drawer.Body> </RouteDrawer.Body>
<Drawer.Footer> <RouteDrawer.Footer>
<div className="flex items-center justify-end gap-x-2"> <div className="flex items-center justify-end gap-x-2">
<Drawer.Close asChild> <RouteDrawer.Close asChild>
<Button size="small" variant="secondary"> <Button size="small" variant="secondary">
{t("actions.cancel")} {t("actions.cancel")}
</Button> </Button>
</Drawer.Close> </RouteDrawer.Close>
<Button size="small" isLoading={isLoading} type="submit"> <Button size="small" isLoading={isLoading} type="submit">
{t("actions.save")} {t("actions.save")}
</Button> </Button>
</div> </div>
</Drawer.Footer> </RouteDrawer.Footer>
</form> </form>
</Form> </RouteDrawer.Form>
) )
} }
@@ -1,30 +1,13 @@
import { Drawer, Heading } from "@medusajs/ui" import { Heading } from "@medusajs/ui"
import { useAdminStore } from "medusa-react" import { useAdminStore } from "medusa-react"
import { useEffect, useState } from "react"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
import { json, useNavigate } from "react-router-dom" import { json } from "react-router-dom"
import { RouteDrawer } from "../../../components/route-modal"
import { EditStoreForm } from "./components/edit-store-form/edit-store-form" import { EditStoreForm } from "./components/edit-store-form/edit-store-form"
export const StoreEdit = () => { export const StoreEdit = () => {
const [open, setOpen] = useState(false)
const { store, isLoading, isError, error } = useAdminStore()
const navigate = useNavigate()
const { t } = useTranslation() const { t } = useTranslation()
const { store, isLoading, isError, error } = useAdminStore()
useEffect(() => {
setOpen(true)
}, [])
const onOpenChange = (open: boolean) => {
if (!open) {
setTimeout(() => {
navigate(`/settings/store`, { replace: true })
}, 200)
}
setOpen(open)
}
if (isError) { if (isError) {
throw error throw error
@@ -35,15 +18,11 @@ export const StoreEdit = () => {
} }
return ( return (
<Drawer open={open} onOpenChange={onOpenChange}> <RouteDrawer>
<Drawer.Content> <RouteDrawer.Header>
<Drawer.Header>
<Heading className="capitalize">{t("store.editStore")}</Heading> <Heading className="capitalize">{t("store.editStore")}</Heading>
</Drawer.Header> </RouteDrawer.Header>
{store && ( {store && <EditStoreForm store={store} />}
<EditStoreForm store={store} onSuccess={() => onOpenChange(false)} /> </RouteDrawer>
)}
</Drawer.Content>
</Drawer>
) )
} }
@@ -1,17 +1,19 @@
import { zodResolver } from "@hookform/resolvers/zod" import { zodResolver } from "@hookform/resolvers/zod"
import { User } from "@medusajs/medusa" import { User } from "@medusajs/medusa"
import { Button, Drawer, Input } from "@medusajs/ui" import { Button, Input } from "@medusajs/ui"
import { useAdminUpdateUser } from "medusa-react" import { useAdminUpdateUser } from "medusa-react"
import { useEffect } from "react"
import { useForm } from "react-hook-form" import { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
import * as zod from "zod" import * as zod from "zod"
import { Form } from "../../../../../components/common/form" import { Form } from "../../../../../components/common/form"
import {
RouteDrawer,
useRouteModal,
} from "../../../../../components/route-modal"
type EditUserFormProps = { type EditUserFormProps = {
user: Omit<User, "password_hash"> user: Omit<User, "password_hash">
subscribe: (state: boolean) => void
onSuccessfulSubmit: () => void
} }
const EditUserFormSchema = zod.object({ const EditUserFormSchema = zod.object({
@@ -19,11 +21,10 @@ const EditUserFormSchema = zod.object({
last_name: zod.string().optional(), last_name: zod.string().optional(),
}) })
export const EditUserForm = ({ export const EditUserForm = ({ user }: EditUserFormProps) => {
user, const { t } = useTranslation()
subscribe, const { handleSuccess } = useRouteModal()
onSuccessfulSubmit,
}: EditUserFormProps) => {
const form = useForm<zod.infer<typeof EditUserFormSchema>>({ const form = useForm<zod.infer<typeof EditUserFormSchema>>({
defaultValues: { defaultValues: {
first_name: user.first_name || "", first_name: user.first_name || "",
@@ -32,33 +33,23 @@ export const EditUserForm = ({
resolver: zodResolver(EditUserFormSchema), resolver: zodResolver(EditUserFormSchema),
}) })
const {
formState: { isDirty },
} = form
useEffect(() => {
subscribe(isDirty)
}, [isDirty])
const { t } = useTranslation()
const { mutateAsync, isLoading } = useAdminUpdateUser(user.id) const { mutateAsync, isLoading } = useAdminUpdateUser(user.id)
const handleSubmit = form.handleSubmit(async (values) => { const handleSubmit = form.handleSubmit(async (values) => {
await mutateAsync(values, { await mutateAsync(values, {
onSuccess: () => { onSuccess: () => {
onSuccessfulSubmit() handleSuccess()
}, },
}) })
}) })
return ( return (
<Form {...form}> <RouteDrawer.Form form={form}>
<form <form
onSubmit={handleSubmit} onSubmit={handleSubmit}
className="flex flex-1 flex-col overflow-hidden" className="flex flex-1 flex-col overflow-hidden"
> >
<Drawer.Body className="flex max-w-full flex-1 flex-col gap-y-8 overflow-y-auto"> <RouteDrawer.Body className="flex max-w-full flex-1 flex-col gap-y-8 overflow-y-auto">
<Form.Field <Form.Field
control={form.control} control={form.control}
name="first_name" name="first_name"
@@ -89,20 +80,20 @@ export const EditUserForm = ({
) )
}} }}
/> />
</Drawer.Body> </RouteDrawer.Body>
<Drawer.Footer> <RouteDrawer.Footer>
<div className="flex items-center justify-end gap-x-2"> <div className="flex items-center justify-end gap-x-2">
<Drawer.Close asChild> <RouteDrawer.Close asChild>
<Button size="small" variant="secondary"> <Button size="small" variant="secondary">
{t("actions.cancel")} {t("actions.cancel")}
</Button> </Button>
</Drawer.Close> </RouteDrawer.Close>
<Button size="small" type="submit" isLoading={isLoading}> <Button size="small" type="submit" isLoading={isLoading}>
{t("actions.save")} {t("actions.save")}
</Button> </Button>
</div> </div>
</Drawer.Footer> </RouteDrawer.Footer>
</form> </form>
</Form> </RouteDrawer.Form>
) )
} }
@@ -1,39 +1,25 @@
import { Drawer, Heading } from "@medusajs/ui" import { Heading } from "@medusajs/ui"
import { useAdminUser } from "medusa-react" import { useAdminUser } from "medusa-react"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
import { useParams } from "react-router-dom" import { useParams } from "react-router-dom"
import { useRouteModalState } from "../../../hooks/use-route-modal-state" import { RouteDrawer } from "../../../components/route-modal"
import { EditUserForm } from "./components/edit-user-form" import { EditUserForm } from "./components/edit-user-form"
export const UserEdit = () => { export const UserEdit = () => {
const [open, onOpenChange, subscribe] = useRouteModalState()
const { t } = useTranslation() const { t } = useTranslation()
const { id } = useParams() const { id } = useParams()
const { user, isLoading, isError, error } = useAdminUser(id!) const { user, isLoading, isError, error } = useAdminUser(id!)
const handleSuccessfulSubmit = () => {
onOpenChange(false, true)
}
if (isError) { if (isError) {
throw error throw error
} }
return ( return (
<Drawer open={open} onOpenChange={onOpenChange}> <RouteDrawer>
<Drawer.Content> <RouteDrawer.Header>
<Drawer.Header>
<Heading>{t("users.editUser")}</Heading> <Heading>{t("users.editUser")}</Heading>
</Drawer.Header> </RouteDrawer.Header>
{!isLoading && user && ( {!isLoading && user && <EditUserForm user={user} />}
<EditUserForm </RouteDrawer>
user={user}
subscribe={subscribe}
onSuccessfulSubmit={handleSuccessfulSubmit}
/>
)}
</Drawer.Content>
</Drawer>
) )
} }
@@ -4,7 +4,6 @@ import { Invite } from "@medusajs/medusa"
import { import {
Button, Button,
Container, Container,
FocusModal,
Heading, Heading,
Input, Input,
Select, Select,
@@ -30,7 +29,7 @@ import {
useAdminResendInvite, useAdminResendInvite,
useAdminStore, useAdminStore,
} from "medusa-react" } from "medusa-react"
import { useEffect, useMemo } from "react" import { useMemo } from "react"
import { useForm } from "react-hook-form" import { useForm } from "react-hook-form"
import { Trans, useTranslation } from "react-i18next" import { Trans, useTranslation } from "react-i18next"
import * as zod from "zod" import * as zod from "zod"
@@ -38,10 +37,7 @@ import { ActionMenu } from "../../../../../components/common/action-menu"
import { NoRecords } from "../../../../../components/common/empty-table-content" import { NoRecords } from "../../../../../components/common/empty-table-content"
import { Form } from "../../../../../components/common/form" import { Form } from "../../../../../components/common/form"
import { LocalizedTablePagination } from "../../../../../components/localization/localized-table-pagination" import { LocalizedTablePagination } from "../../../../../components/localization/localized-table-pagination"
import { RouteFocusModal } from "../../../../../components/route-modal"
type InviteUserFormProps = {
subscribe: (state: boolean) => void
}
enum UserRole { enum UserRole {
MEMBER = "member", MEMBER = "member",
@@ -56,7 +52,9 @@ const InviteUserSchema = zod.object({
const PAGE_SIZE = 10 const PAGE_SIZE = 10
export const InviteUserForm = ({ subscribe }: InviteUserFormProps) => { export const InviteUserForm = () => {
const { t } = useTranslation()
const form = useForm<zod.infer<typeof InviteUserSchema>>({ const form = useForm<zod.infer<typeof InviteUserSchema>>({
defaultValues: { defaultValues: {
user: "", user: "",
@@ -64,15 +62,6 @@ export const InviteUserForm = ({ subscribe }: InviteUserFormProps) => {
}, },
resolver: zodResolver(InviteUserSchema), resolver: zodResolver(InviteUserSchema),
}) })
const { mutateAsync, isLoading: isMutating } = useAdminCreateInvite()
const {
formState: { isDirty },
} = form
useEffect(() => {
subscribe(isDirty)
}, [isDirty, subscribe])
const { invites, isLoading, isError, error } = useAdminInvites() const { invites, isLoading, isError, error } = useAdminInvites()
const count = invites?.length ?? 0 const count = invites?.length ?? 0
@@ -89,7 +78,7 @@ export const InviteUserForm = ({ subscribe }: InviteUserFormProps) => {
getPaginationRowModel: getPaginationRowModel(), getPaginationRowModel: getPaginationRowModel(),
}) })
const { t } = useTranslation() const { mutateAsync, isLoading: isMutating } = useAdminCreateInvite()
const handleSubmit = form.handleSubmit(async (values) => { const handleSubmit = form.handleSubmit(async (values) => {
await mutateAsync( await mutateAsync(
@@ -110,13 +99,13 @@ export const InviteUserForm = ({ subscribe }: InviteUserFormProps) => {
} }
return ( return (
<Form {...form}> <RouteFocusModal.Form form={form}>
<form <form
onSubmit={handleSubmit} onSubmit={handleSubmit}
className="flex h-full flex-col overflow-hidden" className="flex h-full flex-col overflow-hidden"
> >
<FocusModal.Header /> <RouteFocusModal.Header />
<FocusModal.Body className="flex flex-1 flex-col overflow-hidden"> <RouteFocusModal.Body className="flex flex-1 flex-col overflow-hidden">
<div className="flex flex-1 flex-col items-center overflow-y-auto"> <div className="flex flex-1 flex-col items-center overflow-y-auto">
<div className="flex w-full max-w-[720px] flex-col gap-y-8 px-2 py-16"> <div className="flex w-full max-w-[720px] flex-col gap-y-8 px-2 py-16">
<div> <div>
@@ -249,9 +238,9 @@ export const InviteUserForm = ({ subscribe }: InviteUserFormProps) => {
</div> </div>
</div> </div>
</div> </div>
</FocusModal.Body> </RouteFocusModal.Body>
</form> </form>
</Form> </RouteFocusModal.Form>
) )
} }
@@ -1,15 +1,10 @@
import { FocusModal } from "@medusajs/ui" import { RouteFocusModal } from "../../../components/route-modal"
import { useRouteModalState } from "../../../hooks/use-route-modal-state"
import { InviteUserForm } from "./components/invite-user-form/invite-user-form" import { InviteUserForm } from "./components/invite-user-form/invite-user-form"
export const UserInvite = () => { export const UserInvite = () => {
const [open, onOpenChange, subscribe] = useRouteModalState()
return ( return (
<FocusModal open={open} onOpenChange={onOpenChange}> <RouteFocusModal>
<FocusModal.Content> <InviteUserForm />
<InviteUserForm subscribe={subscribe} /> </RouteFocusModal>
</FocusModal.Content>
</FocusModal>
) )
} }