feat(admin-sdk,admin-bundler,admin-shared,medusa): Restructure admin packages (#8988)
**What** - Renames /admin-next -> /admin - Renames @medusajs/admin-sdk -> @medusajs/admin-bundler - Creates a new package called @medusajs/admin-sdk that will hold all tooling relevant to creating admin extensions. This is currently `defineRouteConfig` and `defineWidgetConfig`, but will eventually also export methods for adding custom fields, register translation, etc. - cc: @shahednasser we should update the examples in the docs so these functions are imported from `@medusajs/admin-sdk`. People will also need to install the package in their project, as it's no longer a transient dependency. - cc: @olivermrbl we might want to publish a changelog when this is merged, as it is a breaking change, and will require people to import the `defineXConfig` from the new package instead of `@medusajs/admin-shared`. - Updates CODEOWNERS so /admin packages does not require a review from the UI team.
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"
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
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 { useRouteModal } from "../route-modal-provider"
|
||||
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}>
|
||||
<Content stackedModalOpen={stackedModalOpen}>{children}</Content>
|
||||
</StackedModalProvider>
|
||||
</RouteModalProvider>
|
||||
</FocusModal>
|
||||
)
|
||||
}
|
||||
|
||||
type ContentProps = PropsWithChildren<{
|
||||
stackedModalOpen: boolean
|
||||
}>
|
||||
|
||||
const Content = ({ stackedModalOpen, children }: ContentProps) => {
|
||||
const { __internal } = useRouteModal()
|
||||
|
||||
const shouldPreventClose = !__internal.closeOnEscape
|
||||
|
||||
return (
|
||||
<FocusModal.Content
|
||||
onEscapeKeyDown={
|
||||
shouldPreventClose
|
||||
? (e) => {
|
||||
e.preventDefault()
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
className={clx({
|
||||
"!bg-ui-bg-disabled !inset-x-5 !inset-y-3": stackedModalOpen,
|
||||
})}
|
||||
>
|
||||
{children}
|
||||
</FocusModal.Content>
|
||||
)
|
||||
}
|
||||
|
||||
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,88 @@
|
||||
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
|
||||
onClose?: (isSubmitSuccessful: boolean) => void
|
||||
}>
|
||||
|
||||
export const RouteModalForm = <TFieldValues extends FieldValues = any>({
|
||||
form,
|
||||
blockSearch = false,
|
||||
children,
|
||||
onClose,
|
||||
}: RouteModalFormProps<TFieldValues>) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const {
|
||||
formState: { isDirty },
|
||||
} = form
|
||||
|
||||
const blocker = useBlocker(({ currentLocation, nextLocation }) => {
|
||||
const { isSubmitSuccessful } = nextLocation.state || {}
|
||||
|
||||
if (isSubmitSuccessful) {
|
||||
onClose?.(true)
|
||||
return false
|
||||
}
|
||||
|
||||
const isPathChanged = currentLocation.pathname !== nextLocation.pathname
|
||||
const isSearchChanged = currentLocation.search !== nextLocation.search
|
||||
|
||||
if (blockSearch) {
|
||||
const ret = isDirty && (isPathChanged || isSearchChanged)
|
||||
|
||||
if (!ret) {
|
||||
onClose?.(isSubmitSuccessful)
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
const ret = isDirty && isPathChanged
|
||||
|
||||
if (!ret) {
|
||||
onClose?.(isSubmitSuccessful)
|
||||
}
|
||||
|
||||
return ret
|
||||
})
|
||||
|
||||
const handleCancel = () => {
|
||||
blocker?.reset?.()
|
||||
}
|
||||
|
||||
const handleContinue = () => {
|
||||
blocker?.proceed?.()
|
||||
onClose?.(false)
|
||||
}
|
||||
|
||||
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"
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
import { createContext } from "react"
|
||||
|
||||
type RouteModalProviderState = {
|
||||
handleSuccess: (path?: string) => void
|
||||
setCloseOnEscape: (value: boolean) => void
|
||||
__internal: {
|
||||
closeOnEscape: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export const RouteModalProviderContext =
|
||||
createContext<RouteModalProviderState | null>(null)
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
import { PropsWithChildren, useCallback, useMemo, useState } 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 [closeOnEscape, setCloseOnEscape] = useState(true)
|
||||
|
||||
const handleSuccess = useCallback(
|
||||
(path?: string) => {
|
||||
const to = path || prev
|
||||
navigate(to, { replace: true, state: { isSubmitSuccessful: true } })
|
||||
},
|
||||
[navigate, prev]
|
||||
)
|
||||
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
handleSuccess,
|
||||
setCloseOnEscape,
|
||||
__internal: { closeOnEscape },
|
||||
}),
|
||||
[handleSuccess, setCloseOnEscape, closeOnEscape]
|
||||
)
|
||||
|
||||
return (
|
||||
<RouteModalProviderContext.Provider value={value}>
|
||||
{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-focus-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