feat(dashboard,types,js-sdk,ui): Add missing Price List features (#7856)
**What** - Adds missing features to Price List domain - Adds `StackedFocusModal` and `StackedDrawer` components that should replace SplitView across the project. - Add Footer to FocusModal - Adds missing js-sdk functions and types **Note** The DatePickers in the PriceLists forms do not work as intended atm. The component is broken, and needs to be fixed. I am working on a fix, but choose to move that work into a separate branch, to prevent this PR from getting bigger then it already is. Will update once the fixes have been merged.
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
export { RouteDrawer } from "./route-drawer"
|
||||
export { RouteFocusModal } from "./route-focus-modal"
|
||||
export { useRouteModal } from "./route-modal-provider"
|
||||
|
||||
export { StackedDrawer } from "./stacked-drawer"
|
||||
export { StackedFocusModal } from "./stacked-focus-modal"
|
||||
export { useStackedModal } from "./stacked-modal-provider"
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./route-drawer"
|
||||
@@ -0,0 +1,78 @@
|
||||
import { Drawer, clx } from "@medusajs/ui"
|
||||
import { PropsWithChildren, useEffect, useState } from "react"
|
||||
import { useNavigate } from "react-router-dom"
|
||||
import { RouteModalForm } from "../route-modal-form"
|
||||
import { RouteModalProvider } from "../route-modal-provider/route-provider"
|
||||
import { StackedModalProvider } from "../stacked-modal-provider"
|
||||
|
||||
type RouteDrawerProps = PropsWithChildren<{
|
||||
prev?: string
|
||||
}>
|
||||
|
||||
const Root = ({ prev = "..", children }: RouteDrawerProps) => {
|
||||
const navigate = useNavigate()
|
||||
const [open, setOpen] = useState(false)
|
||||
const [stackedModalOpen, onStackedModalOpen] = useState(false)
|
||||
|
||||
/**
|
||||
* Open the modal when the component mounts. This
|
||||
* ensures that the entry animation is played.
|
||||
*/
|
||||
useEffect(() => {
|
||||
setOpen(true)
|
||||
|
||||
return () => {
|
||||
setOpen(false)
|
||||
onStackedModalOpen(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
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}>
|
||||
<StackedModalProvider onOpenChange={onStackedModalOpen}>
|
||||
<Drawer.Content
|
||||
className={clx({
|
||||
"!bg-ui-bg-disabled !inset-y-5 !right-5": stackedModalOpen,
|
||||
})}
|
||||
>
|
||||
{children}
|
||||
</Drawer.Content>
|
||||
</StackedModalProvider>
|
||||
</RouteModalProvider>
|
||||
</Drawer>
|
||||
)
|
||||
}
|
||||
|
||||
const Header = Drawer.Header
|
||||
const Title = Drawer.Title
|
||||
const Description = Drawer.Description
|
||||
const Body = Drawer.Body
|
||||
const Footer = Drawer.Footer
|
||||
const Close = Drawer.Close
|
||||
const Form = RouteModalForm
|
||||
|
||||
/**
|
||||
* 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,
|
||||
Title,
|
||||
Body,
|
||||
Description,
|
||||
Footer,
|
||||
Close,
|
||||
Form,
|
||||
})
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./route-focus-modal"
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
import { FocusModal, clx } from "@medusajs/ui"
|
||||
import { PropsWithChildren, useEffect, useState } from "react"
|
||||
import { useNavigate } from "react-router-dom"
|
||||
import { RouteModalForm } from "../route-modal-form"
|
||||
import { RouteModalProvider } from "../route-modal-provider/route-provider"
|
||||
import { StackedModalProvider } from "../stacked-modal-provider"
|
||||
|
||||
type RouteFocusModalProps = PropsWithChildren<{
|
||||
prev?: string
|
||||
}>
|
||||
|
||||
const Root = ({ prev = "..", children }: RouteFocusModalProps) => {
|
||||
const navigate = useNavigate()
|
||||
const [open, setOpen] = useState(false)
|
||||
const [stackedModalOpen, onStackedModalOpen] = useState(false)
|
||||
|
||||
/**
|
||||
* Open the modal when the component mounts. This
|
||||
* ensures that the entry animation is played.
|
||||
*/
|
||||
useEffect(() => {
|
||||
setOpen(true)
|
||||
|
||||
return () => {
|
||||
setOpen(false)
|
||||
onStackedModalOpen(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
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}>
|
||||
<StackedModalProvider onOpenChange={onStackedModalOpen}>
|
||||
<FocusModal.Content
|
||||
className={clx({
|
||||
"!bg-ui-bg-disabled !inset-x-5 !inset-y-3": stackedModalOpen,
|
||||
})}
|
||||
>
|
||||
{children}
|
||||
</FocusModal.Content>
|
||||
</StackedModalProvider>
|
||||
</RouteModalProvider>
|
||||
</FocusModal>
|
||||
)
|
||||
}
|
||||
|
||||
const Header = FocusModal.Header
|
||||
const Title = FocusModal.Title
|
||||
const Description = FocusModal.Description
|
||||
const Footer = FocusModal.Footer
|
||||
const Body = FocusModal.Body
|
||||
const Close = FocusModal.Close
|
||||
const Form = RouteModalForm
|
||||
|
||||
/**
|
||||
* 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,
|
||||
Title,
|
||||
Body,
|
||||
Description,
|
||||
Footer,
|
||||
Close,
|
||||
Form,
|
||||
})
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./route-modal-form"
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
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 RouteModalFormProps<TFieldValues extends FieldValues> = PropsWithChildren<{
|
||||
form: UseFormReturn<TFieldValues>
|
||||
blockSearch?: boolean
|
||||
}>
|
||||
|
||||
export const RouteModalForm = <TFieldValues extends FieldValues = any>({
|
||||
form,
|
||||
blockSearch = false,
|
||||
children,
|
||||
}: RouteModalFormProps<TFieldValues>) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const {
|
||||
formState: { isDirty },
|
||||
} = form
|
||||
|
||||
const blocker = useBlocker(({ currentLocation, nextLocation }) => {
|
||||
const { isSubmitSuccessful } = nextLocation.state || {}
|
||||
|
||||
if (isSubmitSuccessful) {
|
||||
return false
|
||||
}
|
||||
|
||||
const isPathChanged = currentLocation.pathname !== nextLocation.pathname
|
||||
const isSearchChanged = currentLocation.search !== nextLocation.search
|
||||
|
||||
if (blockSearch) {
|
||||
return isDirty && (isPathChanged || isSearchChanged)
|
||||
}
|
||||
|
||||
return isDirty && isPathChanged
|
||||
})
|
||||
|
||||
const handleCancel = () => {
|
||||
blocker?.reset?.()
|
||||
}
|
||||
|
||||
const handleContinue = () => {
|
||||
blocker?.proceed?.()
|
||||
}
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
{children}
|
||||
<Prompt open={blocker.state === "blocked"} variant="confirmation">
|
||||
<Prompt.Content>
|
||||
<Prompt.Header>
|
||||
<Prompt.Title>{t("general.unsavedChangesTitle")}</Prompt.Title>
|
||||
<Prompt.Description>
|
||||
{t("general.unsavedChangesDescription")}
|
||||
</Prompt.Description>
|
||||
</Prompt.Header>
|
||||
<Prompt.Footer>
|
||||
<Prompt.Cancel onClick={handleCancel} type="button">
|
||||
{t("actions.cancel")}
|
||||
</Prompt.Cancel>
|
||||
<Prompt.Action onClick={handleContinue} type="button">
|
||||
{t("actions.continue")}
|
||||
</Prompt.Action>
|
||||
</Prompt.Footer>
|
||||
</Prompt.Content>
|
||||
</Prompt>
|
||||
</Form>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from "./route-provider"
|
||||
export * from "./use-route-modal"
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
import { createContext } from "react"
|
||||
|
||||
type RouteModalProviderState = {
|
||||
handleSuccess: (path?: string) => void
|
||||
}
|
||||
|
||||
export const RouteModalProviderContext =
|
||||
createContext<RouteModalProviderState | null>(null)
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
import { PropsWithChildren } from "react"
|
||||
import { useNavigate } from "react-router-dom"
|
||||
import { RouteModalProviderContext } from "./route-modal-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>
|
||||
)
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
import { useContext } from "react"
|
||||
import { RouteModalProviderContext } from "./route-modal-context"
|
||||
|
||||
export const useRouteModal = () => {
|
||||
const context = useContext(RouteModalProviderContext)
|
||||
|
||||
if (!context) {
|
||||
throw new Error("useRouteModal must be used within a RouteModalProvider")
|
||||
}
|
||||
|
||||
return context
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./stacked-drawer"
|
||||
@@ -0,0 +1,85 @@
|
||||
import { Drawer, clx } from "@medusajs/ui"
|
||||
import {
|
||||
ComponentPropsWithoutRef,
|
||||
PropsWithChildren,
|
||||
forwardRef,
|
||||
useEffect,
|
||||
} from "react"
|
||||
import { useStackedModal } from "../stacked-modal-provider"
|
||||
|
||||
type StackedDrawerProps = PropsWithChildren<{
|
||||
/**
|
||||
* A unique identifier for the modal. This is used to differentiate stacked modals,
|
||||
* when multiple stacked modals are registered to the same parent modal.
|
||||
*/
|
||||
id: string
|
||||
}>
|
||||
|
||||
/**
|
||||
* A stacked modal that can be rendered above a parent modal.
|
||||
*/
|
||||
export const Root = ({ id, children }: StackedDrawerProps) => {
|
||||
const { register, unregister, getIsOpen, setIsOpen } = useStackedModal()
|
||||
|
||||
useEffect(() => {
|
||||
register(id)
|
||||
|
||||
return () => unregister(id)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Drawer open={getIsOpen(id)} onOpenChange={(open) => setIsOpen(id, open)}>
|
||||
{children}
|
||||
</Drawer>
|
||||
)
|
||||
}
|
||||
|
||||
const Close = Drawer.Close
|
||||
Close.displayName = "StackedDrawer.Close"
|
||||
|
||||
const Header = Drawer.Header
|
||||
Header.displayName = "StackedDrawer.Header"
|
||||
|
||||
const Body = Drawer.Body
|
||||
Body.displayName = "StackedDrawer.Body"
|
||||
|
||||
const Trigger = Drawer.Trigger
|
||||
Trigger.displayName = "StackedDrawer.Trigger"
|
||||
|
||||
const Footer = Drawer.Footer
|
||||
Footer.displayName = "StackedDrawer.Footer"
|
||||
|
||||
const Title = Drawer.Title
|
||||
Title.displayName = "StackedDrawer.Title"
|
||||
|
||||
const Description = Drawer.Description
|
||||
Description.displayName = "StackedDrawer.Description"
|
||||
|
||||
const Content = forwardRef<
|
||||
HTMLDivElement,
|
||||
ComponentPropsWithoutRef<typeof Drawer.Content>
|
||||
>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<Drawer.Content
|
||||
ref={ref}
|
||||
className={clx(className)}
|
||||
overlayProps={{
|
||||
className: "bg-transparent",
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
Content.displayName = "StackedDrawer.Content"
|
||||
|
||||
export const StackedDrawer = Object.assign(Root, {
|
||||
Close,
|
||||
Header,
|
||||
Body,
|
||||
Content,
|
||||
Trigger,
|
||||
Footer,
|
||||
Description,
|
||||
Title,
|
||||
})
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./stacked-foucs-modal"
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
import { FocusModal, clx } from "@medusajs/ui"
|
||||
import {
|
||||
ComponentPropsWithoutRef,
|
||||
PropsWithChildren,
|
||||
forwardRef,
|
||||
useEffect,
|
||||
} from "react"
|
||||
import { useStackedModal } from "../stacked-modal-provider"
|
||||
|
||||
type StackedFocusModalProps = PropsWithChildren<{
|
||||
/**
|
||||
* A unique identifier for the modal. This is used to differentiate stacked modals,
|
||||
* when multiple stacked modals are registered to the same parent modal.
|
||||
*/
|
||||
id: string
|
||||
}>
|
||||
|
||||
/**
|
||||
* A stacked modal that can be rendered above a parent modal.
|
||||
*/
|
||||
export const Root = ({ id, children }: StackedFocusModalProps) => {
|
||||
const { register, unregister, getIsOpen, setIsOpen } = useStackedModal()
|
||||
|
||||
useEffect(() => {
|
||||
register(id)
|
||||
|
||||
return () => unregister(id)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<FocusModal
|
||||
open={getIsOpen(id)}
|
||||
onOpenChange={(open) => setIsOpen(id, open)}
|
||||
>
|
||||
{children}
|
||||
</FocusModal>
|
||||
)
|
||||
}
|
||||
|
||||
const Close = FocusModal.Close
|
||||
Close.displayName = "StackedFocusModal.Close"
|
||||
|
||||
const Header = FocusModal.Header
|
||||
Header.displayName = "StackedFocusModal.Header"
|
||||
|
||||
const Body = FocusModal.Body
|
||||
Body.displayName = "StackedFocusModal.Body"
|
||||
|
||||
const Trigger = FocusModal.Trigger
|
||||
Trigger.displayName = "StackedFocusModal.Trigger"
|
||||
|
||||
const Footer = FocusModal.Footer
|
||||
Footer.displayName = "StackedFocusModal.Footer"
|
||||
|
||||
const Title = FocusModal.Title
|
||||
Title.displayName = "StackedFocusModal.Title"
|
||||
|
||||
const Description = FocusModal.Description
|
||||
Description.displayName = "StackedFocusModal.Description"
|
||||
|
||||
const Content = forwardRef<
|
||||
HTMLDivElement,
|
||||
ComponentPropsWithoutRef<typeof FocusModal.Content>
|
||||
>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<FocusModal.Content
|
||||
ref={ref}
|
||||
className={clx("top-6", className)}
|
||||
overlayProps={{
|
||||
className: "bg-transparent",
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
Content.displayName = "StackedFocusModal.Content"
|
||||
|
||||
export const StackedFocusModal = Object.assign(Root, {
|
||||
Close,
|
||||
Header,
|
||||
Body,
|
||||
Content,
|
||||
Trigger,
|
||||
Footer,
|
||||
Description,
|
||||
Title,
|
||||
})
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from "./stacked-modal-provider"
|
||||
export * from "./use-stacked-modal"
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
import { createContext } from "react"
|
||||
|
||||
type StackedModalState = {
|
||||
getIsOpen: (id: string) => boolean
|
||||
setIsOpen: (id: string, open: boolean) => void
|
||||
register: (id: string) => void
|
||||
unregister: (id: string) => void
|
||||
}
|
||||
|
||||
export const StackedModalContext = createContext<StackedModalState | null>(null)
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
import { PropsWithChildren, useState } from "react"
|
||||
import { StackedModalContext } from "./stacked-modal-context"
|
||||
|
||||
type StackedModalProviderProps = PropsWithChildren<{
|
||||
onOpenChange: (open: boolean) => void
|
||||
}>
|
||||
|
||||
export const StackedModalProvider = ({
|
||||
children,
|
||||
onOpenChange,
|
||||
}: StackedModalProviderProps) => {
|
||||
const [state, setState] = useState<Record<string, boolean>>({})
|
||||
|
||||
const getIsOpen = (id: string) => {
|
||||
return state[id] || false
|
||||
}
|
||||
|
||||
const setIsOpen = (id: string, open: boolean) => {
|
||||
setState((prevState) => ({
|
||||
...prevState,
|
||||
[id]: open,
|
||||
}))
|
||||
|
||||
onOpenChange(open)
|
||||
}
|
||||
|
||||
const register = (id: string) => {
|
||||
setState((prevState) => ({
|
||||
...prevState,
|
||||
[id]: false,
|
||||
}))
|
||||
}
|
||||
|
||||
const unregister = (id: string) => {
|
||||
setState((prevState) => {
|
||||
const newState = { ...prevState }
|
||||
delete newState[id]
|
||||
return newState
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<StackedModalContext.Provider
|
||||
value={{
|
||||
getIsOpen,
|
||||
setIsOpen,
|
||||
register,
|
||||
unregister,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</StackedModalContext.Provider>
|
||||
)
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
import { useContext } from "react"
|
||||
import { StackedModalContext } from "./stacked-modal-context"
|
||||
|
||||
export const useStackedModal = () => {
|
||||
const context = useContext(StackedModalContext)
|
||||
|
||||
if (!context) {
|
||||
throw new Error(
|
||||
"useStackedModal must be used within a StackedModalProvider"
|
||||
)
|
||||
}
|
||||
|
||||
return context
|
||||
}
|
||||
Reference in New Issue
Block a user