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:
Kasper Fabricius Kristensen
2024-06-28 14:08:23 +00:00
committed by GitHub
parent 9f3998393b
commit c1740218e9
295 changed files with 4488 additions and 2350 deletions
@@ -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"
@@ -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"
@@ -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"
@@ -0,0 +1,8 @@
import { createContext } from "react"
type RouteModalProviderState = {
handleSuccess: (path?: string) => void
}
export const RouteModalProviderContext =
createContext<RouteModalProviderState | null>(null)
@@ -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>
)
}
@@ -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"
@@ -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"
@@ -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)
@@ -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>
)
}
@@ -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
}