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
2024-02-22 10:03:40 +01:00
committed by GitHub
parent add861d205
commit 72a17d6cd7
88 changed files with 1750 additions and 1799 deletions

View File

@@ -83,6 +83,7 @@
"gallery": "Gallery",
"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.",
"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.",
"availableInSalesChannels": "Available in <0>{{x}}</0> of <1>{{y}}</1> 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.",
"providersHint": "The providers that are available in the region.",
"shippingOptions": "Shipping Options",
"returnShippingOptions": "Return Shipping Options"
"returnShippingOptions": "Return Shipping Options",
"return": "Return",
"outbound": "Outbound",
"priceType": "Price Type",
"flatRate": "Flat Rate",
"calculated": "Calculated"
},
"locations": {
"domain": "Locations",
@@ -399,6 +405,7 @@
"dateIssued": "Date issued",
"issuedDate": "Issued date",
"expiryDate": "Expiry date",
"price": "Price",
"height": "Height",
"width": "Width",
"length": "Length",

View File

@@ -0,0 +1,3 @@
export { RouteDrawer } from "./route-drawer"
export { RouteFocusModal } from "./route-focus-modal"
export { useRouteModal } from "./route-modal-provider"

View File

@@ -0,0 +1 @@
export * from "./route-drawer"

View File

@@ -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,
})

View File

@@ -0,0 +1 @@
export * from "./route-focus-modal"

View File

@@ -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,
})

View File

@@ -0,0 +1 @@
export * from "./route-form"

View File

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

View File

@@ -0,0 +1 @@
export * from "./route-provider"

View File

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

View File

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

View File

@@ -0,0 +1 @@
export * from "./countries-cell"

View File

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

View File

@@ -0,0 +1 @@
export * from "./fulfillment-providers-cell"

View File

@@ -0,0 +1 @@
export * from "./payment-providers-cell"

View File

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

View File

@@ -0,0 +1 @@
export * from "./region-cell"

View File

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

View File

@@ -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()} />
),
}),
],
[]
)
}

View File

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

View File

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

View File

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

View File

@@ -1,36 +1,26 @@
import { FocusModal } from "@medusajs/ui"
import { useAdminPublishableApiKeySalesChannels } from "medusa-react"
import { useParams } from "react-router-dom"
import { useRouteModalState } from "../../../hooks/use-route-modal-state"
import { RouteFocusModal } from "../../../components/route-modal"
import { AddSalesChannelsToApiKeyForm } from "./components"
export const ApiKeyManagementAddSalesChannels = () => {
const { id } = useParams()
const [open, onOpenChange, subscribe] = useRouteModalState()
const { sales_channels, isLoading, isError, error } =
useAdminPublishableApiKeySalesChannels(id!)
const handleSuccessfulSubmit = () => {
onOpenChange(false, true)
}
if (isError) {
throw error
}
return (
<FocusModal open={open} onOpenChange={onOpenChange}>
<FocusModal.Content>
{!isLoading && sales_channels && (
<AddSalesChannelsToApiKeyForm
apiKey={id!}
preSelected={sales_channels.map((sc) => sc.id)}
onSuccessfulSubmit={handleSuccessfulSubmit}
subscribe={subscribe}
/>
)}
</FocusModal.Content>
</FocusModal>
<RouteFocusModal>
{!isLoading && sales_channels && (
<AddSalesChannelsToApiKeyForm
apiKey={id!}
preSelected={sales_channels.map((sc) => sc.id)}
/>
)}
</RouteFocusModal>
)
}

View File

@@ -3,7 +3,6 @@ import { SalesChannel } from "@medusajs/medusa"
import {
Button,
Checkbox,
FocusModal,
Hint,
StatusBadge,
Table,
@@ -26,17 +25,18 @@ import { useEffect, useMemo, useState } from "react"
import { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next"
import * as zod from "zod"
import { Form } from "../../../../components/common/form"
import { OrderBy } from "../../../../components/filtering/order-by"
import { Query } from "../../../../components/filtering/query"
import { LocalizedTablePagination } from "../../../../components/localization/localized-table-pagination"
import {
RouteFocusModal,
useRouteModal,
} from "../../../../components/route-modal"
import { useQueryParams } from "../../../../hooks/use-query-params"
type AddSalesChannelsToApiKeyFormProps = {
apiKey: string
preSelected: string[]
subscribe: (state: boolean) => void
onSuccessfulSubmit: () => void
}
const AddSalesChannelsToApiKeySchema = zod.object({
@@ -48,10 +48,9 @@ const PAGE_SIZE = 50
export const AddSalesChannelsToApiKeyForm = ({
apiKey,
preSelected,
subscribe,
onSuccessfulSubmit,
}: AddSalesChannelsToApiKeyFormProps) => {
const { t } = useTranslation()
const { handleSuccess } = useRouteModal()
const form = useForm<zod.infer<typeof AddSalesChannelsToApiKeySchema>>({
defaultValues: {
@@ -60,13 +59,7 @@ export const AddSalesChannelsToApiKeyForm = ({
resolver: zodResolver(AddSalesChannelsToApiKeySchema),
})
const {
formState: { isDirty },
} = form
useEffect(() => {
subscribe(isDirty)
}, [isDirty])
const { setValue } = form
const { mutateAsync, isLoading: isMutating } =
useAdminAddPublishableKeySalesChannelsBatch(apiKey)
@@ -87,11 +80,15 @@ export const AddSalesChannelsToApiKeyForm = ({
const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
useEffect(() => {
form.setValue(
setValue(
"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 { sales_channels, count } = useAdminSalesChannels(
@@ -135,36 +132,36 @@ export const AddSalesChannelsToApiKeyForm = ({
},
{
onSuccess: () => {
onSuccessfulSubmit()
handleSuccess()
},
}
)
})
return (
<Form {...form}>
<RouteFocusModal.Form form={form}>
<form
onSubmit={handleSubmit}
className="flex h-full flex-col overflow-hidden"
>
<FocusModal.Header>
<RouteFocusModal.Header>
<div className="flex items-center justify-end gap-x-2">
{form.formState.errors.sales_channel_ids && (
<Hint variant="error">
{form.formState.errors.sales_channel_ids.message}
</Hint>
)}
<FocusModal.Close asChild>
<RouteFocusModal.Close asChild>
<Button size="small" variant="secondary">
{t("actions.cancel")}
</Button>
</FocusModal.Close>
</RouteFocusModal.Close>
<Button size="small" type="submit" isLoading={isMutating}>
{t("actions.save")}
</Button>
</div>
</FocusModal.Header>
<FocusModal.Body className="flex h-full w-full flex-col items-center divide-y overflow-y-auto">
</RouteFocusModal.Header>
<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></div>
<div className="flex items-center gap-x-2">
@@ -236,9 +233,9 @@ export const AddSalesChannelsToApiKeyForm = ({
pageSize={PAGE_SIZE}
/>
</div>
</FocusModal.Body>
</RouteFocusModal.Body>
</form>
</Form>
</RouteFocusModal.Form>
)
}

View File

@@ -1,15 +1,10 @@
import { FocusModal } from "@medusajs/ui"
import { useRouteModalState } from "../../../hooks/use-route-modal-state"
import { RouteFocusModal } from "../../../components/route-modal"
import { CreatePublishableApiKeyForm } from "./components/create-publishable-api-key-form"
export const ApiKeyManagementCreate = () => {
const [open, onOpenChange, subscribe] = useRouteModalState()
return (
<FocusModal open={open} onOpenChange={onOpenChange}>
<FocusModal.Content>
<CreatePublishableApiKeyForm subscribe={subscribe} />
</FocusModal.Content>
</FocusModal>
<RouteFocusModal>
<CreatePublishableApiKeyForm />
</RouteFocusModal>
)
}

View File

@@ -1,26 +1,23 @@
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 { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next"
import * as zod from "zod"
import { useEffect } from "react"
import { useNavigate } from "react-router-dom"
import { Form } from "../../../../../components/common/form"
type CreatePublishableApiKeyFormProps = {
subscribe: (state: boolean) => void
}
import {
RouteFocusModal,
useRouteModal,
} from "../../../../../components/route-modal"
const CreatePublishableApiKeySchema = zod.object({
title: zod.string().min(1),
})
export const CreatePublishableApiKeyForm = ({
subscribe,
}: CreatePublishableApiKeyFormProps) => {
const { mutateAsync, isLoading } = useAdminCreatePublishableApiKey()
export const CreatePublishableApiKeyForm = () => {
const { t } = useTranslation()
const { handleSuccess } = useRouteModal()
const form = useForm<zod.infer<typeof CreatePublishableApiKeySchema>>({
defaultValues: {
@@ -29,46 +26,35 @@ export const CreatePublishableApiKeyForm = ({
resolver: zodResolver(CreatePublishableApiKeySchema),
})
const {
formState: { isDirty },
} = form
useEffect(() => {
subscribe(isDirty)
}, [isDirty])
const { t } = useTranslation()
const navigate = useNavigate()
const { mutateAsync, isLoading } = useAdminCreatePublishableApiKey()
const handleSubmit = form.handleSubmit(async (values) => {
await mutateAsync(values, {
onSuccess: ({ publishable_api_key }) => {
navigate(`/settings/api-key-management/${publishable_api_key.id}`, {
replace: true,
})
handleSuccess(`/settings/api-key-management/${publishable_api_key.id}`)
},
})
})
return (
<Form {...form}>
<RouteFocusModal.Form form={form}>
<form
className="flex h-full flex-col overflow-hidden"
onSubmit={handleSubmit}
>
<FocusModal.Header>
<RouteFocusModal.Header>
<div className="flex items-center justify-end gap-x-2">
<FocusModal.Close asChild>
<RouteFocusModal.Close asChild>
<Button size="small" variant="secondary">
{t("actions.cancel")}
</Button>
</FocusModal.Close>
</RouteFocusModal.Close>
<Button size="small" type="submit" isLoading={isLoading}>
{t("actions.save")}
</Button>
</div>
</FocusModal.Header>
<FocusModal.Body className="flex flex-1 flex-col overflow-hidden">
</RouteFocusModal.Header>
<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 w-full max-w-[720px] flex-col gap-y-8 px-2 py-16">
<div>
@@ -88,7 +74,7 @@ export const CreatePublishableApiKeyForm = ({
<Form.Item>
<Form.Label>{t("fields.title")}</Form.Label>
<Form.Control>
<Input size="small" {...field} />
<Input {...field} />
</Form.Control>
<Form.ErrorMessage />
</Form.Item>
@@ -98,8 +84,8 @@ export const CreatePublishableApiKeyForm = ({
</div>
</div>
</div>
</FocusModal.Body>
</RouteFocusModal.Body>
</form>
</Form>
</RouteFocusModal.Form>
)
}

View File

@@ -1,40 +1,29 @@
import { Drawer, Heading } from "@medusajs/ui"
import { Heading } from "@medusajs/ui"
import { useAdminPublishableApiKey } from "medusa-react"
import { useTranslation } from "react-i18next"
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"
export const ApiKeyManagementEdit = () => {
const [open, onOpenChange, subscribe] = useRouteModalState()
const { id } = useParams()
const { t } = useTranslation()
const { publishable_api_key, isLoading, isError, error } =
useAdminPublishableApiKey(id!)
const handleSuccessfulSubmit = () => {
onOpenChange(false, true)
}
if (isError) {
throw error
}
return (
<Drawer open={open} onOpenChange={onOpenChange}>
<Drawer.Content>
<Drawer.Header>
<Heading>{t("apiKeyManagement.editKey")}</Heading>
</Drawer.Header>
{!isLoading && publishable_api_key && (
<EditApiKeyForm
apiKey={publishable_api_key}
onSuccessfulSubmit={handleSuccessfulSubmit}
subscribe={subscribe}
/>
)}
</Drawer.Content>
</Drawer>
<RouteDrawer>
<RouteDrawer.Header>
<Heading>{t("apiKeyManagement.editKey")}</Heading>
</RouteDrawer.Header>
{!isLoading && publishable_api_key && (
<EditApiKeyForm apiKey={publishable_api_key} />
)}
</RouteDrawer>
)
}

View File

@@ -1,29 +1,28 @@
import { zodResolver } from "@hookform/resolvers/zod"
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 { useEffect } from "react"
import { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next"
import * as zod from "zod"
import { Form } from "../../../../../components/common/form"
import {
RouteDrawer,
useRouteModal,
} from "../../../../../components/route-modal"
type EditApiKeyFormProps = {
apiKey: PublishableApiKey
onSuccessfulSubmit: () => void
subscribe: (state: boolean) => void
}
const EditApiKeySchema = zod.object({
title: zod.string().min(1),
})
export const EditApiKeyForm = ({
apiKey,
onSuccessfulSubmit,
subscribe,
}: EditApiKeyFormProps) => {
export const EditApiKeyForm = ({ apiKey }: EditApiKeyFormProps) => {
const { t } = useTranslation()
const { handleSuccess } = useRouteModal()
const form = useForm<zod.infer<typeof EditApiKeySchema>>({
defaultValues: {
@@ -32,28 +31,23 @@ export const EditApiKeyForm = ({
resolver: zodResolver(EditApiKeySchema),
})
const {
formState: { isDirty },
} = form
useEffect(() => {
subscribe(isDirty)
}, [isDirty])
const { mutateAsync, isLoading } = useAdminUpdatePublishableApiKey(apiKey.id)
const handleSubmit = form.handleSubmit(async (data) => {
await mutateAsync(data, {
onSuccess: () => {
onSuccessfulSubmit()
handleSuccess()
},
onError: (error) => {
console.log(error)
},
})
})
return (
<Form {...form}>
<RouteDrawer.Form form={form}>
<form onSubmit={handleSubmit} className="flex flex-1 flex-col">
<Drawer.Body>
<RouteDrawer.Body>
<div className="flex flex-col gap-y-4">
<Form.Field
control={form.control}
@@ -71,20 +65,20 @@ export const EditApiKeyForm = ({
}}
/>
</div>
</Drawer.Body>
<Drawer.Footer>
</RouteDrawer.Body>
<RouteDrawer.Footer>
<div className="flex items-center gap-x-2">
<Drawer.Close asChild>
<RouteDrawer.Close asChild>
<Button size="small" variant="secondary">
{t("actions.cancel")}
</Button>
</Drawer.Close>
</RouteDrawer.Close>
<Button size="small" type="submit" isLoading={isLoading}>
{t("actions.save")}
</Button>
</div>
</Drawer.Footer>
</RouteDrawer.Footer>
</form>
</Form>
</RouteDrawer.Form>
)
}

View File

@@ -1,34 +1,22 @@
import { FocusModal } from "@medusajs/ui"
import { useAdminCollection } from "medusa-react"
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"
export const CollectionAddProducts = () => {
const { id } = useParams()
const { collection, isLoading, isError, error } = useAdminCollection(id!)
const [open, onOpenChange, subscribe] = useRouteModalState()
const handleSuccessfulSubmit = () => {
onOpenChange(false, true)
}
if (isError) {
throw error
}
return (
<FocusModal open={open} onOpenChange={onOpenChange}>
<FocusModal.Content>
{!isLoading && collection && (
<AddProductsToCollectionForm
collection={collection}
onSuccessfulSubmit={handleSuccessfulSubmit}
subscribe={subscribe}
/>
)}
</FocusModal.Content>
</FocusModal>
<RouteFocusModal>
{!isLoading && collection && (
<AddProductsToCollectionForm collection={collection} />
)}
</RouteFocusModal>
)
}

View File

@@ -1,14 +1,6 @@
import { zodResolver } from "@hookform/resolvers/zod"
import type { Product, ProductCollection } from "@medusajs/medusa"
import {
Button,
Checkbox,
FocusModal,
Hint,
Table,
Tooltip,
clx,
} from "@medusajs/ui"
import { Button, Checkbox, Hint, Table, Tooltip, clx } from "@medusajs/ui"
import {
PaginationState,
RowSelectionState,
@@ -30,7 +22,6 @@ import {
NoRecords,
NoResults,
} from "../../../../../components/common/empty-table-content"
import { Form } from "../../../../../components/common/form"
import {
ProductAvailabilityCell,
ProductCollectionCell,
@@ -41,14 +32,16 @@ import {
import { OrderBy } from "../../../../../components/filtering/order-by"
import { Query } from "../../../../../components/filtering/query"
import { LocalizedTablePagination } from "../../../../../components/localization/localized-table-pagination"
import {
RouteFocusModal,
useRouteModal,
} from "../../../../../components/route-modal"
import { useHandleTableScroll } from "../../../../../hooks/use-handle-table-scroll"
import { useQueryParams } from "../../../../../hooks/use-query-params"
import { queryClient } from "../../../../../lib/medusa"
type AddProductsToCollectionFormProps = {
collection: ProductCollection
subscribe: (state: boolean) => void
onSuccessfulSubmit: () => void
}
const AddProductsToSalesChannelSchema = zod.object({
@@ -59,10 +52,9 @@ const PAGE_SIZE = 50
export const AddProductsToCollectionForm = ({
collection,
subscribe,
onSuccessfulSubmit,
}: AddProductsToCollectionFormProps) => {
const { t } = useTranslation()
const { handleSuccess } = useRouteModal()
const form = useForm<zod.infer<typeof AddProductsToSalesChannelSchema>>({
defaultValues: {
@@ -71,13 +63,7 @@ export const AddProductsToCollectionForm = ({
resolver: zodResolver(AddProductsToSalesChannelSchema),
})
const {
formState: { isDirty },
} = form
useEffect(() => {
subscribe(isDirty)
}, [isDirty])
const { setValue } = form
const { mutateAsync, isLoading: isMutating } =
useAdminAddProductsToCollection(collection.id)
@@ -98,11 +84,15 @@ export const AddProductsToCollectionForm = ({
const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
useEffect(() => {
form.setValue(
setValue(
"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"])
@@ -151,7 +141,7 @@ export const AddProductsToCollectionForm = ({
* determine if they are added to the collection or not.
*/
queryClient.invalidateQueries(adminProductKeys.lists())
onSuccessfulSubmit()
handleSuccess()
},
}
)
@@ -169,29 +159,29 @@ export const AddProductsToCollectionForm = ({
}
return (
<Form {...form}>
<RouteFocusModal.Form form={form}>
<form
onSubmit={handleSubmit}
className="flex h-full flex-col overflow-hidden"
>
<FocusModal.Header>
<RouteFocusModal.Header>
<div className="flex items-center justify-end gap-x-2">
{form.formState.errors.product_ids && (
<Hint variant="error">
{form.formState.errors.product_ids.message}
</Hint>
)}
<FocusModal.Close asChild>
<RouteFocusModal.Close asChild>
<Button size="small" variant="secondary">
{t("actions.cancel")}
</Button>
</FocusModal.Close>
</RouteFocusModal.Close>
<Button size="small" type="submit" isLoading={isMutating}>
{t("actions.save")}
</Button>
</div>
</FocusModal.Header>
<FocusModal.Body className="flex h-full w-full flex-col items-center divide-y overflow-y-auto">
</RouteFocusModal.Header>
<RouteFocusModal.Body className="flex h-full w-full flex-col items-center divide-y overflow-y-auto">
{!noRecords && (
<div className="flex w-full items-center justify-between px-6 py-4">
<div></div>
@@ -291,9 +281,9 @@ export const AddProductsToCollectionForm = ({
{/* TODO: fix this, and add NoRecords as well */}
</div>
)}
</FocusModal.Body>
</RouteFocusModal.Body>
</form>
</Form>
</RouteFocusModal.Form>
)
}

View File

@@ -1,15 +1,10 @@
import { FocusModal } from "@medusajs/ui"
import { useRouteModalState } from "../../../hooks/use-route-modal-state"
import { RouteFocusModal } from "../../../components/route-modal"
import { CreateCollectionForm } from "./components/create-collection-form"
export const CollectionCreate = () => {
const [open, onOpenChange, subscribe] = useRouteModalState()
return (
<FocusModal open={open} onOpenChange={onOpenChange}>
<FocusModal.Content>
<CreateCollectionForm subscribe={subscribe} />
</FocusModal.Content>
</FocusModal>
<RouteFocusModal>
<CreateCollectionForm />
</RouteFocusModal>
)
}

View File

@@ -1,27 +1,24 @@
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 { useEffect } from "react"
import { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next"
import { useNavigate } from "react-router-dom"
import * as zod from "zod"
import { Form } from "../../../../../components/common/form"
type CreateCollectionFormProps = {
subscribe: (state: boolean) => void
}
import { Form } from "../../../../../components/common/form"
import {
RouteFocusModal,
useRouteModal,
} from "../../../../../components/route-modal"
const CreateCollectionSchema = zod.object({
title: zod.string().min(1),
handle: zod.string().optional(),
})
export const CreateCollectionForm = ({
subscribe,
}: CreateCollectionFormProps) => {
export const CreateCollectionForm = () => {
const { t } = useTranslation()
const navigate = useNavigate()
const { handleSuccess } = useRouteModal()
const form = useForm<zod.infer<typeof CreateCollectionSchema>>({
defaultValues: {
@@ -31,34 +28,26 @@ export const CreateCollectionForm = ({
resolver: zodResolver(CreateCollectionSchema),
})
const {
formState: { isDirty },
} = form
useEffect(() => {
subscribe(isDirty)
}, [isDirty])
const { mutateAsync, isLoading } = useAdminCreateCollection()
const handleSubmit = form.handleSubmit(async (data) => {
await mutateAsync(data, {
onSuccess: ({ collection }) => {
navigate(`/collections/${collection.id}`)
handleSuccess(`/collections/${collection.id}`)
},
})
})
return (
<Form {...form}>
<RouteFocusModal.Form form={form}>
<form onSubmit={handleSubmit}>
<FocusModal.Header>
<RouteFocusModal.Header>
<div className="flex items-center justify-end gap-x-2">
<FocusModal.Close asChild>
<RouteFocusModal.Close asChild>
<Button size="small" variant="secondary">
{t("actions.cancel")}
</Button>
</FocusModal.Close>
</RouteFocusModal.Close>
<Button
size="small"
variant="primary"
@@ -68,8 +57,8 @@ export const CreateCollectionForm = ({
{t("actions.create")}
</Button>
</div>
</FocusModal.Header>
<FocusModal.Body className="flex flex-col items-center py-16">
</RouteFocusModal.Header>
<RouteFocusModal.Body className="flex flex-col items-center py-16">
<div className="flex w-full max-w-[720px] flex-col gap-y-8">
<div>
<Heading>{t("collections.createCollection")}</Heading>
@@ -131,8 +120,8 @@ export const CreateCollectionForm = ({
/>
</div>
</div>
</FocusModal.Body>
</RouteFocusModal.Body>
</form>
</Form>
</RouteFocusModal.Form>
)
}

View File

@@ -1,8 +1,8 @@
import { Drawer, Heading } from "@medusajs/ui"
import { Heading } from "@medusajs/ui"
import { useAdminCollection } from "medusa-react"
import { useTranslation } from "react-i18next"
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"
export const CollectionEdit = () => {
@@ -10,30 +10,18 @@ export const CollectionEdit = () => {
const { t } = useTranslation()
const { collection, isLoading, isError, error } = useAdminCollection(id!)
const [open, onOpenChange, subscribe] = useRouteModalState()
const handleSuccessfulSubmit = () => {
onOpenChange(false, true)
}
if (isError) {
throw error
}
return (
<Drawer open={open} onOpenChange={onOpenChange}>
<Drawer.Content>
<Drawer.Header>
<Heading>{t("collections.editCollection")}</Heading>
</Drawer.Header>
{!isLoading && collection && (
<EditCollectionForm
collection={collection}
onSuccessfulSubmit={handleSuccessfulSubmit}
subscribe={subscribe}
/>
)}
</Drawer.Content>
</Drawer>
<RouteDrawer>
<RouteDrawer.Header>
<Heading>{t("collections.editCollection")}</Heading>
</RouteDrawer.Header>
{!isLoading && collection && (
<EditCollectionForm collection={collection} />
)}
</RouteDrawer>
)
}

View File

@@ -1,17 +1,19 @@
import { zodResolver } from "@hookform/resolvers/zod"
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 { useEffect } from "react"
import { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next"
import * as zod from "zod"
import { Form } from "../../../../../components/common/form"
import {
RouteDrawer,
useRouteModal,
} from "../../../../../components/route-modal"
type EditCollectionFormProps = {
collection: ProductCollection
subscribe: (state: boolean) => void
onSuccessfulSubmit: () => void
}
const EditCollectionSchema = zod.object({
@@ -19,12 +21,9 @@ const EditCollectionSchema = zod.object({
handle: zod.string().min(1),
})
export const EditCollectionForm = ({
collection,
onSuccessfulSubmit,
subscribe,
}: EditCollectionFormProps) => {
export const EditCollectionForm = ({ collection }: EditCollectionFormProps) => {
const { t } = useTranslation()
const { handleSuccess } = useRouteModal()
const form = useForm<zod.infer<typeof EditCollectionSchema>>({
defaultValues: {
@@ -34,28 +33,20 @@ export const EditCollectionForm = ({
resolver: zodResolver(EditCollectionSchema),
})
const {
formState: { isDirty },
} = form
useEffect(() => {
subscribe(isDirty)
}, [isDirty])
const { mutateAsync, isLoading } = useAdminUpdateCollection(collection.id)
const handleSubmit = form.handleSubmit(async (data) => {
await mutateAsync(data, {
onSuccess: () => {
onSuccessfulSubmit()
handleSuccess()
},
})
})
return (
<Form {...form}>
<RouteDrawer.Form form={form}>
<form onSubmit={handleSubmit} className="flex flex-1 flex-col">
<Drawer.Body>
<RouteDrawer.Body>
<div className="flex flex-col gap-y-4">
<Form.Field
control={form.control}
@@ -102,20 +93,20 @@ export const EditCollectionForm = ({
}}
/>
</div>
</Drawer.Body>
<Drawer.Footer>
</RouteDrawer.Body>
<RouteDrawer.Footer>
<div className="flex items-center gap-x-2">
<Drawer.Close asChild>
<RouteDrawer.Close asChild>
<Button size="small" variant="secondary">
{t("actions.cancel")}
</Button>
</Drawer.Close>
</RouteDrawer.Close>
<Button size="small" type="submit" isLoading={isLoading}>
{t("actions.save")}
</Button>
</div>
</Drawer.Footer>
</RouteDrawer.Footer>
</form>
</Form>
</RouteDrawer.Form>
)
}

View File

@@ -1,14 +1,6 @@
import { zodResolver } from "@hookform/resolvers/zod"
import { Customer } from "@medusajs/medusa"
import {
Button,
Checkbox,
FocusModal,
Hint,
Table,
Tooltip,
clx,
} from "@medusajs/ui"
import { Button, Checkbox, Hint, Table, Tooltip, clx } from "@medusajs/ui"
import {
PaginationState,
RowSelectionState,
@@ -25,21 +17,23 @@ import {
import { useEffect, useMemo, useState } from "react"
import { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next"
import { useNavigate } from "react-router-dom"
import * as zod from "zod"
import {
NoRecords,
NoResults,
} from "../../../../../components/common/empty-table-content"
import { Form } from "../../../../../components/common/form"
import { Query } from "../../../../../components/filtering/query"
import { LocalizedTablePagination } from "../../../../../components/localization/localized-table-pagination"
import {
RouteFocusModal,
useRouteModal,
} from "../../../../../components/route-modal"
import { useQueryParams } from "../../../../../hooks/use-query-params"
import { queryClient } from "../../../../../lib/medusa"
type AddCustomersFormProps = {
customerGroupId: string
subscribe: (state: boolean) => void
}
const AddCustomersSchema = zod.object({
@@ -50,10 +44,9 @@ const PAGE_SIZE = 10
export const AddCustomersForm = ({
customerGroupId,
subscribe,
}: AddCustomersFormProps) => {
const navigate = useNavigate()
const { t } = useTranslation()
const { handleSuccess } = useRouteModal()
const form = useForm<zod.infer<typeof AddCustomersSchema>>({
defaultValues: {
@@ -62,13 +55,7 @@ export const AddCustomersForm = ({
resolver: zodResolver(AddCustomersSchema),
})
const {
formState: { isDirty },
} = form
useEffect(() => {
subscribe(isDirty)
}, [isDirty])
const { setValue } = form
const [{ pageIndex, pageSize }, setPagination] = useState<PaginationState>({
pageIndex: 0,
@@ -86,11 +73,15 @@ export const AddCustomersForm = ({
const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
useEffect(() => {
form.setValue(
setValue(
"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 { customers, count, isLoading, isError, error } = useAdminCustomers({
@@ -131,7 +122,7 @@ export const AddCustomersForm = ({
{
onSuccess: () => {
queryClient.invalidateQueries(adminCustomerKeys.lists())
navigate(`/customer-groups/${customerGroupId}`)
handleSuccess(`/customer-groups/${customerGroupId}`)
},
}
)
@@ -147,23 +138,23 @@ export const AddCustomersForm = ({
}
return (
<Form {...form}>
<RouteFocusModal.Form form={form}>
<form
className="flex h-full flex-col overflow-hidden"
onSubmit={handleSubmit}
>
<FocusModal.Header>
<RouteFocusModal.Header>
<div className="flex items-center justify-end gap-x-2">
{form.formState.errors.customer_ids && (
<Hint variant="error">
{form.formState.errors.customer_ids.message}
</Hint>
)}
<FocusModal.Close asChild>
<RouteFocusModal.Close asChild>
<Button variant="secondary" size="small">
{t("actions.cancel")}
</Button>
</FocusModal.Close>
</RouteFocusModal.Close>
<Button
type="submit"
variant="primary"
@@ -173,8 +164,8 @@ export const AddCustomersForm = ({
{t("general.add")}
</Button>
</div>
</FocusModal.Header>
<FocusModal.Body className="flex h-full w-full flex-col items-center divide-y overflow-y-auto">
</RouteFocusModal.Header>
<RouteFocusModal.Body className="flex h-full w-full flex-col items-center divide-y overflow-y-auto">
{noRecords ? (
<div className="flex w-full flex-1 items-center justify-center">
<NoRecords />
@@ -259,9 +250,9 @@ export const AddCustomersForm = ({
/>
</div>
)}
</FocusModal.Body>
</RouteFocusModal.Body>
</form>
</Form>
</RouteFocusModal.Form>
)
}

View File

@@ -1,18 +1,14 @@
import { FocusModal } from "@medusajs/ui"
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"
export const CustomerGroupAddCustomers = () => {
const [open, onOpenChange, subscribe] = useRouteModalState()
const { id } = useParams()
return (
<FocusModal open={open} onOpenChange={onOpenChange}>
<FocusModal.Content>
<AddCustomersForm customerGroupId={id!} subscribe={subscribe} />
</FocusModal.Content>
</FocusModal>
<RouteFocusModal>
<AddCustomersForm customerGroupId={id!} />
</RouteFocusModal>
)
}

View File

@@ -1,26 +1,23 @@
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 { useEffect } from "react"
import { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next"
import { useNavigate } from "react-router-dom"
import * as zod from "zod"
import { Form } from "../../../../../components/common/form"
type CreateCustomerGroupFormProps = {
subscribe: (state: boolean) => void
}
import { Form } from "../../../../../components/common/form"
import {
RouteFocusModal,
useRouteModal,
} from "../../../../../components/route-modal"
const CreateCustomerGroupSchema = zod.object({
name: zod.string().min(1),
})
export const CreateCustomerGroupForm = ({
subscribe,
}: CreateCustomerGroupFormProps) => {
export const CreateCustomerGroupForm = () => {
const { t } = useTranslation()
const navigate = useNavigate()
const { handleSuccess } = useRouteModal()
const form = useForm<zod.infer<typeof CreateCustomerGroupSchema>>({
defaultValues: {
@@ -29,14 +26,6 @@ export const CreateCustomerGroupForm = ({
resolver: zodResolver(CreateCustomerGroupSchema),
})
const {
formState: { isDirty },
} = form
useEffect(() => {
subscribe(isDirty)
}, [isDirty])
const { mutateAsync, isLoading } = useAdminCreateCustomerGroup()
const handleSubmit = form.handleSubmit(async (data) => {
@@ -46,22 +35,22 @@ export const CreateCustomerGroupForm = ({
},
{
onSuccess: ({ customer_group }) => {
navigate(`/customer-groups/${customer_group.id}`)
handleSuccess(`/customer-groups/${customer_group.id}`)
},
}
)
})
return (
<Form {...form}>
<RouteFocusModal.Form form={form}>
<form onSubmit={handleSubmit}>
<FocusModal.Header>
<RouteFocusModal.Header>
<div className="flex items-center justify-end gap-x-2">
<FocusModal.Close asChild>
<RouteFocusModal.Close asChild>
<Button variant="secondary" size="small">
{t("actions.cancel")}
</Button>
</FocusModal.Close>
</RouteFocusModal.Close>
<Button
type="submit"
variant="primary"
@@ -71,8 +60,8 @@ export const CreateCustomerGroupForm = ({
{t("actions.create")}
</Button>
</div>
</FocusModal.Header>
<FocusModal.Body className="flex flex-col items-center pt-[72px]">
</RouteFocusModal.Header>
<RouteFocusModal.Body className="flex flex-col items-center pt-[72px]">
<div className="flex w-full max-w-[720px] flex-col gap-y-8">
<div>
<Heading>{t("customerGroups.createCustomerGroup")}</Heading>
@@ -98,8 +87,8 @@ export const CreateCustomerGroupForm = ({
/>
</div>
</div>
</FocusModal.Body>
</RouteFocusModal.Body>
</form>
</Form>
</RouteFocusModal.Form>
)
}

View File

@@ -1,15 +1,10 @@
import { FocusModal } from "@medusajs/ui"
import { useRouteModalState } from "../../../hooks/use-route-modal-state"
import { RouteFocusModal } from "../../../components/route-modal"
import { CreateCustomerGroupForm } from "./components/create-customer-group-form"
export const CustomerGroupCreate = () => {
const [open, onOpenChange, subscribe] = useRouteModalState()
return (
<FocusModal open={open} onOpenChange={onOpenChange}>
<FocusModal.Content>
<CreateCustomerGroupForm subscribe={subscribe} />
</FocusModal.Content>
</FocusModal>
<RouteFocusModal>
<CreateCustomerGroupForm />
</RouteFocusModal>
)
}

View File

@@ -1,17 +1,18 @@
import { zodResolver } from "@hookform/resolvers/zod"
import { CustomerGroup } from "@medusajs/medusa"
import { Button, Drawer, Input } from "@medusajs/ui"
import { Button, Input } from "@medusajs/ui"
import { useAdminUpdateCustomerGroup } from "medusa-react"
import { useEffect } from "react"
import { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next"
import * as z from "zod"
import { Form } from "../../../../../components/common/form"
import {
RouteDrawer,
useRouteModal,
} from "../../../../../components/route-modal"
type EditCustomerGroupFormProps = {
group: CustomerGroup
onSuccessfulSubmit: () => void
subscribe: (state: boolean) => void
}
const EditCustomerGroupSchema = z.object({
@@ -20,10 +21,9 @@ const EditCustomerGroupSchema = z.object({
export const EditCustomerGroupForm = ({
group,
onSuccessfulSubmit,
subscribe,
}: EditCustomerGroupFormProps) => {
const { t } = useTranslation()
const { handleSuccess } = useRouteModal()
const form = useForm<z.infer<typeof EditCustomerGroupSchema>>({
defaultValues: {
@@ -32,31 +32,23 @@ export const EditCustomerGroupForm = ({
resolver: zodResolver(EditCustomerGroupSchema),
})
const {
formState: { isDirty },
} = form
useEffect(() => {
subscribe(isDirty)
}, [isDirty])
const { mutateAsync, isLoading } = useAdminUpdateCustomerGroup(group.id)
const handleSubmit = form.handleSubmit(async (data) => {
await mutateAsync(data, {
onSuccess: () => {
onSuccessfulSubmit()
handleSuccess()
},
})
})
return (
<Form {...form}>
<RouteDrawer.Form form={form}>
<form
onSubmit={handleSubmit}
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
control={form.control}
name="name"
@@ -72,20 +64,20 @@ export const EditCustomerGroupForm = ({
)
}}
/>
</Drawer.Body>
<Drawer.Footer>
</RouteDrawer.Body>
<RouteDrawer.Footer>
<div className="flex items-center justify-end gap-x-2">
<Drawer.Close asChild>
<RouteDrawer.Close asChild>
<Button size="small" variant="secondary">
{t("actions.cancel")}
</Button>
</Drawer.Close>
</RouteDrawer.Close>
<Button size="small" type="submit" isLoading={isLoading}>
{t("actions.save")}
</Button>
</div>
</Drawer.Footer>
</RouteDrawer.Footer>
</form>
</Form>
</RouteDrawer.Form>
)
}

View File

@@ -1,13 +1,11 @@
import { Drawer, Heading } from "@medusajs/ui"
import { Heading } from "@medusajs/ui"
import { useAdminCustomerGroup } from "medusa-react"
import { useTranslation } from "react-i18next"
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"
export const CustomerGroupEdit = () => {
const [open, onOpenChange, subscribe] = useRouteModalState()
const { id } = useParams()
const { customer_group, isLoading, isError, error } = useAdminCustomerGroup(
id!
@@ -15,28 +13,18 @@ export const CustomerGroupEdit = () => {
const { t } = useTranslation()
const handleSuccessfulSubmit = () => {
onOpenChange(false, true)
}
if (isError) {
throw error
}
return (
<Drawer open={open} onOpenChange={onOpenChange}>
<Drawer.Content>
<Drawer.Header>
<Heading>{t("customerGroups.editCustomerGroup")}</Heading>
</Drawer.Header>
{!isLoading && customer_group && (
<EditCustomerGroupForm
subscribe={subscribe}
onSuccessfulSubmit={handleSuccessfulSubmit}
group={customer_group}
/>
)}
</Drawer.Content>
</Drawer>
<RouteDrawer>
<RouteDrawer.Header>
<Heading>{t("customerGroups.editCustomerGroup")}</Heading>
</RouteDrawer.Header>
{!isLoading && customer_group && (
<EditCustomerGroupForm group={customer_group} />
)}
</RouteDrawer>
)
}

View File

@@ -1,16 +1,15 @@
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 { useEffect } from "react"
import { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next"
import { useNavigate } from "react-router-dom"
import * as zod from "zod"
import { Form } from "../../../../../components/common/form"
type CreateCustomerFormProps = {
subscribe: (state: boolean) => void
}
import { Form } from "../../../../../components/common/form"
import {
RouteFocusModal,
useRouteModal,
} from "../../../../../components/route-modal"
const CreateCustomerSchema = zod
.object({
@@ -31,9 +30,9 @@ const CreateCustomerSchema = zod
}
})
export const CreateCustomerForm = ({ subscribe }: CreateCustomerFormProps) => {
export const CreateCustomerForm = () => {
const { t } = useTranslation()
const navigate = useNavigate()
const { handleSuccess } = useRouteModal()
const form = useForm<zod.infer<typeof CreateCustomerSchema>>({
defaultValues: {
@@ -46,13 +45,6 @@ export const CreateCustomerForm = ({ subscribe }: CreateCustomerFormProps) => {
},
resolver: zodResolver(CreateCustomerSchema),
})
const {
formState: { isDirty },
} = form
useEffect(() => {
subscribe(isDirty)
}, [isDirty])
const { mutateAsync, isLoading } = useAdminCreateCustomer()
@@ -67,22 +59,22 @@ export const CreateCustomerForm = ({ subscribe }: CreateCustomerFormProps) => {
},
{
onSuccess: ({ customer }) => {
navigate(`/customers/${customer.id}`, { replace: true })
handleSuccess(`/customers/${customer.id}`)
},
}
)
})
return (
<Form {...form}>
<RouteFocusModal.Form form={form}>
<form onSubmit={handleSubmit}>
<FocusModal.Header>
<RouteFocusModal.Header>
<div className="flex items-center justify-end gap-x-2">
<FocusModal.Close asChild>
<RouteFocusModal.Close asChild>
<Button size="small" variant="secondary">
{t("actions.cancel")}
</Button>
</FocusModal.Close>
</RouteFocusModal.Close>
<Button
size="small"
variant="primary"
@@ -92,8 +84,8 @@ export const CreateCustomerForm = ({ subscribe }: CreateCustomerFormProps) => {
{t("actions.create")}
</Button>
</div>
</FocusModal.Header>
<FocusModal.Body className="flex flex-col items-center py-16">
</RouteFocusModal.Header>
<RouteFocusModal.Body className="flex flex-col items-center py-16">
<div className="flex w-full max-w-[720px] flex-col gap-y-8">
<div>
<Heading>{t("customers.createCustomer")}</Heading>
@@ -214,8 +206,8 @@ export const CreateCustomerForm = ({ subscribe }: CreateCustomerFormProps) => {
</div>
</div>
</div>
</FocusModal.Body>
</RouteFocusModal.Body>
</form>
</Form>
</RouteFocusModal.Form>
)
}

View File

@@ -1,15 +1,10 @@
import { FocusModal } from "@medusajs/ui"
import { useRouteModalState } from "../../../hooks/use-route-modal-state"
import { RouteFocusModal } from "../../../components/route-modal"
import { CreateCustomerForm } from "./components/create-customer-form"
export const CustomerCreate = () => {
const [open, onOpenChange, subscribe] = useRouteModalState()
return (
<FocusModal open={open} onOpenChange={onOpenChange}>
<FocusModal.Content>
<CreateCustomerForm subscribe={subscribe} />
</FocusModal.Content>
</FocusModal>
<RouteFocusModal>
<CreateCustomerForm />
</RouteFocusModal>
)
}

View File

@@ -1,17 +1,19 @@
import { zodResolver } from "@hookform/resolvers/zod"
import { Customer } from "@medusajs/medusa"
import { Button, Drawer, Input } from "@medusajs/ui"
import { Button, Input } from "@medusajs/ui"
import { useAdminUpdateCustomer } from "medusa-react"
import { useEffect } from "react"
import { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next"
import * as zod from "zod"
import { Form } from "../../../../../components/common/form"
import {
RouteDrawer,
useRouteModal,
} from "../../../../../components/route-modal"
type EditCustomerFormProps = {
customer: Customer
subscribe: (state: boolean) => void
onSuccessfulSubmit: () => void
}
const EditCustomerSchema = zod.object({
@@ -21,12 +23,9 @@ const EditCustomerSchema = zod.object({
phone: zod.string().optional(),
})
export const EditCustomerForm = ({
customer,
subscribe,
onSuccessfulSubmit,
}: EditCustomerFormProps) => {
export const EditCustomerForm = ({ customer }: EditCustomerFormProps) => {
const { t } = useTranslation()
const { handleSuccess } = useRouteModal()
const form = useForm<zod.infer<typeof EditCustomerSchema>>({
defaultValues: {
@@ -38,14 +37,6 @@ export const EditCustomerForm = ({
resolver: zodResolver(EditCustomerSchema),
})
const {
formState: { isDirty },
} = form
useEffect(() => {
subscribe(isDirty)
}, [isDirty])
const { mutateAsync, isLoading } = useAdminUpdateCustomer(customer.id)
const handleSubmit = form.handleSubmit(async (data) => {
@@ -58,16 +49,16 @@ export const EditCustomerForm = ({
},
{
onSuccess: () => {
onSuccessfulSubmit()
handleSuccess()
},
}
)
})
return (
<Form {...form}>
<RouteDrawer.Form form={form}>
<form onSubmit={handleSubmit} className="flex flex-1 flex-col">
<Drawer.Body>
<RouteDrawer.Body>
<div className="flex flex-col gap-y-4">
<Form.Field
control={form.control}
@@ -130,14 +121,14 @@ export const EditCustomerForm = ({
}}
/>
</div>
</Drawer.Body>
<Drawer.Footer>
</RouteDrawer.Body>
<RouteDrawer.Footer>
<div className="flex items-center justify-end gap-x-2">
<Drawer.Close asChild>
<RouteDrawer.Close asChild>
<Button variant="secondary" size="small">
{t("actions.cancel")}
</Button>
</Drawer.Close>
</RouteDrawer.Close>
<Button
isLoading={isLoading}
type="submit"
@@ -147,8 +138,8 @@ export const EditCustomerForm = ({
{t("actions.save")}
</Button>
</div>
</Drawer.Footer>
</RouteDrawer.Footer>
</form>
</Form>
</RouteDrawer.Form>
)
}

View File

@@ -1,39 +1,26 @@
import { Drawer, Heading } from "@medusajs/ui"
import { Heading } from "@medusajs/ui"
import { useAdminCustomer } from "medusa-react"
import { useTranslation } from "react-i18next"
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"
export const CustomerEdit = () => {
const [open, onOpenChange, subscribe] = useRouteModalState()
const { t } = useTranslation()
const { id } = useParams()
const { customer, isLoading, isError, error } = useAdminCustomer(id!)
const handleSuccessfulSubmit = () => {
onOpenChange(false, true)
}
if (isError) {
throw error
}
return (
<Drawer open={open} onOpenChange={onOpenChange}>
<Drawer.Content>
<Drawer.Header>
<Heading>{t("customers.editCustomer")}</Heading>
</Drawer.Header>
{!isLoading && customer && (
<EditCustomerForm
customer={customer}
onSuccessfulSubmit={handleSuccessfulSubmit}
subscribe={subscribe}
/>
)}
</Drawer.Content>
</Drawer>
<RouteDrawer>
<RouteDrawer.Header>
<Heading>{t("customers.editCustomer")}</Heading>
</RouteDrawer.Header>
{!isLoading && customer && <EditCustomerForm customer={customer} />}
</RouteDrawer>
)
}

View File

@@ -3,7 +3,6 @@ import {
Button,
CurrencyInput,
DatePicker,
FocusModal,
Heading,
Input,
Select,
@@ -14,20 +13,19 @@ import {
} from "@medusajs/ui"
import * as Collapsible from "@radix-ui/react-collapsible"
import { useAdminCreateGiftCard, useAdminRegions } from "medusa-react"
import { useEffect, useState } from "react"
import { useState } from "react"
import { useForm, useWatch } from "react-hook-form"
import { useTranslation } from "react-i18next"
import * as zod from "zod"
import { useNavigate } from "react-router-dom"
import { Form } from "../../../../../components/common/form"
import {
RouteFocusModal,
useRouteModal,
} from "../../../../../components/route-modal"
import { currencies } from "../../../../../lib/currencies"
import { getDbAmount } from "../../../../../lib/money-amount-helpers"
type CreateGiftCardFormProps = {
subscribe: (state: boolean) => void
}
const CreateGiftCardSchema = zod.object({
region_id: zod.string(),
value: zod.string(),
@@ -37,7 +35,7 @@ const CreateGiftCardSchema = zod.object({
personal_message: zod.string().optional(),
})
export const CreateGiftCardForm = ({ subscribe }: CreateGiftCardFormProps) => {
export const CreateGiftCardForm = () => {
const [showDateFields, setShowDateFields] = useState(false)
const { regions } = useAdminRegions({
@@ -45,7 +43,7 @@ export const CreateGiftCardForm = ({ subscribe }: CreateGiftCardFormProps) => {
fields: "id,name,currency_code",
})
const { t } = useTranslation()
const navigate = useNavigate()
const { handleSuccess } = useRouteModal()
const form = useForm<zod.infer<typeof CreateGiftCardSchema>>({
defaultValues: {
@@ -59,11 +57,7 @@ export const CreateGiftCardForm = ({ subscribe }: CreateGiftCardFormProps) => {
resolver: zodResolver(CreateGiftCardSchema),
})
const {
formState: { isDirty },
setValue,
setError,
} = form
const { setValue, setError } = form
const regionId = useWatch({
control: form.control,
@@ -75,10 +69,6 @@ export const CreateGiftCardForm = ({ subscribe }: CreateGiftCardFormProps) => {
? currencies[currencyCode.toUpperCase()].symbol_native
: undefined
useEffect(() => {
subscribe(isDirty)
}, [isDirty, subscribe])
const { mutateAsync, isLoading } = useAdminCreateGiftCard()
const handleOpenChange = (open: boolean) => {
@@ -95,7 +85,7 @@ export const CreateGiftCardForm = ({ subscribe }: CreateGiftCardFormProps) => {
if (!currencyCode) {
setError("region_id", {
type: "manual",
message: "Region not found",
message: t("giftCards.selectRegionFirst"),
})
return
@@ -114,31 +104,31 @@ export const CreateGiftCardForm = ({ subscribe }: CreateGiftCardFormProps) => {
},
{
onSuccess: ({ gift_card }) => {
navigate(`../${gift_card.id}`, { replace: true })
handleSuccess(`../${gift_card.id}`)
},
}
)
})
return (
<Form {...form}>
<RouteFocusModal.Form form={form}>
<form
className="flex h-full flex-col overflow-hidden"
onSubmit={handleSubmit}
>
<FocusModal.Header>
<RouteFocusModal.Header>
<div className="flex items-center justify-end gap-x-2">
<FocusModal.Close asChild>
<RouteFocusModal.Close asChild>
<Button size="small" variant="secondary">
{t("actions.cancel")}
</Button>
</FocusModal.Close>
</RouteFocusModal.Close>
<Button size="small" type="submit" isLoading={isLoading}>
{t("actions.save")}
</Button>
</div>
</FocusModal.Header>
<FocusModal.Body className="flex h-full w-full flex-col items-center overflow-y-auto py-16">
</RouteFocusModal.Header>
<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>
<Heading>{t("giftCards.createGiftCard")}</Heading>
@@ -304,8 +294,8 @@ export const CreateGiftCardForm = ({ subscribe }: CreateGiftCardFormProps) => {
/>
</div>
</div>
</FocusModal.Body>
</RouteFocusModal.Body>
</form>
</Form>
</RouteFocusModal.Form>
)
}

View File

@@ -1,15 +1,10 @@
import { FocusModal } from "@medusajs/ui"
import { useRouteModalState } from "../../../hooks/use-route-modal-state"
import { RouteFocusModal } from "../../../components/route-modal"
import { CreateGiftCardForm } from "./components/create-gift-card-form"
export const GiftCardCreate = () => {
const [open, onOpenChange, subscribe] = useRouteModalState()
return (
<FocusModal open={open} onOpenChange={onOpenChange}>
<FocusModal.Content>
<CreateGiftCardForm subscribe={subscribe} />
</FocusModal.Content>
</FocusModal>
<RouteFocusModal>
<CreateGiftCardForm />
</RouteFocusModal>
)
}

View File

@@ -4,19 +4,22 @@ import {
Button,
CurrencyInput,
DatePicker,
Drawer,
Select,
Switch,
Text,
} from "@medusajs/ui"
import * as Collapsible from "@radix-ui/react-collapsible"
import { useAdminRegions, useAdminUpdateGiftCard } from "medusa-react"
import { useEffect, useState } from "react"
import { useState } from "react"
import { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next"
import * as zod from "zod"
import { Form } from "../../../../../components/common/form"
import {
RouteDrawer,
useRouteModal,
} from "../../../../../components/route-modal"
import { currencies } from "../../../../../lib/currencies"
import { isAxiosError } from "../../../../../lib/is-axios-error"
import {
@@ -26,8 +29,6 @@ import {
type EditGiftCardFormProps = {
giftCard: GiftCard
onSuccessfulSubmit: () => void
subscribe: (state: boolean) => void
}
const EditGiftCardSchema = zod.object({
@@ -37,13 +38,12 @@ const EditGiftCardSchema = zod.object({
ends_at: zod.date().nullable(),
})
export const EditGiftCardForm = ({
giftCard,
onSuccessfulSubmit,
subscribe,
}: EditGiftCardFormProps) => {
export const EditGiftCardForm = ({ giftCard }: EditGiftCardFormProps) => {
const { t } = useTranslation()
const { handleSuccess } = useRouteModal()
const [showDateFields, setShowDateFields] = useState(!!giftCard.ends_at)
const form = useForm<zod.infer<typeof EditGiftCardSchema>>({
defaultValues: {
region_id: giftCard.region_id,
@@ -57,14 +57,7 @@ export const EditGiftCardForm = ({
resolver: zodResolver(EditGiftCardSchema),
})
const {
formState: { isDirty },
setValue,
} = form
useEffect(() => {
subscribe(isDirty)
}, [isDirty, subscribe])
const { setValue } = form
const { regions } = useAdminRegions({
limit: 1000,
@@ -117,7 +110,7 @@ export const EditGiftCardForm = ({
},
{
onSuccess: () => {
onSuccessfulSubmit()
handleSuccess()
},
onError: (error) => {
if (isAxiosError(error)) {
@@ -132,19 +125,19 @@ export const EditGiftCardForm = ({
})
return (
<Form {...form}>
<RouteDrawer.Form form={form}>
<form
onSubmit={handleSubmit}
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
control={form.control}
name="balance"
render={({ field: { onChange, ...field } }) => {
return (
<Form.Item>
<Form.Label>{t("fields.balance")}</Form.Label>
<Form.Label>{t("giftCards.balance")}</Form.Label>
<Form.Control>
<CurrencyInput
code={giftCard.region.currency_code.toUpperCase()}
@@ -258,20 +251,20 @@ export const EditGiftCardForm = ({
)
}}
/>
</Drawer.Body>
<Drawer.Footer>
</RouteDrawer.Body>
<RouteDrawer.Footer>
<div className="flex items-center gap-x-2">
<Drawer.Close asChild>
<RouteDrawer.Close asChild>
<Button size="small" variant="secondary">
{t("actions.cancel")}
</Button>
</Drawer.Close>
</RouteDrawer.Close>
<Button size="small" type="submit" isLoading={isLoading}>
{t("actions.save")}
</Button>
</div>
</Drawer.Footer>
</RouteDrawer.Footer>
</form>
</Form>
</RouteDrawer.Form>
)
}

View File

@@ -1,39 +1,26 @@
import { Drawer, Heading } from "@medusajs/ui"
import { Heading } from "@medusajs/ui"
import { useAdminGiftCard } from "medusa-react"
import { useTranslation } from "react-i18next"
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"
export const GiftCardEdit = () => {
const { id } = useParams()
const { t } = useTranslation()
const [open, onOpenChange, subscribe] = useRouteModalState()
const { gift_card, isLoading, isError, error } = useAdminGiftCard(id!)
const handleSuccessfulSubmit = () => {
onOpenChange(false, true)
}
if (isError) {
throw error
}
return (
<Drawer open={open} onOpenChange={onOpenChange}>
<Drawer.Content>
<Drawer.Header>
<Heading>{t("giftCards.editGiftCard")}</Heading>
</Drawer.Header>
{!isLoading && gift_card && (
<EditGiftCardForm
giftCard={gift_card}
onSuccessfulSubmit={handleSuccessfulSubmit}
subscribe={subscribe}
/>
)}
</Drawer.Content>
</Drawer>
<RouteDrawer>
<RouteDrawer.Header>
<Heading>{t("giftCards.editGiftCard")}</Heading>
</RouteDrawer.Header>
{!isLoading && gift_card && <EditGiftCardForm giftCard={gift_card} />}
</RouteDrawer>
)
}

View File

@@ -1,15 +1,8 @@
import { FocusModal } from "@medusajs/ui"
import { useAdminAddLocationToSalesChannel } from "medusa-react"
import { useRouteModalState } from "../../../hooks/use-route-modal-state"
import { RouteFocusModal } from "../../../components/route-modal"
export const LocationAddSalesChannels = () => {
const [open, onOpenChange] = useRouteModalState()
const { mutateAsync } = useAdminAddLocationToSalesChannel() // TODO: We need a batch mutation instead of this to avoid multiple requests
return (
<FocusModal open={open} onOpenChange={onOpenChange}>
<FocusModal.Content></FocusModal.Content>
</FocusModal>
)
return <RouteFocusModal></RouteFocusModal>
}

View File

@@ -1,11 +1,12 @@
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 { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next"
import * as zod from "zod"
import { CountrySelect } from "../../../../../components/common/country-select"
import { Form } from "../../../../../components/common/form"
import { RouteFocusModal } from "../../../../../components/route-modal"
const CreateLocationSchema = zod.object({
name: zod.string().min(1),
@@ -22,7 +23,7 @@ const CreateLocationSchema = zod.object({
})
export const CreateLocationForm = () => {
const { mutateAsync, isLoading } = useAdminCreateStockLocation()
const { t } = useTranslation()
const form = useForm<zod.infer<typeof CreateLocationSchema>>({
defaultValues: {
@@ -41,7 +42,7 @@ export const CreateLocationForm = () => {
resolver: zodResolver(CreateLocationSchema),
})
const { t } = useTranslation()
const { mutateAsync, isLoading } = useAdminCreateStockLocation()
const handleSubmit = form.handleSubmit(async (values) => {
mutateAsync(
@@ -56,24 +57,24 @@ export const CreateLocationForm = () => {
})
return (
<Form {...form}>
<RouteFocusModal.Form form={form}>
<form
onSubmit={handleSubmit}
className="flex h-full flex-col overflow-hidden"
>
<FocusModal.Header>
<RouteFocusModal.Header>
<div className="flex items-center justify-end gap-x-2">
<FocusModal.Close asChild>
<RouteFocusModal.Close asChild>
<Button size="small" variant="secondary">
{t("actions.cancel")}
</Button>
</FocusModal.Close>
</RouteFocusModal.Close>
<Button type="submit" size="small" isLoading={isLoading}>
{t("actions.save")}
</Button>
</div>
</FocusModal.Header>
<FocusModal.Body className="flex flex-1 flex-col overflow-hidden">
</RouteFocusModal.Header>
<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 w-full max-w-[720px] flex-col gap-y-8 px-2 py-16">
<div>
@@ -227,8 +228,8 @@ export const CreateLocationForm = () => {
</div>
</div>
</div>
</FocusModal.Body>
</RouteFocusModal.Body>
</form>
</Form>
</RouteFocusModal.Form>
)
}

View File

@@ -1,15 +1,10 @@
import { FocusModal } from "@medusajs/ui"
import { useRouteModalState } from "../../../hooks/use-route-modal-state"
import { RouteFocusModal } from "../../../components/route-modal"
import { CreateLocationForm } from "./components/create-location-form"
export const LocationCreate = () => {
const [open, onOpenChange] = useRouteModalState()
return (
<FocusModal open={open} onOpenChange={onOpenChange}>
<FocusModal.Content>
<CreateLocationForm />
</FocusModal.Content>
</FocusModal>
<RouteFocusModal>
<CreateLocationForm />
</RouteFocusModal>
)
}

View File

@@ -1,12 +1,14 @@
import { zodResolver } from "@hookform/resolvers/zod"
import { StockLocationExpandedDTO } from "@medusajs/types"
import { Button, Drawer, Input } from "@medusajs/ui"
import { Button, Input } from "@medusajs/ui"
import { useAdminUpdateStockLocation } from "medusa-react"
import { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next"
import * as zod from "zod"
import { CountrySelect } from "../../../../../components/common/country-select"
import { Form } from "../../../../../components/common/form"
import { RouteDrawer } from "../../../../../components/route-modal"
type EditLocationFormProps = {
location: StockLocationExpandedDTO
@@ -56,12 +58,12 @@ export const EditLocationForm = ({ location }: EditLocationFormProps) => {
})
return (
<Form {...form}>
<RouteDrawer.Form form={form}>
<form
onSubmit={handleSubmit}
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>
<Form.Field
control={form.control}
@@ -201,18 +203,20 @@ export const EditLocationForm = ({ location }: EditLocationFormProps) => {
}}
/>
</div>
</Drawer.Body>
<Drawer.Footer>
</RouteDrawer.Body>
<RouteDrawer.Footer>
<div className="flex items-center justify-end gap-x-2">
<Drawer.Close asChild>
<RouteDrawer.Close asChild>
<Button size="small" variant="secondary">
{t("actions.cancel")}
</Button>
</Drawer.Close>
<Button size="small">{t("actions.save")}</Button>
</RouteDrawer.Close>
<Button size="small" type="submit" isLoading={isLoading}>
{t("actions.save")}
</Button>
</div>
</Drawer.Footer>
</RouteDrawer.Footer>
</form>
</Form>
</RouteDrawer.Form>
)
}

View File

@@ -1,12 +1,11 @@
import { Drawer, Heading } from "@medusajs/ui"
import { Heading } from "@medusajs/ui"
import { useAdminStockLocations } from "medusa-react"
import { useTranslation } from "react-i18next"
import { json, useParams } from "react-router-dom"
import { useRouteModalState } from "../../../hooks/use-route-modal-state"
import { useParams } from "react-router-dom"
import { RouteDrawer } from "../../../components/route-modal"
import { EditLocationForm } from "./components/edit-location-form/edit-location-form"
export const LocationEdit = () => {
const [open, onOpenChange] = useRouteModalState()
const { id } = useParams()
const { stock_locations, isLoading, isError, error } = useAdminStockLocations(
@@ -24,24 +23,14 @@ export const LocationEdit = () => {
const stock_location = stock_locations?.[0]
if (!isLoading && !stock_location) {
throw json({ message: "Not found" }, 404)
}
return (
<Drawer open={open} onOpenChange={onOpenChange}>
<Drawer.Content>
<Drawer.Header>
<Heading className="capitalize">
{t("locations.editLocation")}
</Heading>
</Drawer.Header>
{isLoading || !stock_location ? (
<div>Loading...</div>
) : (
<EditLocationForm location={stock_location} />
)}
</Drawer.Content>
</Drawer>
<RouteDrawer>
<RouteDrawer.Header>
<Heading className="capitalize">{t("locations.editLocation")}</Heading>
</RouteDrawer.Header>
{!isLoading && stock_location && (
<EditLocationForm location={stock_location} />
)}
</RouteDrawer>
)
}

View File

@@ -1,19 +1,20 @@
import { zodResolver } from "@hookform/resolvers/zod"
import { Product } from "@medusajs/medusa"
import { Button, Drawer, Input } from "@medusajs/ui"
import { Button, Input } from "@medusajs/ui"
import { useAdminUpdateProduct } from "medusa-react"
import { useEffect } from "react"
import { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next"
import * as zod from "zod"
import { CountrySelect } from "../../../../../components/common/country-select"
import { Form } from "../../../../../components/common/form"
import {
RouteDrawer,
useRouteModal,
} from "../../../../../components/route-modal"
type ProductAttributesFormProps = {
product: Product
subscribe: (state: boolean) => void
onSuccessfulSubmit: () => void
}
const dimension = zod
@@ -39,9 +40,10 @@ const ProductAttributesSchema = zod.object({
export const ProductAttributesForm = ({
product,
subscribe,
onSuccessfulSubmit,
}: ProductAttributesFormProps) => {
const { t } = useTranslation()
const { handleSuccess } = useRouteModal()
const form = useForm<zod.infer<typeof ProductAttributesSchema>>({
defaultValues: {
height: product.height ? product.height : null,
@@ -55,15 +57,6 @@ export const ProductAttributesForm = ({
resolver: zodResolver(ProductAttributesSchema),
})
const {
formState: { isDirty },
} = form
useEffect(() => {
subscribe(isDirty)
}, [isDirty, subscribe])
const { t } = useTranslation()
const { mutateAsync, isLoading } = useAdminUpdateProduct(product.id)
const handleSubmit = form.handleSubmit(async (data) => {
@@ -79,16 +72,16 @@ export const ProductAttributesForm = ({
},
{
onSuccess: () => {
onSuccessfulSubmit()
handleSuccess()
},
}
)
})
return (
<Form {...form}>
<RouteDrawer.Form form={form}>
<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 flex-col gap-y-4">
<Form.Field
@@ -254,20 +247,20 @@ export const ProductAttributesForm = ({
/>
</div>
</div>
</Drawer.Body>
<Drawer.Footer>
</RouteDrawer.Body>
<RouteDrawer.Footer>
<div className="flex items-center justify-end gap-x-2">
<Drawer.Close asChild>
<RouteDrawer.Close asChild>
<Button size="small" variant="secondary">
{t("actions.cancel")}
</Button>
</Drawer.Close>
</RouteDrawer.Close>
<Button size="small" type="submit" isLoading={isLoading}>
{t("actions.save")}
</Button>
</div>
</Drawer.Footer>
</RouteDrawer.Footer>
</form>
</Form>
</RouteDrawer.Form>
)
}

View File

@@ -1,41 +1,27 @@
import { Drawer, Heading } from "@medusajs/ui"
import { Heading } from "@medusajs/ui"
import { useAdminProduct } from "medusa-react"
import { useTranslation } from "react-i18next"
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"
export const ProductAttributes = () => {
const { id } = useParams()
const [open, onOpenChange, subscribe] = useRouteModalState()
const { t } = useTranslation()
const { product, isLoading, isError, error } = useAdminProduct(id!)
const handleSuccessfulSubmit = () => {
onOpenChange(false, true)
}
if (isError) {
throw error
}
return (
<Drawer open={open} onOpenChange={onOpenChange}>
<Drawer.Content>
<Drawer.Header>
<Heading>{t("products.editAttributes")}</Heading>
</Drawer.Header>
{!isLoading && product && (
<ProductAttributesForm
product={product}
onSuccessfulSubmit={handleSuccessfulSubmit}
subscribe={subscribe}
/>
)}
</Drawer.Content>
</Drawer>
<RouteDrawer>
<RouteDrawer.Header>
<Heading>{t("products.editAttributes")}</Heading>
</RouteDrawer.Header>
{!isLoading && product && <ProductAttributesForm product={product} />}
</RouteDrawer>
)
}

View File

@@ -1,18 +1,16 @@
import { zodResolver } from "@hookform/resolvers/zod"
import { Button, FocusModal } from "@medusajs/ui"
import { Button } from "@medusajs/ui"
import { useAdminCreateProduct } from "medusa-react"
import { useEffect } from "react"
import { UseFormReturn, useForm } from "react-hook-form"
import { useTranslation } from "react-i18next"
import * as zod from "zod"
import { Form } from "../../../../../components/common/form"
import {
RouteFocusModal,
useRouteModal,
} from "../../../../../components/route-modal"
import { CreateProductDetails } from "./create-product-details"
type CreateProductFormProps = {
subscribe: (state: boolean) => void
}
const CreateProductSchema = zod.object({
title: zod.string(),
subtitle: zod.string().optional(),
@@ -38,8 +36,10 @@ const CreateProductSchema = zod.object({
type Schema = zod.infer<typeof CreateProductSchema>
export type CreateProductFormReturn = UseFormReturn<Schema>
export const CreateProductForm = ({ subscribe }: CreateProductFormProps) => {
export const CreateProductForm = () => {
const { t } = useTranslation()
const { handleSuccess } = useRouteModal()
const form = useForm<Schema>({
defaultValues: {
title: "",
@@ -61,14 +61,6 @@ export const CreateProductForm = ({ subscribe }: CreateProductFormProps) => {
resolver: zodResolver(CreateProductSchema),
})
const {
formState: { isDirty },
} = form
useEffect(() => {
subscribe(isDirty)
}, [subscribe, isDirty])
const { mutateAsync, isLoading } = useAdminCreateProduct()
const handleSubmit = form.handleSubmit(async (values) => {
@@ -83,34 +75,36 @@ export const CreateProductForm = ({ subscribe }: CreateProductFormProps) => {
weight: values.weight ? parseFloat(values.weight) : undefined,
},
{
onSuccess: () => {},
onSuccess: ({ product }) => {
handleSuccess(`../${product.id}`)
},
}
)
})
return (
<Form {...form}>
<RouteFocusModal.Form form={form}>
<form onSubmit={handleSubmit} className="flex h-full flex-col">
<FocusModal.Header>
<RouteFocusModal.Header>
<div className="flex items-center justify-end gap-x-2">
<FocusModal.Close asChild>
<RouteFocusModal.Close asChild>
<Button variant="secondary" size="small">
{t("actions.cancel")}
</Button>
</FocusModal.Close>
</RouteFocusModal.Close>
<Button type="submit" size="small" isLoading={isLoading}>
{t("actions.save")}
</Button>
</div>
</FocusModal.Header>
<FocusModal.Body className="flex flex-1 flex-col overflow-hidden">
</RouteFocusModal.Header>
<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 h-full w-full">
<CreateProductDetails form={form} />
</div>
</div>
</FocusModal.Body>
</RouteFocusModal.Body>
</form>
</Form>
</RouteFocusModal.Form>
)
}

View File

@@ -1,15 +1,10 @@
import { FocusModal } from "@medusajs/ui"
import { useRouteModalState } from "../../../hooks/use-route-modal-state"
import { RouteFocusModal } from "../../../components/route-modal"
import { CreateProductForm } from "./components/create-product-form"
export const ProductCreate = () => {
const [open, onOpenChange, subscribe] = useRouteModalState()
return (
<FocusModal open={open} onOpenChange={onOpenChange}>
<FocusModal.Content>
<CreateProductForm subscribe={subscribe} />
</FocusModal.Content>
</FocusModal>
<RouteFocusModal>
<CreateProductForm />
</RouteFocusModal>
)
}

View File

@@ -1,27 +1,20 @@
import { zodResolver } from "@hookform/resolvers/zod"
import { Product } from "@medusajs/medusa"
import { ProductStatus } from "@medusajs/types"
import {
Button,
Drawer,
Input,
Select,
Switch,
Text,
Textarea,
} from "@medusajs/ui"
import { Button, Input, Select, Switch, Text, Textarea } from "@medusajs/ui"
import { useAdminUpdateProduct } from "medusa-react"
import { useEffect } from "react"
import { useForm } from "react-hook-form"
import { Trans, useTranslation } from "react-i18next"
import * as zod from "zod"
import { Form } from "../../../../../components/common/form"
import {
RouteDrawer,
useRouteModal,
} from "../../../../../components/route-modal"
type EditProductFormProps = {
product: Product
subscribe: (state: boolean) => void
onSuccessfulSubmit: () => void
}
const EditProductSchema = zod.object({
@@ -34,11 +27,10 @@ const EditProductSchema = zod.object({
discountable: zod.boolean(),
})
export const EditProductForm = ({
product,
subscribe,
onSuccessfulSubmit,
}: EditProductFormProps) => {
export const EditProductForm = ({ product }: EditProductFormProps) => {
const { t } = useTranslation()
const { handleSuccess } = useRouteModal()
const form = useForm<zod.infer<typeof EditProductSchema>>({
defaultValues: {
status: product.status,
@@ -52,15 +44,6 @@ export const EditProductForm = ({
resolver: zodResolver(EditProductSchema),
})
const {
formState: { isDirty },
} = form
useEffect(() => {
subscribe(isDirty)
}, [isDirty, subscribe])
const { t } = useTranslation()
const { mutateAsync, isLoading } = useAdminUpdateProduct(product.id)
const handleSubmit = form.handleSubmit(async (data) => {
@@ -71,16 +54,16 @@ export const EditProductForm = ({
},
{
onSuccess: () => {
onSuccessfulSubmit()
handleSuccess()
},
}
)
})
return (
<Form {...form}>
<RouteDrawer.Form form={form}>
<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 flex-col gap-y-4">
<Form.Field
@@ -247,8 +230,7 @@ export const EditProductForm = ({
</Form.Control>
</div>
<Form.Hint className="!mt-1">
When unchecked discounts will not be applied to this
product.
{t("products.discountableHint")}
</Form.Hint>
<Form.ErrorMessage />
</Form.Item>
@@ -256,20 +238,20 @@ export const EditProductForm = ({
}}
/>
</div>
</Drawer.Body>
<Drawer.Footer>
</RouteDrawer.Body>
<RouteDrawer.Footer>
<div className="flex items-center justify-end gap-x-2">
<Drawer.Close asChild>
<RouteDrawer.Close asChild>
<Button size="small" variant="secondary">
{t("actions.cancel")}
</Button>
</Drawer.Close>
</RouteDrawer.Close>
<Button size="small" type="submit" isLoading={isLoading}>
{t("actions.save")}
</Button>
</div>
</Drawer.Footer>
</RouteDrawer.Footer>
</form>
</Form>
</RouteDrawer.Form>
)
}

View File

@@ -1,41 +1,27 @@
import { Drawer, Heading } from "@medusajs/ui"
import { Heading } from "@medusajs/ui"
import { useAdminProduct } from "medusa-react"
import { useTranslation } from "react-i18next"
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"
export const ProductEdit = () => {
const { id } = useParams()
const [open, onOpenChange, subscribe] = useRouteModalState()
const { t } = useTranslation()
const { product, isLoading, isError, error } = useAdminProduct(id!)
const handleSuccessfulSubmit = () => {
onOpenChange(false, true)
}
if (isError) {
throw error
}
return (
<Drawer open={open} onOpenChange={onOpenChange}>
<Drawer.Content>
<Drawer.Header>
<Heading>{t("products.editProduct")}</Heading>
</Drawer.Header>
{!isLoading && product && (
<EditProductForm
product={product}
onSuccessfulSubmit={handleSuccessfulSubmit}
subscribe={subscribe}
/>
)}
</Drawer.Content>
</Drawer>
<RouteDrawer>
<RouteDrawer.Header>
<Heading>{t("products.editProduct")}</Heading>
</RouteDrawer.Header>
{!isLoading && product && <EditProductForm product={product} />}
</RouteDrawer>
)
}

View File

@@ -11,14 +11,16 @@ import { Button, IconButton, Kbd, Tooltip } from "@medusajs/ui"
import * as Dialog from "@radix-ui/react-dialog"
import { Variants, motion } from "framer-motion"
import { useAdminProduct } from "medusa-react"
import { useCallback, useEffect, useMemo } from "react"
import { useCallback, useEffect, useMemo, useState } from "react"
import { useTranslation } from "react-i18next"
import { Link, useParams, useSearchParams } from "react-router-dom"
import { useRouteModalState } from "../../../hooks/use-route-modal-state"
export const ProductGallery = () => {
const [open, onOpenChange] = useRouteModalState()
const [open, setOpen] = useState(false)
useEffect(() => {
setOpen(true)
}, [])
const { id } = useParams()
const [searchParams, setSearchParams] = useSearchParams()
@@ -78,7 +80,7 @@ export const ProductGallery = () => {
}
return (
<Dialog.Root modal open={open} onOpenChange={onOpenChange}>
<Dialog.Root modal open={open} onOpenChange={setOpen}>
<Dialog.Portal>
<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">

View File

@@ -1,15 +1,13 @@
import { zodResolver } from "@hookform/resolvers/zod"
import { Product } from "@medusajs/medusa"
import { Button, Drawer } from "@medusajs/ui"
import { Button } from "@medusajs/ui"
import { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next"
import * as zod from "zod"
import { Form } from "../../../../../components/common/form"
import { RouteDrawer } from "../../../../../components/route-modal"
type EditProductOptionsFormProps = {
product: Product
handleSuccess: () => void
subscribe: (state: boolean) => void
}
const EditProductOptionsSchema = zod.object({})
@@ -22,22 +20,22 @@ export const EditProductOptionsForm = (props: EditProductOptionsFormProps) => {
})
return (
<Form {...form}>
<RouteDrawer.Form form={form}>
<form className="flex flex-1 flex-col overflow-hidden">
<Drawer.Body className="flex flex-1 flex-col gap-y-8 overflow-auto"></Drawer.Body>
<Drawer.Footer>
<RouteDrawer.Body className="flex flex-1 flex-col gap-y-8 overflow-auto"></RouteDrawer.Body>
<RouteDrawer.Footer>
<div className="flex items-center justify-end gap-x-2">
<Drawer.Close asChild>
<RouteDrawer.Close asChild>
<Button variant="secondary" size="small">
{t("actions.cancel")}
</Button>
</Drawer.Close>
</RouteDrawer.Close>
<Button type="submit" size="small">
{t("actions.save")}
</Button>
</div>
</Drawer.Footer>
</RouteDrawer.Footer>
</form>
</Form>
</RouteDrawer.Form>
)
}

View File

@@ -1,39 +1,26 @@
import { Drawer, Heading } from "@medusajs/ui"
import { Heading } from "@medusajs/ui"
import { useAdminProduct } from "medusa-react"
import { useTranslation } from "react-i18next"
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"
export const ProductOptions = () => {
const { id } = useParams()
const { t } = useTranslation()
const [open, onOpenChange, subscribe] = useRouteModalState()
const { product, isLoading, isError, error } = useAdminProduct(id!)
const handleSuccess = () => {
onOpenChange(false, true)
}
if (isError) {
throw error
}
return (
<Drawer open={open} onOpenChange={onOpenChange}>
<Drawer.Content>
<Drawer.Header>
<Heading>{t("products.editOptions")}</Heading>
</Drawer.Header>
{!isLoading && product && (
<EditProductOptionsForm
product={product}
handleSuccess={handleSuccess}
subscribe={subscribe}
/>
)}
</Drawer.Content>
</Drawer>
<RouteDrawer>
<RouteDrawer.Header>
<Heading>{t("products.editOptions")}</Heading>
</RouteDrawer.Header>
{!isLoading && product && <EditProductOptionsForm product={product} />}
</RouteDrawer>
)
}

View File

@@ -1,9 +1,17 @@
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 { useAdminSalesChannels, useAdminUpdateProduct } from "medusa-react"
import { useEffect, useMemo, useState } from "react"
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 { useSalesChannelTableColumns } from "../../../../../hooks/table/columns/use-sales-channel-table-columns"
import { useSalesChannelTableFilters } from "../../../../../hooks/table/filters/use-sales-channel-table-filters"
@@ -12,18 +20,28 @@ import { useDataTable } from "../../../../../hooks/use-data-table"
type EditSalesChannelsFormProps = {
product: Product
subscribe: (state: boolean) => void
onSuccessfulSubmit: () => void
}
const EditSalesChannelsSchema = zod.object({
sales_channels: zod.array(zod.string()).optional(),
})
const PAGE_SIZE = 50
export const EditSalesChannelsForm = ({
product,
subscribe,
onSuccessfulSubmit,
}: EditSalesChannelsFormProps) => {
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 =
product.sales_channels?.reduce((acc, curr) => {
@@ -34,13 +52,13 @@ export const EditSalesChannelsForm = ({
const [rowSelection, setRowSelection] =
useState<RowSelectionState>(initialState)
const isDirty = Object.entries(initialState).some(
([key, value]) => value !== rowSelection[key]
)
useEffect(() => {
subscribe(isDirty)
}, [isDirty, subscribe])
const ids = Object.keys(rowSelection)
setValue("sales_channels", ids, {
shouldDirty: true,
shouldTouch: true,
})
}, [rowSelection, setValue])
const { searchParams, raw } = useSalesChannelTableQuery({
pageSize: PAGE_SIZE,
@@ -76,12 +94,10 @@ export const EditSalesChannelsForm = ({
product.id
)
const handleSubmit = async () => {
const selected = Object.keys(rowSelection).filter((key) => {
return rowSelection[key]
})
const handleSubmit = form.handleSubmit(async (data) => {
const arr = data.sales_channels ?? []
const sales_channels = selected.map((id) => {
const sales_channels = arr.map((id) => {
return {
id,
}
@@ -93,46 +109,48 @@ export const EditSalesChannelsForm = ({
},
{
onSuccess: () => {
onSuccessfulSubmit()
handleSuccess()
},
}
)
}
})
if (isError) {
throw error
}
return (
<div className="flex h-full flex-col overflow-hidden">
<FocusModal.Header>
<div className="flex items-center justify-end gap-x-2">
<FocusModal.Close asChild>
<Button size="small" variant="secondary">
{t("actions.cancel")}
<RouteFocusModal.Form form={form}>
<div className="flex h-full flex-col overflow-hidden">
<RouteFocusModal.Header>
<div className="flex items-center justify-end gap-x-2">
<RouteFocusModal.Close asChild>
<Button size="small" variant="secondary">
{t("actions.cancel")}
</Button>
</RouteFocusModal.Close>
<Button size="small" isLoading={isMutating} onClick={handleSubmit}>
{t("actions.save")}
</Button>
</FocusModal.Close>
<Button size="small" isLoading={isMutating} onClick={handleSubmit}>
{t("actions.save")}
</Button>
</div>
</FocusModal.Header>
<FocusModal.Body>
<DataTable
table={table}
columns={columns}
pageSize={PAGE_SIZE}
isLoading={isLoading}
count={count}
filters={filters}
search
pagination
orderBy={["name", "created_at", "updated_at"]}
queryObject={raw}
layout="fill"
/>
</FocusModal.Body>
</div>
</div>
</RouteFocusModal.Header>
<RouteFocusModal.Body>
<DataTable
table={table}
columns={columns}
pageSize={PAGE_SIZE}
isLoading={isLoading}
count={count}
filters={filters}
search
pagination
orderBy={["name", "created_at", "updated_at"]}
queryObject={raw}
layout="fill"
/>
</RouteFocusModal.Body>
</div>
</RouteFocusModal.Form>
)
}

View File

@@ -1,35 +1,20 @@
import { FocusModal } from "@medusajs/ui"
import { useAdminProduct } from "medusa-react"
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"
export const ProductSalesChannels = () => {
const { id } = useParams()
const [open, onOpenChange, subscribe] = useRouteModalState()
const { product, isLoading, isError, error } = useAdminProduct(id!)
const handleSuccessfulSubmit = () => {
onOpenChange(false, true)
}
if (isError) {
throw error
}
return (
<FocusModal open={open} onOpenChange={onOpenChange}>
<FocusModal.Content>
{!isLoading && product && (
<EditSalesChannelsForm
product={product}
onSuccessfulSubmit={handleSuccessfulSubmit}
subscribe={subscribe}
/>
)}
</FocusModal.Content>
</FocusModal>
<RouteFocusModal>
{!isLoading && product && <EditSalesChannelsForm product={product} />}
</RouteFocusModal>
)
}

View File

@@ -1,19 +1,22 @@
import { User } from "@medusajs/medusa"
import * as zod from "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 { useForm } from "react-hook-form"
import { Trans, useTranslation } from "react-i18next"
import * as zod from "zod"
import { Form } from "../../../../../components/common/form"
import {
RouteDrawer,
useRouteModal,
} from "../../../../../components/route-modal"
import { languages } from "../../../../../i18n/config"
import { queryClient } from "../../../../../lib/medusa"
type EditProfileProps = {
user: Omit<User, "password_hash">
usageInsights: boolean
onSuccess: () => void
}
const EditProfileSchema = zod.object({
@@ -23,13 +26,9 @@ const EditProfileSchema = zod.object({
usage_insights: zod.boolean(),
})
export const EditProfileForm = ({
user,
usageInsights,
onSuccess,
}: EditProfileProps) => {
export const EditProfileForm = ({ user, usageInsights }: EditProfileProps) => {
const { t, i18n } = useTranslation()
const { mutateAsync, isLoading } = useAdminUpdateUser(user.id)
const { handleSuccess } = useRouteModal()
const form = useForm<zod.infer<typeof EditProfileSchema>>({
defaultValues: {
@@ -49,6 +48,8 @@ export const EditProfileForm = ({
a.display_name.localeCompare(b.display_name)
)
const { mutateAsync, isLoading } = useAdminUpdateUser(user.id)
const handleSubmit = form.handleSubmit(async (values) => {
await mutateAsync(
{
@@ -56,12 +57,7 @@ export const EditProfileForm = ({
last_name: values.last_name,
},
{
onSuccess: ({ user }) => {
form.reset({
first_name: user.first_name,
last_name: user.last_name,
})
onSuccess: () => {
// Invalidate the current user session.
queryClient.invalidateQueries(adminAuthKeys.details())
},
@@ -73,13 +69,13 @@ export const EditProfileForm = ({
changeLanguage(values.language)
onSuccess()
handleSuccess()
})
return (
<Form {...form}>
<RouteDrawer.Form form={form}>
<form onSubmit={handleSubmit} className="flex flex-1 flex-col">
<Drawer.Body>
<RouteDrawer.Body>
<div className="flex flex-col gap-y-8">
<div className="grid grid-cols-2 gap-4">
<Form.Field
@@ -122,9 +118,6 @@ export const EditProfileForm = ({
<Form.Control>
<Select
{...field}
// open={selectOpen}
// onOpenChange={setSelectOpen}
value={field.value}
onValueChange={field.onChange}
size="small"
>
@@ -175,6 +168,7 @@ export const EditProfileForm = ({
i18nKey="profile.userInsightsHint"
components={[
<a
key="hint-link"
className="text-ui-fg-interactive hover:text-ui-fg-interactive-hover transition-fg underline"
href="https://docs.medusajs.com/usage#admin-analytics"
target="_blank"
@@ -189,20 +183,20 @@ export const EditProfileForm = ({
)}
/>
</div>
</Drawer.Body>
<Drawer.Footer>
</RouteDrawer.Body>
<RouteDrawer.Footer>
<div className="flex items-center gap-x-2">
<Drawer.Close asChild>
<RouteDrawer.Close asChild>
<Button size="small" variant="secondary">
{t("actions.cancel")}
</Button>
</Drawer.Close>
</RouteDrawer.Close>
<Button size="small" type="submit" isLoading={isLoading}>
{t("actions.save")}
</Button>
</div>
</Drawer.Footer>
</RouteDrawer.Footer>
</form>
</Form>
</RouteDrawer.Form>
)
}

View File

@@ -1,52 +1,26 @@
import { Drawer, Heading } from "@medusajs/ui"
import { Heading } from "@medusajs/ui"
import { useAdminGetSession } from "medusa-react"
import { useEffect, useState } from "react"
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"
export const ProfileEdit = () => {
const [open, setOpen] = useState(false)
const navigate = useNavigate()
const { user, isLoading, isError, error } = useAdminGetSession()
const { t } = useTranslation()
useEffect(() => {
setOpen(true)
}, [])
const onOpenChange = (open: boolean) => {
if (!open) {
setTimeout(() => {
navigate(`/settings/profile`, { replace: true })
}, 200)
}
setOpen(open)
}
if (isError) {
throw error
}
return (
<Drawer open={open} onOpenChange={onOpenChange}>
<Drawer.Content>
<Drawer.Header className="capitalize">
<Heading>{t("profile.editProfile")}</Heading>
</Drawer.Header>
{isLoading || !user ? (
<div>Loading...</div>
) : (
<EditProfileForm
user={user}
usageInsights={false}
onSuccess={() => onOpenChange(false)}
/>
)}
</Drawer.Content>
</Drawer>
<RouteDrawer>
<RouteDrawer.Header className="capitalize">
<Heading>{t("profile.editProfile")}</Heading>
</RouteDrawer.Header>
{!isLoading && user && (
<EditProfileForm user={user} usageInsights={false} />
)}
</RouteDrawer>
)
}

View File

@@ -1,16 +1,12 @@
import { zodResolver } from "@hookform/resolvers/zod"
import { Button, FocusModal, Heading, Input, Switch, Text } from "@medusajs/ui"
import { useAdminCreateRegion } from "medusa-react"
import { useEffect } from "react"
import { Button, Heading, Input, Select, Switch, Text } from "@medusajs/ui"
import { useAdminCreateRegion, useAdminStore } from "medusa-react"
import { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next"
import { useNavigate } from "react-router-dom"
import * as zod from "zod"
import { Form } from "../../../../../components/common/form"
type CreateRegionFormProps = {
subscribe: (state: boolean) => void
}
import { RouteFocusModal } from "../../../../../components/route-modal/route-focus-modal"
const CreateRegionSchema = zod.object({
name: zod.string().min(1),
@@ -23,7 +19,7 @@ const CreateRegionSchema = zod.object({
tax_code: zod.string().optional(),
})
export const CreateRegionForm = ({ subscribe }: CreateRegionFormProps) => {
export const CreateRegionForm = () => {
const form = useForm<zod.infer<typeof CreateRegionSchema>>({
defaultValues: {
name: "",
@@ -33,23 +29,19 @@ export const CreateRegionForm = ({ subscribe }: CreateRegionFormProps) => {
fulfillment_providers: [],
payment_providers: [],
tax_code: "",
tax_rate: 0,
},
resolver: zodResolver(CreateRegionSchema),
})
const {
formState: { isDirty },
} = form
useEffect(() => {
subscribe(isDirty)
}, [isDirty, subscribe])
const { t } = useTranslation()
const navigate = useNavigate()
const { mutateAsync, isLoading } = useAdminCreateRegion()
const { store } = useAdminStore()
const storeCurrencies = store?.currencies ?? []
const handleSubmit = form.handleSubmit(async (values) => {
await mutateAsync(
{
@@ -71,24 +63,24 @@ export const CreateRegionForm = ({ subscribe }: CreateRegionFormProps) => {
})
return (
<Form {...form}>
<RouteFocusModal.Form form={form}>
<form
className="flex h-full flex-col overflow-hidden"
onSubmit={handleSubmit}
>
<FocusModal.Header>
<RouteFocusModal.Header>
<div className="flex items-center justify-end gap-x-2">
<FocusModal.Close asChild>
<RouteFocusModal.Close asChild>
<Button size="small" variant="secondary">
{t("actions.cancel")}
</Button>
</FocusModal.Close>
</RouteFocusModal.Close>
<Button size="small" type="submit" isLoading={isLoading}>
{t("actions.save")}
</Button>
</div>
</FocusModal.Header>
<FocusModal.Body className="flex h-full w-full flex-col items-center overflow-y-auto py-16">
</RouteFocusModal.Header>
<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>
<Heading>{t("regions.createRegion")}</Heading>
@@ -116,11 +108,27 @@ export const CreateRegionForm = ({ subscribe }: CreateRegionFormProps) => {
<Form.Field
control={form.control}
name="currency_code"
render={({ field }) => {
render={({ field: { onChange, ref, ...field } }) => {
return (
<Form.Item>
<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.Item>
)
@@ -198,8 +206,8 @@ export const CreateRegionForm = ({ subscribe }: CreateRegionFormProps) => {
<div className="grid grid-cols-2 gap-4"></div>
</div>
</div>
</FocusModal.Body>
</RouteFocusModal.Body>
</form>
</Form>
</RouteFocusModal.Form>
)
}

View File

@@ -1,16 +1,10 @@
import { FocusModal } from "@medusajs/ui"
import { useRouteModalState } from "../../../hooks/use-route-modal-state"
import { RouteFocusModal } from "../../../components/route-modal/route-focus-modal"
import { CreateRegionForm } from "./components/create-region-form"
export const RegionCreate = () => {
const [open, onOpenChange, subscribe] = useRouteModalState()
return (
<FocusModal open={open} onOpenChange={onOpenChange}>
<FocusModal.Content>
<CreateRegionForm subscribe={subscribe} />
</FocusModal.Content>
</FocusModal>
<RouteFocusModal>
<CreateRegionForm />
</RouteFocusModal>
)
}

View File

@@ -0,0 +1 @@
export * from "./region-shipping-option-section"

View File

@@ -1,60 +1,48 @@
import { Region, ShippingOption } from "@medusajs/medusa"
import { Container, Heading, StatusBadge, Table, clx } from "@medusajs/ui"
import {
PaginationState,
RowSelectionState,
createColumnHelper,
flexRender,
getCoreRowModel,
useReactTable,
} from "@tanstack/react-table"
import { Region } from "@medusajs/medusa"
import { Container, Heading } from "@medusajs/ui"
import { useAdminShippingOptions } from "medusa-react"
import { useMemo, useState } from "react"
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 = {
region: Region
}
const PAGE_SIZE = 10
export const RegionShippingOptionSection = ({
region,
}: RegionShippingOptionSectionProps) => {
const { shipping_options, count, isError, error, isLoading } =
useAdminShippingOptions({
region_id: region.id,
is_return: false,
})
const [{ pageIndex, pageSize }, setPagination] = useState<PaginationState>({
pageIndex: 0,
pageSize: count || 0,
const { searchParams, raw } = useShippingOptionTableQuery({
pageSize: PAGE_SIZE,
regionId: region.id,
})
const { shipping_options, count, isError, error, isLoading } =
useAdminShippingOptions(
{
...searchParams,
},
{
keepPreviousData: true,
}
)
const pagination = useMemo(
() => ({
pageIndex,
pageSize,
}),
[pageIndex, pageSize]
)
const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
const filters = useShippingOptionTableFilters()
const columns = useShippingOptionColumns()
const table = useReactTable({
data: shipping_options ?? [],
const { table } = useDataTable({
data: (shipping_options ?? []) as unknown as PricedShippingOption[],
columns,
pageCount: count ? 1 : 0,
state: {
pagination,
rowSelection,
},
manualPagination: true,
getCoreRowModel: getCoreRowModel(),
onPaginationChange: setPagination,
onRowSelectionChange: setRowSelection,
count,
enablePagination: true,
getRowId: (row) => row.id!,
pageSize: PAGE_SIZE,
})
const { t } = useTranslation()
@@ -64,91 +52,30 @@ export const RegionShippingOptionSection = ({
}
return (
<Container className="p-0 divide-y">
<Container className="divide-y p-0">
<div className="px-6 py-4">
<Heading level="h2">{t("regions.shippingOptions")}</Heading>
</div>
<Table>
<Table.Header>
{table.getHeaderGroups().map((headerGroup) => {
return (
<Table.Row
key={headerGroup.id}
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"
>
{headerGroup.headers.map((header) => {
return (
<Table.HeaderCell key={header.id}>
{flexRender(
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 [&_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}
<DataTable
table={table}
columns={columns}
count={count}
filters={filters}
orderBy={[
"name",
"price_type",
"price_incl_tax",
"is_return",
"admin_only",
"created_at",
"updated_at",
]}
isLoading={isLoading}
rowCount={PAGE_SIZE}
pagination
search
queryObject={raw}
/>
</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]
)
}

View File

@@ -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]
)
}

View File

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

View File

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

View File

@@ -1,64 +1,43 @@
import { PencilSquare, Trash } from "@medusajs/icons"
import { Region } from "@medusajs/medusa"
import {
Button,
Container,
Heading,
Table,
Tooltip,
clx,
usePrompt,
} from "@medusajs/ui"
import {
PaginationState,
createColumnHelper,
flexRender,
getCoreRowModel,
useReactTable,
} from "@tanstack/react-table"
import { Button, Container, Heading, usePrompt } from "@medusajs/ui"
import { createColumnHelper } from "@tanstack/react-table"
import { useAdminDeleteRegion, useAdminRegions } from "medusa-react"
import { useMemo, useState } from "react"
import { useMemo } from "react"
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 { 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 = () => {
const navigate = useNavigate()
const { t } = useTranslation()
const [{ pageIndex, pageSize }, setPagination] = useState<PaginationState>({
pageIndex: 0,
pageSize: PAGE_SIZE,
})
const pagination = useMemo(
() => ({
pageIndex,
pageSize,
}),
[pageIndex, pageSize]
const { searchParams, raw } = useRegionTableQuery({ pageSize: PAGE_SIZE })
const { regions, count, isLoading, isError, error } = useAdminRegions(
{
...searchParams,
},
{
keepPreviousData: true,
}
)
const { regions, count, isLoading, isError, error } = useAdminRegions({
limit: PAGE_SIZE,
offset: pageIndex * PAGE_SIZE,
})
const filters = useRegionTableFilters()
const columns = useColumns()
const table = useReactTable({
const { table } = useDataTable({
data: regions ?? [],
columns,
pageCount: Math.ceil((count ?? 0) / PAGE_SIZE),
state: {
pagination,
},
onPaginationChange: setPagination,
getCoreRowModel: getCoreRowModel(),
manualPagination: true,
count,
enablePagination: true,
getRowId: (row) => row.id,
pageSize: PAGE_SIZE,
})
if (isError) {
@@ -66,7 +45,7 @@ export const RegionListTable = () => {
}
return (
<Container className="p-0">
<Container className="divide-y p-0">
<div className="flex items-center justify-between px-6 py-4">
<Heading level="h2">{t("regions.domain")}</Heading>
<Link to="/settings/regions/create">
@@ -75,59 +54,18 @@ export const RegionListTable = () => {
</Button>
</Link>
</div>
<Table>
<Table.Header>
{table.getHeaderGroups().map((headerGroup) => {
return (
<Table.Row
key={headerGroup.id}
className=" [&_th:last-of-type]:w-[1%] [&_th:last-of-type]:whitespace-nowrap"
>
{headerGroup.headers.map((header) => {
return (
<Table.HeaderCell key={header.id}>
{flexRender(
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}
<DataTable
table={table}
columns={columns}
count={count}
rowCount={PAGE_SIZE}
isLoading={isLoading}
filters={filters}
orderBy={["name", "created_at", "updated_at"]}
navigateTo={(row) => `${row.original.id}`}
pagination
search
queryObject={raw}
/>
</Container>
)
@@ -187,124 +125,11 @@ const RegionActions = ({ region }: { region: Region }) => {
const columnHelper = createColumnHelper<Region>()
const useColumns = () => {
const { t } = useTranslation()
const base = useRegionTableColumns()
return useMemo(
() => [
columnHelper.accessor("name", {
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>
)
},
}),
...base,
columnHelper.display({
id: "actions",
cell: ({ row }) => {
@@ -312,6 +137,6 @@ const useColumns = () => {
},
}),
],
[t]
[base]
)
}

View File

@@ -1,14 +1,6 @@
import { zodResolver } from "@hookform/resolvers/zod"
import { Product, SalesChannel } from "@medusajs/medusa"
import {
Button,
Checkbox,
FocusModal,
Hint,
Table,
Tooltip,
clx,
} from "@medusajs/ui"
import { Button, Checkbox, Hint, Table, Tooltip, clx } from "@medusajs/ui"
import {
PaginationState,
RowSelectionState,
@@ -26,7 +18,6 @@ import { useEffect, useMemo, useState } from "react"
import { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next"
import * as zod from "zod"
import { Form } from "../../../../components/common/form"
import {
ProductAvailabilityCell,
ProductCollectionCell,
@@ -37,13 +28,15 @@ import {
import { OrderBy } from "../../../../components/filtering/order-by"
import { Query } from "../../../../components/filtering/query"
import { LocalizedTablePagination } from "../../../../components/localization/localized-table-pagination"
import {
RouteFocusModal,
useRouteModal,
} from "../../../../components/route-modal"
import { useQueryParams } from "../../../../hooks/use-query-params"
import { queryClient } from "../../../../lib/medusa"
type AddProductsToSalesChannelFormProps = {
salesChannel: SalesChannel
subscribe: (state: boolean) => void
onSuccess: () => void
}
const AddProductsToSalesChannelSchema = zod.object({
@@ -54,10 +47,9 @@ const PAGE_SIZE = 50
export const AddProductsToSalesChannelForm = ({
salesChannel,
subscribe,
onSuccess,
}: AddProductsToSalesChannelFormProps) => {
const { t } = useTranslation()
const { handleSuccess } = useRouteModal()
const form = useForm<zod.infer<typeof AddProductsToSalesChannelSchema>>({
defaultValues: {
@@ -66,13 +58,7 @@ export const AddProductsToSalesChannelForm = ({
resolver: zodResolver(AddProductsToSalesChannelSchema),
})
const {
formState: { isDirty },
} = form
useEffect(() => {
subscribe(isDirty)
}, [isDirty])
const { setValue } = form
const { mutateAsync, isLoading: isMutating } =
useAdminAddProductsToSalesChannel(salesChannel.id)
@@ -93,15 +79,19 @@ export const AddProductsToSalesChannelForm = ({
const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
useEffect(() => {
form.setValue(
setValue(
"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 { products, count, isLoading } = useAdminProducts(
const { products, count } = useAdminProducts(
{
expand: "variants,sales_channels",
...params,
@@ -148,36 +138,36 @@ export const AddProductsToSalesChannelForm = ({
* determine if they are added to the sales channel or not.
*/
queryClient.invalidateQueries(adminProductKeys.lists())
onSuccess()
handleSuccess()
},
}
)
})
return (
<Form {...form}>
<RouteFocusModal.Form form={form}>
<form
onSubmit={handleSubmit}
className="flex h-full flex-col overflow-hidden"
>
<FocusModal.Header>
<RouteFocusModal.Header>
<div className="flex items-center justify-end gap-x-2">
{form.formState.errors.product_ids && (
<Hint variant="error">
{form.formState.errors.product_ids.message}
</Hint>
)}
<FocusModal.Close asChild>
<RouteFocusModal.Close asChild>
<Button size="small" variant="secondary">
{t("actions.cancel")}
</Button>
</FocusModal.Close>
</RouteFocusModal.Close>
<Button size="small" type="submit" isLoading={isMutating}>
{t("actions.save")}
</Button>
</div>
</FocusModal.Header>
<FocusModal.Body className="flex h-full w-full flex-col items-center divide-y overflow-y-auto">
</RouteFocusModal.Header>
<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></div>
<div className="flex items-center gap-x-2">
@@ -251,9 +241,9 @@ export const AddProductsToSalesChannelForm = ({
pageSize={PAGE_SIZE}
/>
</div>
</FocusModal.Body>
</RouteFocusModal.Body>
</form>
</Form>
</RouteFocusModal.Form>
)
}

View File

@@ -1,36 +1,21 @@
import { FocusModal } from "@medusajs/ui"
import { useAdminSalesChannel } from "medusa-react"
import { useParams } from "react-router-dom"
import { useRouteModalState } from "../../../hooks/use-route-modal-state"
import { RouteFocusModal } from "../../../components/route-modal"
import { AddProductsToSalesChannelForm } from "./components"
export const SalesChannelAddProducts = () => {
const { id } = useParams()
const [open, onOpenChange, subscribe] = useRouteModalState()
const { sales_channel, isLoading, isError, error } = useAdminSalesChannel(id!)
const handleSuccess = () => {
onOpenChange(false, true)
}
if (isError) {
throw error
}
return (
<FocusModal open={open} onOpenChange={onOpenChange}>
<FocusModal.Content>
{isLoading || !sales_channel ? (
<div>Loading...</div>
) : (
<AddProductsToSalesChannelForm
salesChannel={sales_channel}
onSuccess={handleSuccess}
subscribe={subscribe}
/>
)}
</FocusModal.Content>
</FocusModal>
<RouteFocusModal>
{!isLoading && sales_channel && (
<AddProductsToSalesChannelForm salesChannel={sales_channel} />
)}
</RouteFocusModal>
)
}

View File

@@ -1,25 +1,15 @@
import { zodResolver } from "@hookform/resolvers/zod"
import {
Button,
FocusModal,
Heading,
Input,
Switch,
Text,
Textarea,
} from "@medusajs/ui"
import { Button, Heading, Input, Switch, Text, Textarea } from "@medusajs/ui"
import { useAdminCreateSalesChannel } from "medusa-react"
import { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next"
import { useNavigate } from "react-router-dom"
import * as zod from "zod"
import { useEffect } from "react"
import { Form } from "../../../../../components/common/form"
type CreateSalesChannelFormProps = {
subscribe: (state: boolean) => void
}
import {
RouteFocusModal,
useRouteModal,
} from "../../../../../components/route-modal"
const CreateSalesChannelSchema = zod.object({
name: zod.string().min(1),
@@ -27,9 +17,10 @@ const CreateSalesChannelSchema = zod.object({
enabled: zod.boolean(),
})
export const CreateSalesChannelForm = ({
subscribe,
}: CreateSalesChannelFormProps) => {
export const CreateSalesChannelForm = () => {
const { t } = useTranslation()
const { handleSuccess } = useRouteModal()
const form = useForm<zod.infer<typeof CreateSalesChannelSchema>>({
defaultValues: {
name: "",
@@ -40,17 +31,6 @@ export const CreateSalesChannelForm = ({
})
const { mutateAsync, isLoading } = useAdminCreateSalesChannel()
const {
formState: { isDirty },
} = form
useEffect(() => {
subscribe(isDirty)
}, [isDirty])
const { t } = useTranslation()
const navigate = useNavigate()
const handleSubmit = form.handleSubmit(async (values) => {
await mutateAsync(
{
@@ -60,31 +40,31 @@ export const CreateSalesChannelForm = ({
},
{
onSuccess: ({ sales_channel }) => {
navigate(`../${sales_channel.id}`)
handleSuccess(`../${sales_channel.id}`)
},
}
)
})
return (
<Form {...form}>
<RouteFocusModal.Form form={form}>
<form
onSubmit={handleSubmit}
className="flex h-full flex-col overflow-hidden"
>
<FocusModal.Header>
<RouteFocusModal.Header>
<div className="flex items-center justify-end gap-x-2">
<FocusModal.Close asChild>
<RouteFocusModal.Close asChild>
<Button size="small" variant="secondary">
{t("actions.cancel")}
</Button>
</FocusModal.Close>
</RouteFocusModal.Close>
<Button size="small" type="submit" isLoading={isLoading}>
{t("actions.save")}
</Button>
</div>
</FocusModal.Header>
<FocusModal.Body className="flex flex-1 flex-col overflow-hidden">
</RouteFocusModal.Header>
<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 w-full max-w-[720px] flex-col gap-y-8 px-2 py-16">
<div>
@@ -153,8 +133,8 @@ export const CreateSalesChannelForm = ({
/>
</div>
</div>
</FocusModal.Body>
</RouteFocusModal.Body>
</form>
</Form>
</RouteFocusModal.Form>
)
}

View File

@@ -1,15 +1,10 @@
import { FocusModal } from "@medusajs/ui"
import { useRouteModalState } from "../../../hooks/use-route-modal-state"
import { RouteFocusModal } from "../../../components/route-modal"
import { CreateSalesChannelForm } from "./components/create-sales-channel-form"
export const SalesChannelCreate = () => {
const [open, onOpenChange, subscribe] = useRouteModalState()
return (
<FocusModal open={open} onOpenChange={onOpenChange}>
<FocusModal.Content>
<CreateSalesChannelForm subscribe={subscribe} />
</FocusModal.Content>
</FocusModal>
<RouteFocusModal>
<CreateSalesChannelForm />
</RouteFocusModal>
)
}

View File

@@ -1,18 +1,19 @@
import { zodResolver } from "@hookform/resolvers/zod"
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 { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next"
import * as zod from "zod"
import { useEffect } from "react"
import { Form } from "../../../../../components/common/form"
import {
RouteDrawer,
useRouteModal,
} from "../../../../../components/route-modal"
type EditSalesChannelFormProps = {
salesChannel: SalesChannel
subscribe: (state: boolean) => void
onSuccess: () => void
}
const EditSalesChannelSchema = zod.object({
@@ -23,9 +24,10 @@ const EditSalesChannelSchema = zod.object({
export const EditSalesChannelForm = ({
salesChannel,
subscribe,
onSuccess,
}: EditSalesChannelFormProps) => {
const { t } = useTranslation()
const { handleSuccess } = useRouteModal()
const form = useForm<zod.infer<typeof EditSalesChannelSchema>>({
defaultValues: {
name: salesChannel.name,
@@ -35,16 +37,6 @@ export const EditSalesChannelForm = ({
resolver: zodResolver(EditSalesChannelSchema),
})
const {
formState: { isDirty },
} = form
useEffect(() => {
subscribe(isDirty)
}, [isDirty])
const { t } = useTranslation()
const { mutateAsync, isLoading } = useAdminUpdateSalesChannel(salesChannel.id)
const handleSubmit = form.handleSubmit(async (values) => {
@@ -56,19 +48,19 @@ export const EditSalesChannelForm = ({
},
{
onSuccess: () => {
onSuccess()
handleSuccess()
},
}
)
})
return (
<Form {...form}>
<RouteDrawer.Form form={form}>
<form
onSubmit={handleSubmit}
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
control={form.control}
name="name"
@@ -121,20 +113,20 @@ export const EditSalesChannelForm = ({
)
}}
/>
</Drawer.Body>
<Drawer.Footer>
</RouteDrawer.Body>
<RouteDrawer.Footer>
<div className="flex items-center justify-end gap-x-2">
<Drawer.Close asChild>
<RouteDrawer.Close asChild>
<Button size="small" variant="secondary">
{t("actions.cancel")}
</Button>
</Drawer.Close>
</RouteDrawer.Close>
<Button size="small" type="submit" isLoading={isLoading}>
{t("actions.save")}
</Button>
</div>
</Drawer.Footer>
</RouteDrawer.Footer>
</form>
</Form>
</RouteDrawer.Form>
)
}

View File

@@ -1,41 +1,31 @@
import { Drawer, Heading } from "@medusajs/ui"
import { Heading } from "@medusajs/ui"
import { useAdminSalesChannel } from "medusa-react"
import { useTranslation } from "react-i18next"
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"
export const SalesChannelEdit = () => {
const { id } = useParams()
const { t } = useTranslation()
const [open, onOpenChange, subscribe] = useRouteModalState()
const { sales_channel, isLoading, isError, error } = useAdminSalesChannel(id!)
const onSuccess = () => {
onOpenChange(false, true)
}
const { sales_channel, isLoading, isError, error } = useAdminSalesChannel(id!)
if (isError) {
throw error
}
return (
<Drawer open={open} onOpenChange={onOpenChange}>
<Drawer.Content>
<Drawer.Header>
<Heading className="capitalize">
{t("salesChannels.editSalesChannel")}
</Heading>
</Drawer.Header>
{!isLoading && sales_channel && (
<EditSalesChannelForm
salesChannel={sales_channel}
subscribe={subscribe}
onSuccess={onSuccess}
/>
)}
</Drawer.Content>
</Drawer>
<RouteDrawer>
<RouteDrawer.Header>
<Heading className="capitalize">
{t("salesChannels.editSalesChannel")}
</Heading>
</RouteDrawer.Header>
{!isLoading && sales_channel && (
<EditSalesChannelForm salesChannel={sales_channel} />
)}
</RouteDrawer>
)
}

View File

@@ -3,7 +3,6 @@ import {
Badge,
Button,
Checkbox,
FocusModal,
Hint,
StatusBadge,
Table,
@@ -19,11 +18,18 @@ import {
useReactTable,
} from "@tanstack/react-table"
import { useAdminCurrencies, useAdminUpdateStore } from "medusa-react"
import { FormEvent, useMemo, useState } from "react"
import { useEffect, useMemo, useState } from "react"
import { useTranslation } from "react-i18next"
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 { LocalizedTablePagination } from "../../../../../components/localization/localized-table-pagination"
import {
RouteFocusModal,
useRouteModal,
} from "../../../../../components/route-modal"
import { useHandleTableScroll } from "../../../../../hooks/use-handle-table-scroll"
import { useQueryParams } from "../../../../../hooks/use-query-params"
@@ -38,9 +44,18 @@ const AddCurrenciesSchema = zod.object({
const PAGE_SIZE = 50
export const AddCurrenciesForm = ({ store }: AddCurrenciesFormProps) => {
const [errorMessage, setErrorMessage] = useState<{
currencies?: string | undefined
}>({})
const { t } = useTranslation()
const { handleSuccess } = useRouteModal()
const form = useForm<zod.infer<typeof AddCurrenciesSchema>>({
defaultValues: {
currencies: [],
},
resolver: zodResolver(AddCurrenciesSchema),
})
const { setValue } = form
const [{ pageIndex, pageSize }, setPagination] = useState<PaginationState>({
pageIndex: 0,
pageSize: PAGE_SIZE,
@@ -56,8 +71,16 @@ export const AddCurrenciesForm = ({ store }: AddCurrenciesFormProps) => {
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 { currencies, count, isLoading, isError, error } = useAdminCurrencies({
const { currencies, count, isError, error } = useAdminCurrencies({
limit: PAGE_SIZE,
offset: pageIndex * PAGE_SIZE,
...params,
@@ -83,152 +106,143 @@ export const AddCurrenciesForm = ({ store }: AddCurrenciesFormProps) => {
manualPagination: true,
})
const { t } = useTranslation()
const { mutateAsync, isLoading: isMutating } = useAdminUpdateStore()
const { handleScroll, isScrolled, tableContainerRef } = useHandleTableScroll()
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
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 handleSubmit = form.handleSubmit(async (data) => {
const currencies = Array.from(
new Set([...ids, ...preSelectedRows])
new Set([...data.currencies, ...preSelectedRows])
) as string[]
await mutateAsync({
currencies,
})
}
await mutateAsync(
{
currencies,
},
{
onSuccess: () => {
handleSuccess()
},
}
)
})
if (isError) {
throw error
}
return (
<form
onSubmit={handleSubmit}
className="flex h-full flex-col overflow-hidden"
>
<FocusModal.Header>
<div className="flex flex-1 items-center justify-between">
<div>
{errorMessage.currencies && (
<Hint variant="error">{errorMessage.currencies}</Hint>
)}
</div>
<div className="flex items-center justify-end gap-x-2">
<FocusModal.Close asChild>
<Button size="small" variant="secondary">
{t("actions.cancel")}
</Button>
</FocusModal.Close>
<Button size="small" type="submit" isLoading={isMutating}>
{t("actions.save")}
</Button>
</div>
</div>
</FocusModal.Header>
<FocusModal.Body className="flex flex-1 flex-col overflow-hidden">
<div className="flex items-center justify-between border-b px-6 py-4">
<div></div>
<div className="flex items-center gap-x-2">
<OrderBy keys={["code"]} />
</div>
</div>
<div
className="flex-1 overflow-y-auto"
ref={tableContainerRef}
onScroll={handleScroll}
>
<Table className="relative">
<Table.Header
className={clx(
"bg-ui-bg-base transition-fg sticky inset-x-0 top-0 z-10 border-t-0",
{
"shadow-elevation-card-hover": isScrolled,
}
<RouteFocusModal.Form form={form}>
<form
onSubmit={handleSubmit}
className="flex h-full flex-col overflow-hidden"
>
<RouteFocusModal.Header>
<div className="flex flex-1 items-center justify-between">
<div className="flex items-center">
{form.formState.errors.currencies && (
<Hint variant="error">
{form.formState.errors.currencies.message}
</Hint>
)}
>
{table.getHeaderGroups().map((headerGroup) => {
return (
</div>
<div className="flex items-center justify-end gap-x-2">
<RouteFocusModal.Close asChild>
<Button size="small" variant="secondary">
{t("actions.cancel")}
</Button>
</RouteFocusModal.Close>
<Button size="small" type="submit" isLoading={isMutating}>
{t("actions.save")}
</Button>
</div>
</div>
</RouteFocusModal.Header>
<RouteFocusModal.Body className="flex flex-1 flex-col overflow-hidden">
<div className="flex items-center justify-between border-b px-6 py-4">
<div></div>
<div className="flex items-center gap-x-2">
<OrderBy keys={["code"]} />
</div>
</div>
<div
className="flex-1 overflow-y-auto"
ref={tableContainerRef}
onScroll={handleScroll}
>
<Table className="relative">
<Table.Header
className={clx(
"bg-ui-bg-base transition-fg sticky inset-x-0 top-0 z-10 border-t-0",
{
"shadow-elevation-card-hover": isScrolled,
}
)}
>
{table.getHeaderGroups().map((headerGroup) => {
return (
<Table.Row
key={headerGroup.id}
className="[&_th:first-of-type]:w-[1%] [&_th:first-of-type]:whitespace-nowrap [&_th]:w-1/3"
>
{headerGroup.headers.map((header) => {
return (
<Table.HeaderCell key={header.id}>
{flexRender(
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={headerGroup.id}
className="[&_th:first-of-type]:w-[1%] [&_th:first-of-type]:whitespace-nowrap [&_th]:w-1/3"
key={row.id}
className={clx(
"transition-fg last-of-type:border-b-0",
{
"bg-ui-bg-highlight hover:bg-ui-bg-highlight-hover":
row.getIsSelected(),
},
{
"bg-ui-bg-disabled hover:bg-ui-bg-disabled":
!row.getCanSelect(),
}
)}
>
{headerGroup.headers.map((header) => {
return (
<Table.HeaderCell key={header.id}>
{flexRender(
header.column.columnDef.header,
header.getContext()
)}
</Table.HeaderCell>
)
})}
{row.getVisibleCells().map((cell) => (
<Table.Cell key={cell.id}>
{flexRender(
cell.column.columnDef.cell,
cell.getContext()
)}
</Table.Cell>
))}
</Table.Row>
)
})}
</Table.Header>
<Table.Body className="border-b-0">
{table.getRowModel().rows.map((row) => (
<Table.Row
key={row.id}
className={clx(
"transition-fg last-of-type:border-b-0",
{
"bg-ui-bg-highlight hover:bg-ui-bg-highlight-hover":
row.getIsSelected(),
},
{
"bg-ui-bg-disabled hover:bg-ui-bg-disabled":
!row.getCanSelect(),
}
)}
>
{row.getVisibleCells().map((cell) => (
<Table.Cell key={cell.id}>
{flexRender(
cell.column.columnDef.cell,
cell.getContext()
)}
</Table.Cell>
))}
</Table.Row>
))}
</Table.Body>
</Table>
</div>
<div className="w-full border-t">
<LocalizedTablePagination
canNextPage={table.getCanNextPage()}
canPreviousPage={table.getCanPreviousPage()}
nextPage={table.nextPage}
previousPage={table.previousPage}
count={count ?? 0}
pageIndex={pageIndex}
pageCount={table.getPageCount()}
pageSize={PAGE_SIZE}
/>
</div>
</FocusModal.Body>
</form>
))}
</Table.Body>
</Table>
</div>
<div className="w-full border-t">
<LocalizedTablePagination
canNextPage={table.getCanNextPage()}
canPreviousPage={table.getCanPreviousPage()}
nextPage={table.nextPage}
previousPage={table.previousPage}
count={count ?? 0}
pageIndex={pageIndex}
pageCount={table.getPageCount()}
pageSize={PAGE_SIZE}
/>
</div>
</RouteFocusModal.Body>
</form>
</RouteFocusModal.Form>
)
}

View File

@@ -1,42 +1,17 @@
import { FocusModal } from "@medusajs/ui"
import { useAdminStore } from "medusa-react"
import { useEffect, useState } from "react"
import { useNavigate } from "react-router-dom"
import { RouteFocusModal } from "../../../components/route-modal"
import { AddCurrenciesForm } from "./components/add-currencies-form/add-currencies-form"
export const StoreAddCurrencies = () => {
const [open, setOpen] = useState(false)
const navigate = useNavigate()
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) {
throw error
}
return (
<FocusModal open={open} onOpenChange={onOpenChange}>
<FocusModal.Content>
{isLoading || !store ? (
<div>Loading...</div>
) : (
<AddCurrenciesForm store={store} />
)}
</FocusModal.Content>
</FocusModal>
<RouteFocusModal>
{!isLoading && store && <AddCurrenciesForm store={store} />}
</RouteFocusModal>
)
}

View File

@@ -1,15 +1,18 @@
import { zodResolver } from "@hookform/resolvers/zod"
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 { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next"
import * as zod from "zod"
import { Form } from "../../../../../components/common/form"
import {
RouteDrawer,
useRouteModal,
} from "../../../../../components/route-modal"
type EditStoreFormProps = {
store: Store
onSuccess: () => void
}
const EditStoreSchema = zod.object({
@@ -22,8 +25,9 @@ const EditStoreSchema = zod.object({
invite_link_template: zod.union([zod.literal(""), zod.string().trim().url()]),
})
export const EditStoreForm = ({ store, onSuccess }: EditStoreFormProps) => {
const { mutateAsync, isLoading } = useAdminUpdateStore()
export const EditStoreForm = ({ store }: EditStoreFormProps) => {
const { t } = useTranslation()
const { handleSuccess } = useRouteModal()
const form = useForm<zod.infer<typeof EditStoreSchema>>({
defaultValues: {
@@ -35,7 +39,7 @@ export const EditStoreForm = ({ store, onSuccess }: EditStoreFormProps) => {
resolver: zodResolver(EditStoreSchema),
})
const { t } = useTranslation()
const { mutateAsync, isLoading } = useAdminUpdateStore()
const handleSubmit = form.handleSubmit(async (values) => {
mutateAsync(
@@ -47,16 +51,16 @@ export const EditStoreForm = ({ store, onSuccess }: EditStoreFormProps) => {
},
{
onSuccess: () => {
onSuccess()
handleSuccess()
},
}
)
})
return (
<Form {...form}>
<RouteDrawer.Form form={form}>
<form onSubmit={handleSubmit} className="flex h-full flex-col">
<Drawer.Body>
<RouteDrawer.Body>
<div className="flex flex-col gap-y-8">
<Form.Field
control={form.control}
@@ -123,20 +127,20 @@ export const EditStoreForm = ({ store, onSuccess }: EditStoreFormProps) => {
)}
/>
</div>
</Drawer.Body>
<Drawer.Footer>
</RouteDrawer.Body>
<RouteDrawer.Footer>
<div className="flex items-center justify-end gap-x-2">
<Drawer.Close asChild>
<RouteDrawer.Close asChild>
<Button size="small" variant="secondary">
{t("actions.cancel")}
</Button>
</Drawer.Close>
</RouteDrawer.Close>
<Button size="small" isLoading={isLoading} type="submit">
{t("actions.save")}
</Button>
</div>
</Drawer.Footer>
</RouteDrawer.Footer>
</form>
</Form>
</RouteDrawer.Form>
)
}

View File

@@ -1,30 +1,13 @@
import { Drawer, Heading } from "@medusajs/ui"
import { Heading } from "@medusajs/ui"
import { useAdminStore } from "medusa-react"
import { useEffect, useState } from "react"
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"
export const StoreEdit = () => {
const [open, setOpen] = useState(false)
const { store, isLoading, isError, error } = useAdminStore()
const navigate = useNavigate()
const { t } = useTranslation()
useEffect(() => {
setOpen(true)
}, [])
const onOpenChange = (open: boolean) => {
if (!open) {
setTimeout(() => {
navigate(`/settings/store`, { replace: true })
}, 200)
}
setOpen(open)
}
const { store, isLoading, isError, error } = useAdminStore()
if (isError) {
throw error
@@ -35,15 +18,11 @@ export const StoreEdit = () => {
}
return (
<Drawer open={open} onOpenChange={onOpenChange}>
<Drawer.Content>
<Drawer.Header>
<Heading className="capitalize">{t("store.editStore")}</Heading>
</Drawer.Header>
{store && (
<EditStoreForm store={store} onSuccess={() => onOpenChange(false)} />
)}
</Drawer.Content>
</Drawer>
<RouteDrawer>
<RouteDrawer.Header>
<Heading className="capitalize">{t("store.editStore")}</Heading>
</RouteDrawer.Header>
{store && <EditStoreForm store={store} />}
</RouteDrawer>
)
}

View File

@@ -1,17 +1,19 @@
import { zodResolver } from "@hookform/resolvers/zod"
import { User } from "@medusajs/medusa"
import { Button, Drawer, Input } from "@medusajs/ui"
import { Button, Input } from "@medusajs/ui"
import { useAdminUpdateUser } from "medusa-react"
import { useEffect } from "react"
import { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next"
import * as zod from "zod"
import { Form } from "../../../../../components/common/form"
import {
RouteDrawer,
useRouteModal,
} from "../../../../../components/route-modal"
type EditUserFormProps = {
user: Omit<User, "password_hash">
subscribe: (state: boolean) => void
onSuccessfulSubmit: () => void
}
const EditUserFormSchema = zod.object({
@@ -19,11 +21,10 @@ const EditUserFormSchema = zod.object({
last_name: zod.string().optional(),
})
export const EditUserForm = ({
user,
subscribe,
onSuccessfulSubmit,
}: EditUserFormProps) => {
export const EditUserForm = ({ user }: EditUserFormProps) => {
const { t } = useTranslation()
const { handleSuccess } = useRouteModal()
const form = useForm<zod.infer<typeof EditUserFormSchema>>({
defaultValues: {
first_name: user.first_name || "",
@@ -32,33 +33,23 @@ export const EditUserForm = ({
resolver: zodResolver(EditUserFormSchema),
})
const {
formState: { isDirty },
} = form
useEffect(() => {
subscribe(isDirty)
}, [isDirty])
const { t } = useTranslation()
const { mutateAsync, isLoading } = useAdminUpdateUser(user.id)
const handleSubmit = form.handleSubmit(async (values) => {
await mutateAsync(values, {
onSuccess: () => {
onSuccessfulSubmit()
handleSuccess()
},
})
})
return (
<Form {...form}>
<RouteDrawer.Form form={form}>
<form
onSubmit={handleSubmit}
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
control={form.control}
name="first_name"
@@ -89,20 +80,20 @@ export const EditUserForm = ({
)
}}
/>
</Drawer.Body>
<Drawer.Footer>
</RouteDrawer.Body>
<RouteDrawer.Footer>
<div className="flex items-center justify-end gap-x-2">
<Drawer.Close asChild>
<RouteDrawer.Close asChild>
<Button size="small" variant="secondary">
{t("actions.cancel")}
</Button>
</Drawer.Close>
</RouteDrawer.Close>
<Button size="small" type="submit" isLoading={isLoading}>
{t("actions.save")}
</Button>
</div>
</Drawer.Footer>
</RouteDrawer.Footer>
</form>
</Form>
</RouteDrawer.Form>
)
}

View File

@@ -1,39 +1,25 @@
import { Drawer, Heading } from "@medusajs/ui"
import { Heading } from "@medusajs/ui"
import { useAdminUser } from "medusa-react"
import { useTranslation } from "react-i18next"
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"
export const UserEdit = () => {
const [open, onOpenChange, subscribe] = useRouteModalState()
const { t } = useTranslation()
const { id } = useParams()
const { user, isLoading, isError, error } = useAdminUser(id!)
const handleSuccessfulSubmit = () => {
onOpenChange(false, true)
}
if (isError) {
throw error
}
return (
<Drawer open={open} onOpenChange={onOpenChange}>
<Drawer.Content>
<Drawer.Header>
<Heading>{t("users.editUser")}</Heading>
</Drawer.Header>
{!isLoading && user && (
<EditUserForm
user={user}
subscribe={subscribe}
onSuccessfulSubmit={handleSuccessfulSubmit}
/>
)}
</Drawer.Content>
</Drawer>
<RouteDrawer>
<RouteDrawer.Header>
<Heading>{t("users.editUser")}</Heading>
</RouteDrawer.Header>
{!isLoading && user && <EditUserForm user={user} />}
</RouteDrawer>
)
}

View File

@@ -4,7 +4,6 @@ import { Invite } from "@medusajs/medusa"
import {
Button,
Container,
FocusModal,
Heading,
Input,
Select,
@@ -30,7 +29,7 @@ import {
useAdminResendInvite,
useAdminStore,
} from "medusa-react"
import { useEffect, useMemo } from "react"
import { useMemo } from "react"
import { useForm } from "react-hook-form"
import { Trans, useTranslation } from "react-i18next"
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 { Form } from "../../../../../components/common/form"
import { LocalizedTablePagination } from "../../../../../components/localization/localized-table-pagination"
type InviteUserFormProps = {
subscribe: (state: boolean) => void
}
import { RouteFocusModal } from "../../../../../components/route-modal"
enum UserRole {
MEMBER = "member",
@@ -56,7 +52,9 @@ const InviteUserSchema = zod.object({
const PAGE_SIZE = 10
export const InviteUserForm = ({ subscribe }: InviteUserFormProps) => {
export const InviteUserForm = () => {
const { t } = useTranslation()
const form = useForm<zod.infer<typeof InviteUserSchema>>({
defaultValues: {
user: "",
@@ -64,15 +62,6 @@ export const InviteUserForm = ({ subscribe }: InviteUserFormProps) => {
},
resolver: zodResolver(InviteUserSchema),
})
const { mutateAsync, isLoading: isMutating } = useAdminCreateInvite()
const {
formState: { isDirty },
} = form
useEffect(() => {
subscribe(isDirty)
}, [isDirty, subscribe])
const { invites, isLoading, isError, error } = useAdminInvites()
const count = invites?.length ?? 0
@@ -89,7 +78,7 @@ export const InviteUserForm = ({ subscribe }: InviteUserFormProps) => {
getPaginationRowModel: getPaginationRowModel(),
})
const { t } = useTranslation()
const { mutateAsync, isLoading: isMutating } = useAdminCreateInvite()
const handleSubmit = form.handleSubmit(async (values) => {
await mutateAsync(
@@ -110,13 +99,13 @@ export const InviteUserForm = ({ subscribe }: InviteUserFormProps) => {
}
return (
<Form {...form}>
<RouteFocusModal.Form form={form}>
<form
onSubmit={handleSubmit}
className="flex h-full flex-col overflow-hidden"
>
<FocusModal.Header />
<FocusModal.Body className="flex flex-1 flex-col overflow-hidden">
<RouteFocusModal.Header />
<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 w-full max-w-[720px] flex-col gap-y-8 px-2 py-16">
<div>
@@ -249,9 +238,9 @@ export const InviteUserForm = ({ subscribe }: InviteUserFormProps) => {
</div>
</div>
</div>
</FocusModal.Body>
</RouteFocusModal.Body>
</form>
</Form>
</RouteFocusModal.Form>
)
}

View File

@@ -1,15 +1,10 @@
import { FocusModal } from "@medusajs/ui"
import { useRouteModalState } from "../../../hooks/use-route-modal-state"
import { RouteFocusModal } from "../../../components/route-modal"
import { InviteUserForm } from "./components/invite-user-form/invite-user-form"
export const UserInvite = () => {
const [open, onOpenChange, subscribe] = useRouteModalState()
return (
<FocusModal open={open} onOpenChange={onOpenChange}>
<FocusModal.Content>
<InviteUserForm subscribe={subscribe} />
</FocusModal.Content>
</FocusModal>
<RouteFocusModal>
<InviteUserForm />
</RouteFocusModal>
)
}