-
-
Notifications
You must be signed in to change notification settings - Fork 2.9k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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.
- Loading branch information
1 parent
add861d
commit 72a17d6
Showing
88 changed files
with
1,748 additions
and
1,797 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
3 changes: 3 additions & 0 deletions
3
packages/admin-next/dashboard/src/components/route-modal/index.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
export { RouteDrawer } from "./route-drawer" | ||
export { RouteFocusModal } from "./route-focus-modal" | ||
export { useRouteModal } from "./route-modal-provider" |
1 change: 1 addition & 0 deletions
1
packages/admin-next/dashboard/src/components/route-modal/route-drawer/index.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from "./route-drawer" |
59 changes: 59 additions & 0 deletions
59
packages/admin-next/dashboard/src/components/route-modal/route-drawer/route-drawer.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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, | ||
}) |
1 change: 1 addition & 0 deletions
1
packages/admin-next/dashboard/src/components/route-modal/route-focus-modal/index.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from "./route-focus-modal" |
58 changes: 58 additions & 0 deletions
58
...s/admin-next/dashboard/src/components/route-modal/route-focus-modal/route-focus-modal.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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, | ||
}) |
1 change: 1 addition & 0 deletions
1
packages/admin-next/dashboard/src/components/route-modal/route-form/index.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from "./route-form" |
63 changes: 63 additions & 0 deletions
63
packages/admin-next/dashboard/src/components/route-modal/route-form/route-form.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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> | ||
) | ||
} |
1 change: 1 addition & 0 deletions
1
packages/admin-next/dashboard/src/components/route-modal/route-modal-provider/index.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from "./route-provider" |
41 changes: 41 additions & 0 deletions
41
...s/admin-next/dashboard/src/components/route-modal/route-modal-provider/route-provider.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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> | ||
) | ||
} |
56 changes: 56 additions & 0 deletions
56
...-next/dashboard/src/components/table/table-cells/region/countries-cell/countries-cell.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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> | ||
) | ||
} |
1 change: 1 addition & 0 deletions
1
...ages/admin-next/dashboard/src/components/table/table-cells/region/countries-cell/index.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from "./countries-cell" |
58 changes: 58 additions & 0 deletions
58
...onents/table/table-cells/region/fulfillment-providers-cell/fulfillment-providers-cell.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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> | ||
) | ||
} |
1 change: 1 addition & 0 deletions
1
...ext/dashboard/src/components/table/table-cells/region/fulfillment-providers-cell/index.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from "./fulfillment-providers-cell" |
1 change: 1 addition & 0 deletions
1
...in-next/dashboard/src/components/table/table-cells/region/payment-providers-cell/index.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from "./payment-providers-cell" |
Oops, something went wrong.