feat(dashboard,admin-vite-plugin,admin-bundler,admin-sdk): Rework admin extensions and introduce custom fields API (#9338)
This commit is contained in:
1 parent
35e69d32f2
commit
d71343d6ab
159 files changed
+5264
-2224
No files matched your search
@@ -21,7 +21,9 @@
|
||||
"./css": {
|
||||
"import": "./dist/app.css",
|
||||
"require": "./dist/app.css"
|
||||
}
|
||||
},
|
||||
"./root": "./",
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -29,8 +31,10 @@
|
||||
"directory": "packages/admin/dashboard"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"package.json"
|
||||
"package.json",
|
||||
"src",
|
||||
"index.html",
|
||||
"dist"
|
||||
],
|
||||
"dependencies": {
|
||||
"@ariakit/react": "^0.4.1",
|
||||
@@ -38,6 +42,7 @@
|
||||
"@dnd-kit/sortable": "^8.0.0",
|
||||
"@hookform/error-message": "^2.0.1",
|
||||
"@hookform/resolvers": "3.4.2",
|
||||
"@medusajs/admin-shared": "0.0.1",
|
||||
"@medusajs/icons": "1.2.1",
|
||||
"@medusajs/js-sdk": "0.0.1",
|
||||
"@medusajs/ui": "3.0.0",
|
||||
@@ -81,7 +86,8 @@
|
||||
"tailwindcss": "^3.4.1",
|
||||
"tsup": "^8.0.2",
|
||||
"typescript": "5.2.2",
|
||||
"vite": "^5.2.11"
|
||||
"vite": "^5.2.11",
|
||||
"vite-plugin-inspect": "^0.8.7"
|
||||
},
|
||||
"packageManager": "yarn@3.2.1"
|
||||
}
|
||||
@@ -1,30 +1,26 @@
|
||||
import { Toaster, TooltipProvider } from "@medusajs/ui"
|
||||
import { QueryClientProvider } from "@tanstack/react-query"
|
||||
import { HelmetProvider } from "react-helmet-async"
|
||||
|
||||
import { I18n } from "./components/utilities/i18n"
|
||||
import { queryClient } from "./lib/query-client"
|
||||
import { I18nProvider } from "./providers/i18n-provider"
|
||||
import { DashboardExtensionManager } from "./extensions"
|
||||
import { Providers } from "./providers/providers"
|
||||
import { RouterProvider } from "./providers/router-provider"
|
||||
import { ThemeProvider } from "./providers/theme-provider"
|
||||
|
||||
import displayModule from "virtual:medusa/displays"
|
||||
import formModule from "virtual:medusa/forms"
|
||||
import menuItemModule from "virtual:medusa/menu-items"
|
||||
import widgetModule from "virtual:medusa/widgets"
|
||||
|
||||
import "./index.css"
|
||||
|
||||
function App() {
|
||||
const manager = new DashboardExtensionManager({
|
||||
displayModule,
|
||||
formModule,
|
||||
menuItemModule,
|
||||
widgetModule,
|
||||
})
|
||||
|
||||
return (
|
||||
<HelmetProvider>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ThemeProvider>
|
||||
<I18n />
|
||||
<TooltipProvider>
|
||||
<I18nProvider>
|
||||
<RouterProvider />
|
||||
</I18nProvider>
|
||||
</TooltipProvider>
|
||||
<Toaster />
|
||||
</ThemeProvider>
|
||||
</QueryClientProvider>
|
||||
</HelmetProvider>
|
||||
<Providers api={manager.api}>
|
||||
<RouterProvider />
|
||||
</Providers>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -19,14 +19,13 @@ import * as Collapsible from "@radix-ui/react-collapsible"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
import { useStore } from "../../../hooks/api/store"
|
||||
import { settingsRouteRegex } from "../../../lib/extension-helpers"
|
||||
import { Divider } from "../../common/divider"
|
||||
import { Skeleton } from "../../common/skeleton"
|
||||
import { NavItem, NavItemProps } from "../../layout/nav-item"
|
||||
import { INavItem, NavItem } from "../../layout/nav-item"
|
||||
import { Shell } from "../../layout/shell"
|
||||
|
||||
import { Link, useLocation, useNavigate } from "react-router-dom"
|
||||
import routes from "virtual:medusa/routes/links"
|
||||
import { useDashboardExtension } from "../../../extensions"
|
||||
import { useLogout } from "../../../hooks/api"
|
||||
import { queryClient } from "../../../lib/query-client"
|
||||
import { useSearch } from "../../../providers/search-provider"
|
||||
@@ -177,7 +176,7 @@ const Header = () => {
|
||||
)
|
||||
}
|
||||
|
||||
const useCoreRoutes = (): Omit<NavItemProps, "pathname">[] => {
|
||||
const useCoreRoutes = (): Omit<INavItem, "pathname">[] => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return [
|
||||
@@ -297,14 +296,11 @@ const CoreRouteSection = () => {
|
||||
|
||||
const ExtensionRouteSection = () => {
|
||||
const { t } = useTranslation()
|
||||
const { getMenu } = useDashboardExtension()
|
||||
|
||||
const links = routes.links
|
||||
const menuItems = getMenu("coreExtensions")
|
||||
|
||||
const extensionLinks = links
|
||||
.filter((link) => !settingsRouteRegex.test(link.path))
|
||||
.sort((a, b) => a.label.localeCompare(b.label))
|
||||
|
||||
if (!extensionLinks.length) {
|
||||
if (!menuItems.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -330,13 +326,14 @@ const ExtensionRouteSection = () => {
|
||||
</div>
|
||||
<Collapsible.Content>
|
||||
<nav className="flex flex-col gap-y-0.5 py-1 pb-4">
|
||||
{extensionLinks.map((link) => {
|
||||
{menuItems.map((item, i) => {
|
||||
return (
|
||||
<NavItem
|
||||
key={link.path}
|
||||
to={link.path}
|
||||
label={link.label}
|
||||
icon={link.icon ? <link.icon /> : <SquaresPlus />}
|
||||
key={i}
|
||||
to={item.to}
|
||||
label={item.label}
|
||||
icon={item.icon ? item.icon : <SquaresPlus />}
|
||||
items={item.items}
|
||||
type="extension"
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -19,7 +19,7 @@ type NestedItemProps = {
|
||||
to: string
|
||||
}
|
||||
|
||||
export type NavItemProps = {
|
||||
export type INavItem = {
|
||||
icon?: ReactNode
|
||||
label: string
|
||||
to: string
|
||||
@@ -29,11 +29,11 @@ export type NavItemProps = {
|
||||
}
|
||||
|
||||
const BASE_NAV_LINK_CLASSES =
|
||||
"text-ui-fg-subtle transition-fg hover:bg-ui-bg-subtle-hover flex items-center gap-x-2 rounded-md py-1 pl-0.5 pr-2 outline-none [&>svg]:text-ui-fg-subtle focus-visible:shadow-borders-focus"
|
||||
"text-ui-fg-subtle transition-fg hover:bg-ui-bg-subtle-hover flex items-center gap-x-2 rounded-md py-0.5 pl-0.5 pr-2 outline-none [&>svg]:text-ui-fg-subtle focus-visible:shadow-borders-focus"
|
||||
const ACTIVE_NAV_LINK_CLASSES =
|
||||
"bg-ui-bg-base shadow-elevation-card-rest text-ui-fg-base hover:bg-ui-bg-base"
|
||||
const NESTED_NAV_LINK_CLASSES = "pl-[34px] pr-2 w-full text-ui-fg-muted"
|
||||
const SETTING_NAV_LINK_CLASSES = "pl-2"
|
||||
const NESTED_NAV_LINK_CLASSES = "pl-[34px] pr-2 py-1 w-full text-ui-fg-muted"
|
||||
const SETTING_NAV_LINK_CLASSES = "pl-2 py-1"
|
||||
|
||||
const getIsOpen = (
|
||||
to: string,
|
||||
@@ -89,7 +89,7 @@ export const NavItem = ({
|
||||
items,
|
||||
type = "core",
|
||||
from,
|
||||
}: NavItemProps) => {
|
||||
}: INavItem) => {
|
||||
const { pathname } = useLocation()
|
||||
const [open, setOpen] = useState(getIsOpen(to, items, pathname))
|
||||
|
||||
@@ -149,7 +149,7 @@ export const NavItem = ({
|
||||
<Collapsible.Root open={open} onOpenChange={setOpen}>
|
||||
<Collapsible.Trigger
|
||||
className={clx(
|
||||
"text-ui-fg-subtle hover:text-ui-fg-base transition-fg hover:bg-ui-bg-subtle-hover flex w-full items-center gap-x-2 rounded-md py-1 pl-0.5 pr-2 outline-none lg:hidden",
|
||||
"text-ui-fg-subtle hover:text-ui-fg-base transition-fg hover:bg-ui-bg-subtle-hover flex w-full items-center gap-x-2 rounded-md py-0.5 pl-0.5 pr-2 outline-none lg:hidden",
|
||||
{ "pl-2": isSetting }
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -126,17 +126,17 @@ const Notification = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex items-start justify-center gap-3 border-b p-6 relative">
|
||||
<div className="relative flex items-start justify-center gap-3 border-b p-6">
|
||||
<div className="text-ui-fg-muted flex size-5 items-center justify-center">
|
||||
<InformationCircleSolid />
|
||||
</div>
|
||||
<div className="flex w-full flex-col gap-y-3">
|
||||
<div className="flex flex-col">
|
||||
<div className="items-center flex justify-between">
|
||||
<div className="flex items-center justify-between">
|
||||
<Text size="small" leading="compact" weight="plus">
|
||||
{data.title}
|
||||
</Text>
|
||||
<div className="items-center flex justify-center align-center gap-2">
|
||||
<div className="align-center flex items-center justify-center gap-2">
|
||||
<Text
|
||||
as={"span"}
|
||||
className={clx("text-ui-fg-subtle", {
|
||||
@@ -152,7 +152,7 @@ const Notification = ({
|
||||
</Text>
|
||||
{unread && (
|
||||
<div
|
||||
className="h-2 w-2 rounded bg-ui-bg-interactive"
|
||||
className="bg-ui-bg-interactive h-2 w-2 rounded"
|
||||
role="status"
|
||||
/>
|
||||
)}
|
||||
@@ -201,7 +201,7 @@ const useUnreadNotifications = () => {
|
||||
const [hasUnread, setHasUnread] = useState(false)
|
||||
const { notifications } = useNotifications(
|
||||
{ limit: 1, offset: 0, fields: "created_at" },
|
||||
{ refetchInterval: 3000 }
|
||||
{ refetchInterval: 60_000 }
|
||||
)
|
||||
const lastNotification = notifications?.[0]
|
||||
|
||||
|
||||
+4
-4
@@ -48,12 +48,12 @@ export const SingleColumnPage = <TData,>({
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-y-3">
|
||||
{before.widgets.map((w, i) => {
|
||||
return <w.Component {...widgetProps} key={i} />
|
||||
{before.map((Component, i) => {
|
||||
return <Component {...widgetProps} key={i} />
|
||||
})}
|
||||
{children}
|
||||
{after.widgets.map((w, i) => {
|
||||
return <w.Component {...widgetProps} key={i} />
|
||||
{after.map((Component, i) => {
|
||||
return <Component {...widgetProps} key={i} />
|
||||
})}
|
||||
{showMetadata && <MetadataSection data={data!} />}
|
||||
{showJSON && <JsonViewSection data={data!} />}
|
||||
|
||||
+12
-12
@@ -1,13 +1,13 @@
|
||||
import { clx } from "@medusajs/ui"
|
||||
import { Children, ComponentPropsWithoutRef } from "react"
|
||||
import { Children, ComponentPropsWithoutRef, ComponentType } from "react"
|
||||
import { Outlet } from "react-router-dom"
|
||||
import { JsonViewSection } from "../../../common/json-view-section"
|
||||
import { MetadataSection } from "../../../common/metadata-section"
|
||||
import { PageProps, WidgetImport, WidgetProps } from "../types"
|
||||
import { PageProps, WidgetProps } from "../types"
|
||||
|
||||
interface TwoColumnWidgetProps extends WidgetProps {
|
||||
sideBefore: WidgetImport
|
||||
sideAfter: WidgetImport
|
||||
sideBefore: ComponentType<any>[]
|
||||
sideAfter: ComponentType<any>[]
|
||||
}
|
||||
|
||||
interface TwoColumnPageProps<TData> extends PageProps<TData> {
|
||||
@@ -71,14 +71,14 @@ const Root = <TData,>({
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-y-3">
|
||||
{before.widgets.map((w, i) => {
|
||||
return <w.Component {...widgetProps} key={i} />
|
||||
{before.map((Component, i) => {
|
||||
return <Component {...widgetProps} key={i} />
|
||||
})}
|
||||
<div className="flex flex-col gap-x-4 gap-y-3 xl:flex-row xl:items-start">
|
||||
<div className="flex w-full flex-col gap-y-3">
|
||||
{main}
|
||||
{after.widgets.map((w, i) => {
|
||||
return <w.Component {...widgetProps} key={i} />
|
||||
{after.map((Component, i) => {
|
||||
return <Component {...widgetProps} key={i} />
|
||||
})}
|
||||
{showExtraData && (
|
||||
<div className="hidden flex-col gap-y-3 xl:flex">
|
||||
@@ -88,12 +88,12 @@ const Root = <TData,>({
|
||||
)}
|
||||
</div>
|
||||
<div className="flex w-full max-w-[100%] flex-col gap-y-3 xl:mt-0 xl:max-w-[440px]">
|
||||
{sideBefore.widgets.map((w, i) => {
|
||||
return <w.Component {...widgetProps} key={i} />
|
||||
{sideBefore.map((Component, i) => {
|
||||
return <Component {...widgetProps} key={i} />
|
||||
})}
|
||||
{sidebar}
|
||||
{sideAfter.widgets.map((w, i) => {
|
||||
return <w.Component {...widgetProps} key={i} />
|
||||
{sideAfter.map((Component, i) => {
|
||||
return <Component {...widgetProps} key={i} />
|
||||
})}
|
||||
{showExtraData && (
|
||||
<div className="flex flex-col gap-y-3 xl:hidden">
|
||||
|
||||
@@ -1,16 +1,8 @@
|
||||
import { ReactNode } from "react"
|
||||
|
||||
export type Widget = {
|
||||
Component: React.ComponentType<any>
|
||||
}
|
||||
|
||||
export type WidgetImport = {
|
||||
widgets: Widget[]
|
||||
}
|
||||
import { ComponentType, ReactNode } from "react"
|
||||
|
||||
export interface WidgetProps {
|
||||
before: WidgetImport
|
||||
after: WidgetImport
|
||||
before: ComponentType<any>[]
|
||||
after: ComponentType<any>[]
|
||||
}
|
||||
|
||||
export interface PageProps<TData> {
|
||||
|
||||
@@ -1,14 +1,5 @@
|
||||
import { Outlet } from "react-router-dom";
|
||||
import { Outlet } from "react-router-dom"
|
||||
|
||||
export const PublicLayout = () => {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center px-4 py-6">
|
||||
<div className="bg-ui-bg-base text-ui-fg-subtle w-[520px] px-16 py-20 rounded-[32px] shadow-elevation-modal flex flex-col gap-y-12 items-center">
|
||||
<div className="w-24 h-24 rounded-3xl bg-ui-bg-subtle shadow-elevation-card-hover"></div>
|
||||
<div className="w-full">
|
||||
<Outlet />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
return <Outlet />
|
||||
}
|
||||
@@ -5,12 +5,11 @@ import { Fragment, useEffect, useMemo, useState } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { Link, useLocation } from "react-router-dom"
|
||||
|
||||
import { settingsRouteRegex } from "../../../lib/extension-helpers"
|
||||
import { Divider } from "../../common/divider"
|
||||
import { NavItem, NavItemProps } from "../nav-item"
|
||||
import { INavItem, NavItem } from "../nav-item"
|
||||
import { Shell } from "../shell"
|
||||
|
||||
import routes from "virtual:medusa/routes/links"
|
||||
import { useDashboardExtension } from "../../../extensions"
|
||||
import { UserMenu } from "../user-menu"
|
||||
|
||||
export const SettingsLayout = () => {
|
||||
@@ -21,7 +20,7 @@ export const SettingsLayout = () => {
|
||||
)
|
||||
}
|
||||
|
||||
const useSettingRoutes = (): NavItemProps[] => {
|
||||
const useSettingRoutes = (): INavItem[] => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return useMemo(
|
||||
@@ -67,7 +66,7 @@ const useSettingRoutes = (): NavItemProps[] => {
|
||||
)
|
||||
}
|
||||
|
||||
const useDeveloperRoutes = (): NavItemProps[] => {
|
||||
const useDeveloperRoutes = (): INavItem[] => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return useMemo(
|
||||
@@ -89,7 +88,7 @@ const useDeveloperRoutes = (): NavItemProps[] => {
|
||||
)
|
||||
}
|
||||
|
||||
const useMyAccountRoutes = (): NavItemProps[] => {
|
||||
const useMyAccountRoutes = (): INavItem[] => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return useMemo(
|
||||
@@ -103,21 +102,6 @@ const useMyAccountRoutes = (): NavItemProps[] => {
|
||||
)
|
||||
}
|
||||
|
||||
const useExtensionRoutes = (): NavItemProps[] => {
|
||||
const links = routes.links
|
||||
|
||||
return useMemo(() => {
|
||||
const settingsLinks = links.filter((link) =>
|
||||
settingsRouteRegex.test(link.path)
|
||||
)
|
||||
|
||||
return settingsLinks.map((link) => ({
|
||||
label: link.label,
|
||||
to: link.path,
|
||||
}))
|
||||
}, [links])
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that the `from` prop is not another settings route, to avoid
|
||||
* the user getting stuck in a navigation loop.
|
||||
@@ -131,10 +115,12 @@ const getSafeFromValue = (from: string) => {
|
||||
}
|
||||
|
||||
const SettingsSidebar = () => {
|
||||
const { getMenu } = useDashboardExtension()
|
||||
|
||||
const routes = useSettingRoutes()
|
||||
const developerRoutes = useDeveloperRoutes()
|
||||
const extensionRoutes = useExtensionRoutes()
|
||||
const myAccountRoutes = useMyAccountRoutes()
|
||||
const extensionRoutes = getMenu("settingsExtensions")
|
||||
|
||||
const { t } = useTranslation()
|
||||
|
||||
@@ -227,7 +213,7 @@ const CollapsibleSection = ({
|
||||
items,
|
||||
}: {
|
||||
label: string
|
||||
items: NavItemProps[]
|
||||
items: INavItem[]
|
||||
}) => {
|
||||
return (
|
||||
<Collapsible.Root defaultOpen className="py-3">
|
||||
|
||||
@@ -42,6 +42,7 @@ const Root = ({ prev = "..", children }: RouteDrawerProps) => {
|
||||
<RouteModalProvider prev={prev}>
|
||||
<StackedModalProvider onOpenChange={onStackedModalOpen}>
|
||||
<Drawer.Content
|
||||
aria-describedby={undefined}
|
||||
className={clx({
|
||||
"!bg-ui-bg-disabled !inset-y-5 !right-5": stackedModalOpen,
|
||||
})}
|
||||
|
||||
@@ -20,6 +20,16 @@ export const ErrorBoundary = () => {
|
||||
code = error.status ?? null
|
||||
}
|
||||
|
||||
/**
|
||||
* Log error in development mode.
|
||||
*
|
||||
* react-router-dom will sometimes swallow the error,
|
||||
* so this ensures that we always log it.
|
||||
*/
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
console.error(error)
|
||||
}
|
||||
|
||||
let title: string
|
||||
let message: string
|
||||
|
||||
|
||||
+319
@@ -0,0 +1,319 @@
|
||||
import {
|
||||
CustomFieldContainerZone,
|
||||
CustomFieldFormTab,
|
||||
CustomFieldFormZone,
|
||||
CustomFieldModel,
|
||||
InjectionZone,
|
||||
} from "@medusajs/admin-shared"
|
||||
import * as React from "react"
|
||||
import { INavItem } from "../../components/layout/nav-item"
|
||||
import {
|
||||
ConfigExtension,
|
||||
ConfigField,
|
||||
ConfigFieldMap,
|
||||
DisplayExtension,
|
||||
DisplayMap,
|
||||
DisplayModule,
|
||||
FormExtension,
|
||||
FormField,
|
||||
FormFieldExtension,
|
||||
FormFieldMap,
|
||||
FormModule,
|
||||
FormZoneMap,
|
||||
MenuItemExtension,
|
||||
MenuItemKey,
|
||||
MenuItemModule,
|
||||
WidgetExtension,
|
||||
WidgetModule,
|
||||
ZoneStructure,
|
||||
} from "../types"
|
||||
|
||||
export type DashboardExtensionManagerProps = {
|
||||
formModule: FormModule
|
||||
displayModule: DisplayModule
|
||||
menuItemModule: MenuItemModule
|
||||
widgetModule: WidgetModule
|
||||
}
|
||||
|
||||
export class DashboardExtensionManager {
|
||||
private widgets: Map<InjectionZone, React.ComponentType[]>
|
||||
private menus: Map<MenuItemKey, INavItem[]>
|
||||
private fields: FormFieldMap
|
||||
private configs: ConfigFieldMap
|
||||
private displays: DisplayMap
|
||||
|
||||
constructor({
|
||||
widgetModule,
|
||||
menuItemModule,
|
||||
displayModule,
|
||||
formModule,
|
||||
}: DashboardExtensionManagerProps) {
|
||||
this.widgets = this.populateWidgets(widgetModule.widgets)
|
||||
this.menus = this.populateMenus(menuItemModule.menuItems)
|
||||
|
||||
const { fields, configs } = this.populateForm(formModule)
|
||||
this.fields = fields
|
||||
this.configs = configs
|
||||
this.displays = this.populateDisplays(displayModule)
|
||||
}
|
||||
|
||||
private populateWidgets(widgets: WidgetExtension[] | undefined) {
|
||||
const registry = new Map<InjectionZone, React.ComponentType[]>()
|
||||
|
||||
if (!widgets) {
|
||||
return registry
|
||||
}
|
||||
|
||||
widgets.forEach((widget) => {
|
||||
widget.zone.forEach((zone) => {
|
||||
if (!registry.has(zone)) {
|
||||
registry.set(zone, [])
|
||||
}
|
||||
registry.get(zone)!.push(widget.Component)
|
||||
})
|
||||
})
|
||||
|
||||
return registry
|
||||
}
|
||||
|
||||
private populateMenus(menuItems: MenuItemExtension[] | undefined) {
|
||||
const registry = new Map<MenuItemKey, INavItem[]>()
|
||||
const tempRegistry: Record<string, INavItem> = {}
|
||||
|
||||
if (!menuItems) {
|
||||
return registry
|
||||
}
|
||||
|
||||
menuItems.sort((a, b) => a.path.length - b.path.length)
|
||||
|
||||
menuItems.forEach((item) => {
|
||||
if (item.path.includes("/:")) {
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
console.warn(
|
||||
`Menu item for path "${item.path}" can't be added to the sidebar as it contains a parameter.`
|
||||
)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const isSettingsPath = item.path.startsWith("/settings")
|
||||
const key = isSettingsPath ? "settingsExtensions" : "coreExtensions"
|
||||
|
||||
const navItem: INavItem = {
|
||||
label: item.label,
|
||||
to: item.path,
|
||||
icon: item.icon ? <item.icon /> : undefined,
|
||||
items: [],
|
||||
}
|
||||
|
||||
const pathParts = item.path.split("/").filter(Boolean)
|
||||
const parentPath = "/" + pathParts.slice(0, -1).join("/")
|
||||
|
||||
if (parentPath !== "/" && tempRegistry[parentPath]) {
|
||||
if (!tempRegistry[parentPath].items) {
|
||||
tempRegistry[parentPath].items = []
|
||||
}
|
||||
tempRegistry[parentPath].items!.push(navItem)
|
||||
} else {
|
||||
if (!registry.has(key)) {
|
||||
registry.set(key, [])
|
||||
}
|
||||
registry.get(key)!.push(navItem)
|
||||
}
|
||||
|
||||
tempRegistry[item.path] = navItem
|
||||
})
|
||||
|
||||
return registry
|
||||
}
|
||||
|
||||
private populateForm(formModule: FormModule): {
|
||||
fields: FormFieldMap
|
||||
configs: ConfigFieldMap
|
||||
} {
|
||||
const fields: FormFieldMap = new Map()
|
||||
const configs: ConfigFieldMap = new Map()
|
||||
|
||||
Object.entries(formModule.customFields).forEach(
|
||||
([model, customization]) => {
|
||||
fields.set(
|
||||
model as CustomFieldModel,
|
||||
this.processFields(customization.forms)
|
||||
)
|
||||
configs.set(
|
||||
model as CustomFieldModel,
|
||||
this.processConfigs(customization.configs)
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
return { fields, configs }
|
||||
}
|
||||
|
||||
private processFields(forms: FormExtension[]): FormZoneMap {
|
||||
const formZoneMap: FormZoneMap = new Map()
|
||||
|
||||
forms.forEach((fieldDef) =>
|
||||
this.processFieldDefinition(formZoneMap, fieldDef)
|
||||
)
|
||||
|
||||
return formZoneMap
|
||||
}
|
||||
|
||||
private processConfigs(
|
||||
configs: ConfigExtension[]
|
||||
): Map<CustomFieldFormZone, ConfigField[]> {
|
||||
const modelConfigMap = new Map<CustomFieldFormZone, ConfigField[]>()
|
||||
|
||||
configs.forEach((configDef) => {
|
||||
const { zone, fields } = configDef
|
||||
const zoneConfigs: ConfigField[] = []
|
||||
|
||||
Object.entries(fields).forEach(([name, config]) => {
|
||||
zoneConfigs.push({
|
||||
name,
|
||||
defaultValue: config.defaultValue,
|
||||
validation: config.validation,
|
||||
})
|
||||
})
|
||||
|
||||
modelConfigMap.set(zone, zoneConfigs)
|
||||
})
|
||||
|
||||
return modelConfigMap
|
||||
}
|
||||
|
||||
private processFieldDefinition(
|
||||
formZoneMap: FormZoneMap,
|
||||
fieldDef: FormExtension
|
||||
) {
|
||||
const { zone, tab, fields: fieldsDefinition } = fieldDef
|
||||
const zoneStructure = this.getOrCreateZoneStructure(formZoneMap, zone)
|
||||
|
||||
Object.entries(fieldsDefinition).forEach(([fieldKey, fieldDefinition]) => {
|
||||
const formField = this.createFormField(fieldKey, fieldDefinition)
|
||||
this.addFormFieldToZoneStructure(zoneStructure, formField, tab)
|
||||
})
|
||||
}
|
||||
|
||||
private getOrCreateZoneStructure(
|
||||
formZoneMap: FormZoneMap,
|
||||
zone: CustomFieldFormZone
|
||||
): ZoneStructure {
|
||||
let zoneStructure = formZoneMap.get(zone)
|
||||
if (!zoneStructure) {
|
||||
zoneStructure = { components: [], tabs: new Map() }
|
||||
formZoneMap.set(zone, zoneStructure)
|
||||
}
|
||||
return zoneStructure
|
||||
}
|
||||
|
||||
private createFormField(
|
||||
fieldKey: string,
|
||||
fieldDefinition: FormFieldExtension
|
||||
): FormField {
|
||||
return {
|
||||
name: fieldKey,
|
||||
validation: fieldDefinition.validation,
|
||||
label: fieldDefinition.label,
|
||||
description: fieldDefinition.description,
|
||||
Component: fieldDefinition.Component,
|
||||
}
|
||||
}
|
||||
|
||||
private addFormFieldToZoneStructure(
|
||||
zoneStructure: ZoneStructure,
|
||||
formField: FormField,
|
||||
tab?: CustomFieldFormTab
|
||||
) {
|
||||
if (tab) {
|
||||
let tabFields = zoneStructure.tabs.get(tab)
|
||||
if (!tabFields) {
|
||||
tabFields = []
|
||||
zoneStructure.tabs.set(tab, tabFields)
|
||||
}
|
||||
tabFields.push(formField)
|
||||
} else {
|
||||
zoneStructure.components.push(formField)
|
||||
}
|
||||
}
|
||||
|
||||
private populateDisplays(displayModule: DisplayModule): DisplayMap {
|
||||
const displays = new Map<
|
||||
CustomFieldModel,
|
||||
Map<CustomFieldContainerZone, React.ComponentType<{ data: any }>[]>
|
||||
>()
|
||||
|
||||
Object.entries(displayModule.displays).forEach(([model, customization]) => {
|
||||
displays.set(
|
||||
model as CustomFieldModel,
|
||||
this.processDisplays(customization)
|
||||
)
|
||||
})
|
||||
|
||||
return displays
|
||||
}
|
||||
|
||||
private processDisplays(
|
||||
displays: DisplayExtension[]
|
||||
): Map<CustomFieldContainerZone, React.ComponentType<{ data: any }>[]> {
|
||||
const modelDisplayMap = new Map<
|
||||
CustomFieldContainerZone,
|
||||
React.ComponentType<{ data: any }>[]
|
||||
>()
|
||||
|
||||
displays.forEach((display) => {
|
||||
const { zone, Component } = display
|
||||
if (!modelDisplayMap.has(zone)) {
|
||||
modelDisplayMap.set(zone, [])
|
||||
}
|
||||
modelDisplayMap.get(zone)!.push(Component)
|
||||
})
|
||||
|
||||
return modelDisplayMap
|
||||
}
|
||||
|
||||
private getMenu(path: MenuItemKey) {
|
||||
return this.menus.get(path) || []
|
||||
}
|
||||
|
||||
private getWidgets(zone: InjectionZone) {
|
||||
return this.widgets.get(zone) || []
|
||||
}
|
||||
|
||||
private getFormFields(
|
||||
model: CustomFieldModel,
|
||||
zone: CustomFieldFormZone,
|
||||
tab?: CustomFieldFormTab
|
||||
) {
|
||||
const zoneMap = this.fields.get(model)?.get(zone)
|
||||
|
||||
if (!zoneMap) {
|
||||
return []
|
||||
}
|
||||
|
||||
if (tab) {
|
||||
return zoneMap.tabs.get(tab) || []
|
||||
}
|
||||
|
||||
return zoneMap.components
|
||||
}
|
||||
|
||||
private getFormConfigs(model: CustomFieldModel, zone: CustomFieldFormZone) {
|
||||
return this.configs.get(model)?.get(zone) || []
|
||||
}
|
||||
|
||||
private getDisplays(model: CustomFieldModel, zone: CustomFieldContainerZone) {
|
||||
return this.displays.get(model)?.get(zone) || []
|
||||
}
|
||||
|
||||
get api() {
|
||||
return {
|
||||
getMenu: this.getMenu.bind(this),
|
||||
getWidgets: this.getWidgets.bind(this),
|
||||
getFormFields: this.getFormFields.bind(this),
|
||||
getFormConfigs: this.getFormConfigs.bind(this),
|
||||
getDisplays: this.getDisplays.bind(this),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from "./dashboard-extension-manager";
|
||||
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
import { createContext } from "react"
|
||||
import { DashboardExtensionManager } from "../dashboard-extension-manager"
|
||||
|
||||
type DasboardExtenstionContextValue = DashboardExtensionManager["api"]
|
||||
|
||||
export const DashboardExtensionContext =
|
||||
createContext<DasboardExtenstionContextValue | null>(null)
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
import { PropsWithChildren } from "react"
|
||||
import { DashboardExtensionManager } from "../dashboard-extension-manager/dashboard-extension-manager"
|
||||
import { DashboardExtensionContext } from "./dashboard-extension-context"
|
||||
|
||||
type DashboardExtensionProviderProps = PropsWithChildren<{
|
||||
api: DashboardExtensionManager["api"]
|
||||
}>
|
||||
|
||||
export const DashboardExtensionProvider = ({
|
||||
api,
|
||||
children,
|
||||
}: DashboardExtensionProviderProps) => {
|
||||
return (
|
||||
<DashboardExtensionContext.Provider value={api}>
|
||||
{children}
|
||||
</DashboardExtensionContext.Provider>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export { DashboardExtensionProvider } from "./dashboard-extension-provider"
|
||||
export { useDashboardExtension } from "./use-dashboard-extension"
|
||||
@@ -0,0 +1,32 @@
|
||||
import { InjectionZone } from "@medusajs/admin-shared"
|
||||
import { ComponentType } from "react"
|
||||
import { LoaderFunction } from "react-router-dom"
|
||||
import { CustomFieldConfiguration } from "../../extensions/custom-field-registry/types"
|
||||
|
||||
export type RouteExtension = {
|
||||
Component: ComponentType
|
||||
loader?: LoaderFunction
|
||||
path: string
|
||||
}
|
||||
|
||||
export type MenuItemExtension = {
|
||||
label: string
|
||||
path: string
|
||||
icon?: ComponentType
|
||||
}
|
||||
|
||||
export type WidgetExtension = {
|
||||
Component: ComponentType
|
||||
zone: InjectionZone[]
|
||||
}
|
||||
|
||||
export type RoutingExtensionConfig = {
|
||||
routes: RouteExtension[]
|
||||
menuItems: MenuItemExtension[]
|
||||
}
|
||||
|
||||
export type DashboardExtensionConfig = {
|
||||
customFields?: CustomFieldConfiguration
|
||||
menuItems?: MenuItemExtension[]
|
||||
widgets?: WidgetExtension[]
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
import { useContext } from "react"
|
||||
import { DashboardExtensionContext } from "./dashboard-extension-context"
|
||||
|
||||
export const useDashboardExtension = () => {
|
||||
const context = useContext(DashboardExtensionContext)
|
||||
if (!context) {
|
||||
throw new Error(
|
||||
"useDashboardExtension must be used within a DashboardExtensionProvider"
|
||||
)
|
||||
}
|
||||
return context
|
||||
}
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
import { Input, Switch } from "@medusajs/ui"
|
||||
import { ComponentType } from "react"
|
||||
import { ControllerRenderProps, UseFormReturn } from "react-hook-form"
|
||||
import { Form } from "../../../components/common/form"
|
||||
import { InlineTip } from "../../../components/common/inline-tip"
|
||||
import { FormField } from "../../types"
|
||||
import { FormFieldType } from "./types"
|
||||
import { getFieldType } from "./utils"
|
||||
|
||||
type FormExtensionZoneProps = {
|
||||
fields: FormField[]
|
||||
form: UseFormReturn<any>
|
||||
}
|
||||
|
||||
export const FormExtensionZone = ({ fields, form }: FormExtensionZoneProps) => {
|
||||
return (
|
||||
<div>
|
||||
{fields.map((field, index) => (
|
||||
<FormExtensionField key={index} field={field} form={form} />
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function getFieldLabel(field: FormField) {
|
||||
if (field.label) {
|
||||
return field.label
|
||||
}
|
||||
|
||||
return field.name
|
||||
.split("_")
|
||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join(" ")
|
||||
}
|
||||
|
||||
type FormExtensionFieldProps = {
|
||||
field: FormField
|
||||
form: UseFormReturn<any>
|
||||
}
|
||||
|
||||
const FormExtensionField = ({ field, form }: FormExtensionFieldProps) => {
|
||||
const label = getFieldLabel(field)
|
||||
const description = field.description
|
||||
const placeholder = field.placeholder
|
||||
const Component = field.Component
|
||||
|
||||
const type = getFieldType(field.validation)
|
||||
|
||||
const { control } = form
|
||||
|
||||
return (
|
||||
<Form.Field
|
||||
control={control}
|
||||
name={`additional_data.${field.name}`}
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label>{label}</Form.Label>
|
||||
{description && <Form.Hint>{description}</Form.Hint>}
|
||||
<Form.Control>
|
||||
<FormExtensionFieldComponent
|
||||
field={field}
|
||||
type={type}
|
||||
component={Component}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
type FormExtensionFieldComponentProps = {
|
||||
field: ControllerRenderProps
|
||||
type: FormFieldType
|
||||
component?: ComponentType<any>
|
||||
placeholder?: string
|
||||
}
|
||||
|
||||
const FormExtensionFieldComponent = ({
|
||||
field,
|
||||
type,
|
||||
component,
|
||||
placeholder,
|
||||
}: FormExtensionFieldComponentProps) => {
|
||||
if (component) {
|
||||
const Component = component
|
||||
|
||||
return <Component {...field} placeholder={placeholder} />
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
case "text": {
|
||||
return <Input {...field} placeholder={placeholder} />
|
||||
}
|
||||
case "number": {
|
||||
return <Input {...field} placeholder={placeholder} type="number" />
|
||||
}
|
||||
case "boolean": {
|
||||
return <Switch {...field} />
|
||||
}
|
||||
default: {
|
||||
return (
|
||||
<InlineTip variant="warning">
|
||||
The field type does not support rendering a fallback component. Please
|
||||
provide a component prop.
|
||||
</InlineTip>
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./form-extension-zone"
|
||||
@@ -0,0 +1 @@
|
||||
export type FormFieldType = "text" | "number" | "boolean" | "unsupported"
|
||||
@@ -0,0 +1,66 @@
|
||||
import {
|
||||
ZodBoolean,
|
||||
ZodEffects,
|
||||
ZodNull,
|
||||
ZodNullable,
|
||||
ZodNumber,
|
||||
ZodOptional,
|
||||
ZodString,
|
||||
ZodType,
|
||||
ZodUndefined,
|
||||
} from "zod"
|
||||
import { FormFieldType } from "./types"
|
||||
|
||||
export function getFieldLabel(name: string, label?: string) {
|
||||
if (label) {
|
||||
return label
|
||||
}
|
||||
|
||||
return name
|
||||
.split("_")
|
||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join(" ")
|
||||
}
|
||||
|
||||
export function getFieldType(type: ZodType): FormFieldType {
|
||||
if (type instanceof ZodString) {
|
||||
return "text"
|
||||
}
|
||||
|
||||
if (type instanceof ZodNumber) {
|
||||
return "number"
|
||||
}
|
||||
|
||||
if (type instanceof ZodBoolean) {
|
||||
return "boolean"
|
||||
}
|
||||
|
||||
if (type instanceof ZodNullable) {
|
||||
const innerType = type.unwrap()
|
||||
|
||||
return getFieldType(innerType)
|
||||
}
|
||||
|
||||
if (type instanceof ZodOptional) {
|
||||
const innerType = type.unwrap()
|
||||
|
||||
return getFieldType(innerType)
|
||||
}
|
||||
|
||||
if (type instanceof ZodEffects) {
|
||||
const innerType = type.innerType()
|
||||
|
||||
return getFieldType(innerType)
|
||||
}
|
||||
|
||||
return "unsupported"
|
||||
}
|
||||
|
||||
export function getIsFieldOptional(type: ZodType) {
|
||||
return (
|
||||
type instanceof ZodOptional ||
|
||||
type instanceof ZodNull ||
|
||||
type instanceof ZodUndefined ||
|
||||
type instanceof ZodNullable
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { FieldValues, useForm, UseFormProps } from "react-hook-form"
|
||||
import { z, ZodEffects, ZodObject } from "zod"
|
||||
|
||||
import { ConfigField } from "../types"
|
||||
|
||||
interface UseExtendableFormProps<
|
||||
TSchema extends ZodObject<any> | ZodEffects<ZodObject<any>>,
|
||||
TContext = any,
|
||||
TData = any
|
||||
> extends Omit<UseFormProps<z.infer<TSchema>, TContext>, "resolver"> {
|
||||
schema: TSchema
|
||||
configs: ConfigField[]
|
||||
data?: TData
|
||||
}
|
||||
|
||||
function createAdditionalDataSchema(configs: ConfigField[]) {
|
||||
return configs.reduce((acc, config) => {
|
||||
acc[config.name] = config.validation
|
||||
return acc
|
||||
}, {} as Record<string, z.ZodTypeAny>)
|
||||
}
|
||||
|
||||
function createExtendedSchema<
|
||||
TSchema extends ZodObject<any> | ZodEffects<ZodObject<any>>
|
||||
>(baseSchema: TSchema, additionalDataSchema: Record<string, z.ZodTypeAny>) {
|
||||
const extendedObjectSchema = z.object({
|
||||
...(baseSchema instanceof ZodEffects
|
||||
? baseSchema.innerType().shape
|
||||
: baseSchema.shape),
|
||||
additional_data: z.object(additionalDataSchema).optional(),
|
||||
})
|
||||
|
||||
return baseSchema instanceof ZodEffects
|
||||
? baseSchema
|
||||
.superRefine((data, ctx) => {
|
||||
const result = extendedObjectSchema.safeParse(data)
|
||||
if (!result.success) {
|
||||
result.error.issues.forEach((issue) => ctx.addIssue(issue))
|
||||
}
|
||||
})
|
||||
.and(extendedObjectSchema)
|
||||
: extendedObjectSchema
|
||||
}
|
||||
|
||||
function createExtendedDefaultValues<TData>(
|
||||
baseDefaultValues: any,
|
||||
configs: ConfigField[],
|
||||
data?: TData
|
||||
) {
|
||||
const additional_data = configs.reduce((acc, config) => {
|
||||
const { name, defaultValue } = config
|
||||
|
||||
acc[name] =
|
||||
typeof defaultValue === "function" ? defaultValue(data) : defaultValue
|
||||
return acc
|
||||
}, {} as Record<string, any>)
|
||||
|
||||
return Object.assign(baseDefaultValues, { additional_data })
|
||||
}
|
||||
|
||||
export const useExtendableForm = <
|
||||
TSchema extends ZodObject<any> | ZodEffects<ZodObject<any>>,
|
||||
TContext = any,
|
||||
TTransformedValues extends FieldValues | undefined = undefined
|
||||
>({
|
||||
defaultValues: baseDefaultValues,
|
||||
schema: baseSchema,
|
||||
configs,
|
||||
data,
|
||||
...props
|
||||
}: UseExtendableFormProps<TSchema, TContext>) => {
|
||||
const additionalDataSchema = createAdditionalDataSchema(configs)
|
||||
const schema = createExtendedSchema(baseSchema, additionalDataSchema)
|
||||
const defaultValues = createExtendedDefaultValues(
|
||||
baseDefaultValues,
|
||||
configs,
|
||||
data
|
||||
)
|
||||
|
||||
return useForm<z.infer<TSchema>, TContext, TTransformedValues>({
|
||||
...props,
|
||||
defaultValues,
|
||||
resolver: zodResolver(schema),
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from "./form-extension-zone"
|
||||
export * from "./hooks"
|
||||
@@ -0,0 +1,13 @@
|
||||
export * from "./dashboard-extension-manager"
|
||||
export * from "./dashboard-extension-provider"
|
||||
export * from "./forms"
|
||||
export * from "./links/utils"
|
||||
export * from "./routes/utils"
|
||||
|
||||
export {
|
||||
type DisplayModule,
|
||||
type FormModule,
|
||||
type MenuItemModule,
|
||||
type RouteModule,
|
||||
type WidgetModule,
|
||||
} from "./types"
|
||||
@@ -0,0 +1,20 @@
|
||||
import { CustomFieldModel } from "@medusajs/admin-shared"
|
||||
import linkModule from "virtual:medusa/links"
|
||||
|
||||
function appendLinkableFields(
|
||||
fields: string = "",
|
||||
linkable: (string | string[])[] = []
|
||||
) {
|
||||
const linkableFields = linkable.flatMap((link) => {
|
||||
return typeof link === "string"
|
||||
? [`+${link}.*`]
|
||||
: link.map((l) => `+${l}.*`)
|
||||
})
|
||||
|
||||
return [fields, ...linkableFields].join(",")
|
||||
}
|
||||
|
||||
export function getLinkedFields(model: CustomFieldModel, fields: string = "") {
|
||||
const links = linkModule.links[model]
|
||||
return appendLinkableFields(fields, links)
|
||||
}
|
||||
+29
-9
@@ -1,21 +1,37 @@
|
||||
import { RouteObject } from "react-router-dom"
|
||||
import { ErrorBoundary } from "../components/utilities/error-boundary"
|
||||
import { ComponentType } from "react"
|
||||
import { LoaderFunction, RouteObject } from "react-router-dom"
|
||||
import { ErrorBoundary } from "../../components/utilities/error-boundary"
|
||||
import { RouteExtension, RouteModule } from "../types"
|
||||
|
||||
/**
|
||||
* Used to test if a route is a settings route.
|
||||
*/
|
||||
export const settingsRouteRegex = /^\/settings\//
|
||||
const settingsRouteRegex = /^\/settings\//
|
||||
|
||||
export const getRouteExtensions = (
|
||||
module: RouteModule,
|
||||
type: "settings" | "core"
|
||||
) => {
|
||||
return module.routes.filter((route) => {
|
||||
if (type === "settings") {
|
||||
return settingsRouteRegex.test(route.path)
|
||||
}
|
||||
|
||||
return !settingsRouteRegex.test(route.path)
|
||||
})
|
||||
}
|
||||
|
||||
export const createRouteMap = (
|
||||
routes: { path: string; Component: () => JSX.Element }[],
|
||||
routes: RouteExtension[],
|
||||
ignore?: string
|
||||
): RouteObject[] => {
|
||||
const root: RouteObject[] = []
|
||||
|
||||
const addRoute = (
|
||||
pathSegments: string[],
|
||||
Component: () => JSX.Element,
|
||||
currentLevel: RouteObject[]
|
||||
Component: ComponentType,
|
||||
currentLevel: RouteObject[],
|
||||
loader?: LoaderFunction
|
||||
) => {
|
||||
if (!pathSegments.length) {
|
||||
return
|
||||
@@ -35,22 +51,26 @@ export const createRouteMap = (
|
||||
path: "",
|
||||
ErrorBoundary: ErrorBoundary,
|
||||
async lazy() {
|
||||
if (loader) {
|
||||
return { Component, loader }
|
||||
}
|
||||
|
||||
return { Component }
|
||||
},
|
||||
})
|
||||
} else {
|
||||
route.children ||= []
|
||||
addRoute(remainingSegments, Component, route.children)
|
||||
addRoute(remainingSegments, Component, route.children, loader)
|
||||
}
|
||||
}
|
||||
|
||||
routes.forEach(({ path, Component }) => {
|
||||
routes.forEach(({ path, Component, loader }) => {
|
||||
// Remove the ignore segment from the path if it is provided
|
||||
const cleanedPath = ignore
|
||||
? path.replace(ignore, "").replace(/^\/+/, "")
|
||||
: path.replace(/^\/+/, "")
|
||||
const pathSegments = cleanedPath.split("/").filter(Boolean)
|
||||
addRoute(pathSegments, Component, root)
|
||||
addRoute(pathSegments, Component, root, loader)
|
||||
})
|
||||
|
||||
return root
|
||||
@@ -0,0 +1,117 @@
|
||||
import {
|
||||
CustomFieldContainerZone,
|
||||
CustomFieldFormTab,
|
||||
CustomFieldFormZone,
|
||||
CustomFieldModel,
|
||||
InjectionZone,
|
||||
} from "@medusajs/admin-shared"
|
||||
import { ComponentType } from "react"
|
||||
import { LoaderFunction } from "react-router-dom"
|
||||
import { ZodFirstPartySchemaTypes } from "zod"
|
||||
|
||||
export type RouteExtension = {
|
||||
Component: ComponentType
|
||||
loader?: LoaderFunction
|
||||
path: string
|
||||
}
|
||||
|
||||
export type MenuItemExtension = {
|
||||
label: string
|
||||
path: string
|
||||
icon?: ComponentType
|
||||
}
|
||||
|
||||
export type WidgetExtension = {
|
||||
Component: ComponentType
|
||||
zone: InjectionZone[]
|
||||
}
|
||||
|
||||
export type DisplayExtension = {
|
||||
Component: ComponentType<{ data: any }>
|
||||
zone: CustomFieldContainerZone
|
||||
}
|
||||
|
||||
export type FormFieldExtension = {
|
||||
validation: ZodFirstPartySchemaTypes
|
||||
Component?: ComponentType<any>
|
||||
label?: string
|
||||
description?: string
|
||||
placeholder?: string
|
||||
}
|
||||
|
||||
export type FormExtension = {
|
||||
zone: CustomFieldFormZone
|
||||
tab?: CustomFieldFormTab
|
||||
fields: Record<string, FormFieldExtension>
|
||||
}
|
||||
|
||||
export type ConfigFieldExtension = {
|
||||
defaultValue: ((data: any) => any) | any
|
||||
validation: ZodFirstPartySchemaTypes
|
||||
}
|
||||
|
||||
export type ConfigExtension = {
|
||||
zone: CustomFieldFormZone
|
||||
fields: Record<string, ConfigFieldExtension>
|
||||
}
|
||||
|
||||
export type LinkModule = {
|
||||
links: Record<CustomFieldModel, (string | string[])[]>
|
||||
}
|
||||
|
||||
export type DisplayModule = {
|
||||
displays: Record<CustomFieldModel, DisplayExtension[]>
|
||||
}
|
||||
|
||||
export type FormModule = {
|
||||
customFields: Record<
|
||||
CustomFieldModel,
|
||||
{
|
||||
forms: FormExtension[]
|
||||
configs: ConfigExtension[]
|
||||
}
|
||||
>
|
||||
}
|
||||
|
||||
export type WidgetModule = {
|
||||
widgets: WidgetExtension[]
|
||||
}
|
||||
|
||||
export type RouteModule = {
|
||||
routes: RouteExtension[]
|
||||
}
|
||||
|
||||
export type MenuItemModule = {
|
||||
menuItems: MenuItemExtension[]
|
||||
}
|
||||
|
||||
export type MenuItemKey = "coreExtensions" | "settingsExtensions"
|
||||
|
||||
export type FormField = FormFieldExtension & {
|
||||
name: string
|
||||
}
|
||||
|
||||
export type TabFieldMap = Map<CustomFieldFormTab, FormField[]>
|
||||
|
||||
export type ZoneStructure = {
|
||||
components: FormField[]
|
||||
tabs: TabFieldMap
|
||||
}
|
||||
|
||||
export type FormZoneMap = Map<CustomFieldFormZone, ZoneStructure>
|
||||
|
||||
export type FormFieldMap = Map<CustomFieldModel, FormZoneMap>
|
||||
|
||||
export type ConfigField = ConfigFieldExtension & {
|
||||
name: string
|
||||
}
|
||||
|
||||
export type ConfigFieldMap = Map<
|
||||
CustomFieldModel,
|
||||
Map<CustomFieldFormZone, ConfigField[]>
|
||||
>
|
||||
|
||||
export type DisplayMap = Map<
|
||||
CustomFieldModel,
|
||||
Map<CustomFieldContainerZone, React.ComponentType<{ data: any }>[]>
|
||||
>
|
||||
@@ -19,6 +19,7 @@ export const customerGroupsQueryKeys = queryKeysFactory(
|
||||
|
||||
export const useCustomerGroup = (
|
||||
id: string,
|
||||
query?: HttpTypes.AdminGetCustomerGroupParams,
|
||||
options?: Omit<
|
||||
UseQueryOptions<
|
||||
HttpTypes.AdminCustomerGroupResponse,
|
||||
@@ -30,8 +31,8 @@ export const useCustomerGroup = (
|
||||
>
|
||||
) => {
|
||||
const { data, ...rest } = useQuery({
|
||||
queryKey: customerGroupsQueryKeys.detail(id),
|
||||
queryFn: async () => sdk.admin.customerGroup.retrieve(id),
|
||||
queryKey: customerGroupsQueryKeys.detail(id, query),
|
||||
queryFn: async () => sdk.admin.customerGroup.retrieve(id, query),
|
||||
...options,
|
||||
})
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./render"
|
||||
+28
-20
@@ -1,27 +1,35 @@
|
||||
declare module "virtual:medusa/widgets/*" {
|
||||
import type { ComponentType } from "react"
|
||||
|
||||
const widgets: { Component: ComponentType<any> }[]
|
||||
|
||||
export default {
|
||||
widgets,
|
||||
}
|
||||
declare module "virtual:medusa/forms" {
|
||||
import type { FormModule } from "./extensions"
|
||||
const formModule: FormModule
|
||||
export default formModule
|
||||
}
|
||||
|
||||
declare module "virtual:medusa/routes/pages" {
|
||||
const pages: { path: string; Component: () => JSX.Element }[]
|
||||
|
||||
export default {
|
||||
pages,
|
||||
}
|
||||
declare module "virtual:medusa/links" {
|
||||
import type { LinkModule } from "./extensions"
|
||||
const linkModule: LinkModule
|
||||
export default linkModule
|
||||
}
|
||||
|
||||
declare module "virtual:medusa/routes/links" {
|
||||
import type { ComponentType } from "react"
|
||||
declare module "virtual:medusa/displays" {
|
||||
import type { DisplayModule } from "./extensions"
|
||||
const displayModule: DisplayModule
|
||||
export default displayModule
|
||||
}
|
||||
|
||||
const links: { path: string; label: string; icon?: ComponentType }[]
|
||||
declare module "virtual:medusa/routes" {
|
||||
import type { RouteModule } from "./extensions"
|
||||
const routeModule: RouteModule
|
||||
export default routeModule
|
||||
}
|
||||
|
||||
export default {
|
||||
links,
|
||||
}
|
||||
declare module "virtual:medusa/menu-items" {
|
||||
import type { MenuItemModule } from "./extensions"
|
||||
const menuItemModule: MenuItemModule
|
||||
export default menuItemModule
|
||||
}
|
||||
|
||||
declare module "virtual:medusa/widgets" {
|
||||
import type { WidgetModule } from "./extensions"
|
||||
const widgetModule: WidgetModule
|
||||
export default widgetModule
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./providers"
|
||||
@@ -0,0 +1,35 @@
|
||||
import { Toaster, TooltipProvider } from "@medusajs/ui"
|
||||
import { QueryClientProvider } from "@tanstack/react-query"
|
||||
import type { PropsWithChildren } from "react"
|
||||
import { HelmetProvider } from "react-helmet-async"
|
||||
|
||||
import { I18n } from "../components/utilities/i18n"
|
||||
import {
|
||||
DashboardExtensionManager,
|
||||
DashboardExtensionProvider,
|
||||
} from "../extensions"
|
||||
import { queryClient } from "../lib/query-client"
|
||||
import { I18nProvider } from "./i18n-provider"
|
||||
import { ThemeProvider } from "./theme-provider"
|
||||
|
||||
type ProvidersProps = PropsWithChildren<{
|
||||
api: DashboardExtensionManager["api"]
|
||||
}>
|
||||
|
||||
export const Providers = ({ api, children }: ProvidersProps) => {
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<DashboardExtensionProvider api={api}>
|
||||
<HelmetProvider>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ThemeProvider>
|
||||
<I18n />
|
||||
<I18nProvider>{children}</I18nProvider>
|
||||
<Toaster />
|
||||
</ThemeProvider>
|
||||
</QueryClientProvider>
|
||||
</HelmetProvider>
|
||||
</DashboardExtensionProvider>
|
||||
</TooltipProvider>
|
||||
)
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
import routes from "virtual:medusa/routes/pages"
|
||||
import routeModule from "virtual:medusa/routes"
|
||||
import {
|
||||
createRouteMap,
|
||||
getRouteExtensions,
|
||||
} from "../../extensions/routes/utils"
|
||||
|
||||
import { createRouteMap, settingsRouteRegex } from "../../lib/extension-helpers"
|
||||
|
||||
const pages = routes.pages
|
||||
.filter((ext) => !settingsRouteRegex.test(ext.path))
|
||||
.map((ext) => ext)
|
||||
const routes = getRouteExtensions(routeModule, "core")
|
||||
|
||||
/**
|
||||
* Core Route extensions.
|
||||
*/
|
||||
export const RouteExtensions = createRouteMap(pages)
|
||||
export const RouteExtensions = createRouteMap(routes)
|
||||
@@ -1,15 +1,11 @@
|
||||
import {
|
||||
AdminProductCategoryResponse,
|
||||
AdminTaxRegionResponse,
|
||||
HttpTypes,
|
||||
} from "@medusajs/types"
|
||||
import { AdminProductCategoryResponse, HttpTypes } from "@medusajs/types"
|
||||
import { Outlet, RouteObject } from "react-router-dom"
|
||||
|
||||
import { ProtectedRoute } from "../../components/authentication/protected-route"
|
||||
import { MainLayout } from "../../components/layout/main-layout"
|
||||
import { PublicLayout } from "../../components/layout/public-layout"
|
||||
import { SettingsLayout } from "../../components/layout/settings-layout"
|
||||
import { ErrorBoundary } from "../../components/utilities/error-boundary"
|
||||
|
||||
import { getCountryByIso2 } from "../../lib/data/countries"
|
||||
import {
|
||||
getProvinceByIso2,
|
||||
@@ -22,32 +18,14 @@ import { SettingsExtensions } from "./settings-extensions"
|
||||
|
||||
// TODO: Add translations for all breadcrumbs
|
||||
export const RouteMap: RouteObject[] = [
|
||||
{
|
||||
path: "/login",
|
||||
lazy: () => import("../../routes/login"),
|
||||
},
|
||||
{
|
||||
path: "/reset-password",
|
||||
lazy: () => import("../../routes/reset-password"),
|
||||
},
|
||||
{
|
||||
path: "*",
|
||||
lazy: () => import("../../routes/no-match"),
|
||||
},
|
||||
{
|
||||
path: "/invite",
|
||||
lazy: () => import("../../routes/invite"),
|
||||
},
|
||||
{
|
||||
element: <ProtectedRoute />,
|
||||
errorElement: <ErrorBoundary />,
|
||||
children: [
|
||||
{
|
||||
path: "/",
|
||||
element: <MainLayout />,
|
||||
children: [
|
||||
{
|
||||
index: true,
|
||||
path: "/",
|
||||
errorElement: <ErrorBoundary />,
|
||||
lazy: () => import("../../routes/home"),
|
||||
},
|
||||
@@ -677,6 +655,11 @@ export const RouteMap: RouteObject[] = [
|
||||
"../../routes/inventory/inventory-detail/components/edit-inventory-item-attributes"
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "metadata/edit",
|
||||
lazy: () =>
|
||||
import("../../routes/inventory/inventory-metadata"),
|
||||
},
|
||||
{
|
||||
path: "locations",
|
||||
lazy: () =>
|
||||
@@ -691,21 +674,6 @@ export const RouteMap: RouteObject[] = [
|
||||
"../../routes/inventory/inventory-detail/components/adjust-inventory"
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "metadata/edit",
|
||||
lazy: () =>
|
||||
import("../../routes/inventory/inventory-metadata"),
|
||||
},
|
||||
{
|
||||
// TODO: create reservation
|
||||
path: "reservations",
|
||||
lazy: () => import("../../routes/customers/customer-edit"),
|
||||
},
|
||||
{
|
||||
// TODO: edit reservation
|
||||
path: "reservations/:reservation_id",
|
||||
lazy: () => import("../../routes/customers/customer-edit"),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
@@ -780,10 +748,6 @@ export const RouteMap: RouteObject[] = [
|
||||
lazy: () =>
|
||||
import("../../routes/regions/region-add-countries"),
|
||||
},
|
||||
{
|
||||
path: "metadata/edit",
|
||||
lazy: () => import("../../routes/regions/region-metadata"),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
@@ -1160,10 +1124,8 @@ export const RouteMap: RouteObject[] = [
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
path: "publishable-api-keys",
|
||||
errorElement: <ErrorBoundary />,
|
||||
element: <Outlet />,
|
||||
handle: {
|
||||
crumb: () => "Publishable API Keys",
|
||||
@@ -1223,7 +1185,6 @@ export const RouteMap: RouteObject[] = [
|
||||
},
|
||||
{
|
||||
path: "secret-api-keys",
|
||||
errorElement: <ErrorBoundary />,
|
||||
element: <Outlet />,
|
||||
handle: {
|
||||
crumb: () => "Secret API Keys",
|
||||
@@ -1276,7 +1237,6 @@ export const RouteMap: RouteObject[] = [
|
||||
},
|
||||
{
|
||||
path: "tax-regions",
|
||||
errorElement: <ErrorBoundary />,
|
||||
element: <Outlet />,
|
||||
handle: {
|
||||
crumb: () => "Tax Regions",
|
||||
@@ -1298,7 +1258,7 @@ export const RouteMap: RouteObject[] = [
|
||||
Component: Outlet,
|
||||
loader: taxRegionLoader,
|
||||
handle: {
|
||||
crumb: (data: AdminTaxRegionResponse) => {
|
||||
crumb: (data: HttpTypes.AdminTaxRegionResponse) => {
|
||||
return (
|
||||
getCountryByIso2(data.tax_region.country_code)
|
||||
?.display_name ||
|
||||
@@ -1356,7 +1316,7 @@ export const RouteMap: RouteObject[] = [
|
||||
"../../routes/tax-regions/tax-region-province-detail"
|
||||
),
|
||||
handle: {
|
||||
crumb: (data: AdminTaxRegionResponse) => {
|
||||
crumb: (data: HttpTypes.AdminTaxRegionResponse) => {
|
||||
const countryCode =
|
||||
data.tax_region.country_code?.toUpperCase()
|
||||
const provinceCode =
|
||||
@@ -1395,7 +1355,6 @@ export const RouteMap: RouteObject[] = [
|
||||
},
|
||||
{
|
||||
path: "return-reasons",
|
||||
errorElement: <ErrorBoundary />,
|
||||
element: <Outlet />,
|
||||
handle: {
|
||||
crumb: () => "Return Reasons",
|
||||
@@ -1435,4 +1394,30 @@ export const RouteMap: RouteObject[] = [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
element: <PublicLayout />,
|
||||
children: [
|
||||
{
|
||||
errorElement: <ErrorBoundary />,
|
||||
children: [
|
||||
{
|
||||
path: "/login",
|
||||
lazy: () => import("../../routes/login"),
|
||||
},
|
||||
{
|
||||
path: "/reset-password",
|
||||
lazy: () => import("../../routes/reset-password"),
|
||||
},
|
||||
{
|
||||
path: "/invite",
|
||||
lazy: () => import("../../routes/invite"),
|
||||
},
|
||||
{
|
||||
path: "*",
|
||||
lazy: () => import("../../routes/no-match"),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
@@ -1,12 +1,12 @@
|
||||
import routes from "virtual:medusa/routes/pages"
|
||||
import routeModule from "virtual:medusa/routes"
|
||||
import {
|
||||
createRouteMap,
|
||||
getRouteExtensions,
|
||||
} from "../../extensions/routes/utils"
|
||||
|
||||
import { createRouteMap, settingsRouteRegex } from "../../lib/extension-helpers"
|
||||
|
||||
const pages = routes.pages
|
||||
.filter((ext) => settingsRouteRegex.test(ext.path))
|
||||
.map((ext) => ext)
|
||||
const routes = getRouteExtensions(routeModule, "settings")
|
||||
|
||||
/**
|
||||
* Settings Route extensions.
|
||||
*/
|
||||
export const SettingsExtensions = createRouteMap(pages, "/settings")
|
||||
export const SettingsExtensions = createRouteMap(routes, "/settings")
|
||||
+17
-36
@@ -1,39 +1,28 @@
|
||||
import { Outlet, useLoaderData, useParams } from "react-router-dom"
|
||||
import { useLoaderData, useParams } from "react-router-dom"
|
||||
|
||||
import { JsonViewSection } from "../../../components/common/json-view-section"
|
||||
import {
|
||||
GeneralSectionSkeleton,
|
||||
JsonViewSectionSkeleton,
|
||||
TableSectionSkeleton,
|
||||
} from "../../../components/common/skeleton"
|
||||
import { SingleColumnPageSkeleton } from "../../../components/common/skeleton"
|
||||
import { SingleColumnPage } from "../../../components/layout/pages"
|
||||
import { useDashboardExtension } from "../../../extensions"
|
||||
import { useApiKey } from "../../../hooks/api/api-keys"
|
||||
import { ApiKeyType } from "../common/constants"
|
||||
import { ApiKeyGeneralSection } from "./components/api-key-general-section"
|
||||
import { ApiKeySalesChannelSection } from "./components/api-key-sales-channel-section"
|
||||
import { apiKeyLoader } from "./loader"
|
||||
|
||||
import after from "virtual:medusa/widgets/api_key/details/after"
|
||||
import before from "virtual:medusa/widgets/api_key/details/before"
|
||||
|
||||
export const ApiKeyManagementDetail = () => {
|
||||
const initialData = useLoaderData() as Awaited<
|
||||
ReturnType<typeof apiKeyLoader>
|
||||
>
|
||||
|
||||
const { id } = useParams()
|
||||
const { getWidgets } = useDashboardExtension()
|
||||
|
||||
const { api_key, isLoading, isError, error } = useApiKey(id!, undefined, {
|
||||
const { api_key, isLoading, isError, error } = useApiKey(id!, {
|
||||
initialData: initialData,
|
||||
})
|
||||
|
||||
if (isLoading || !api_key) {
|
||||
return (
|
||||
<div className="flex flex-col gap-y-2">
|
||||
<GeneralSectionSkeleton rowCount={4} />
|
||||
<TableSectionSkeleton />
|
||||
<JsonViewSectionSkeleton />
|
||||
</div>
|
||||
)
|
||||
return <SingleColumnPageSkeleton showJSON sections={1} />
|
||||
}
|
||||
|
||||
const isPublishable = api_key?.type === ApiKeyType.PUBLISHABLE
|
||||
@@ -43,25 +32,17 @@ export const ApiKeyManagementDetail = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-y-2">
|
||||
{before.widgets.map((w, i) => {
|
||||
return (
|
||||
<div key={i}>
|
||||
<w.Component apiKey={api_key} />
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
<SingleColumnPage
|
||||
hasOutlet
|
||||
showJSON
|
||||
widgets={{
|
||||
before: getWidgets("api_key.details.before"),
|
||||
after: getWidgets("api_key.details.after"),
|
||||
}}
|
||||
data={api_key}
|
||||
>
|
||||
<ApiKeyGeneralSection apiKey={api_key} />
|
||||
{isPublishable && <ApiKeySalesChannelSection apiKey={api_key} />}
|
||||
{after.widgets.map((w, i) => {
|
||||
return (
|
||||
<div key={i}>
|
||||
<w.Component apiKey={api_key} />
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
<JsonViewSection data={api_key} />
|
||||
<Outlet />
|
||||
</div>
|
||||
</SingleColumnPage>
|
||||
)
|
||||
}
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
import { LoaderFunctionArgs } from "react-router-dom"
|
||||
|
||||
import { AdminApiKeyResponse } from "@medusajs/types"
|
||||
import { HttpTypes } from "@medusajs/types"
|
||||
import { apiKeysQueryKeys } from "../../../hooks/api/api-keys"
|
||||
import { sdk } from "../../../lib/client"
|
||||
import { queryClient } from "../../../lib/query-client"
|
||||
@@ -15,7 +15,7 @@ export const apiKeyLoader = async ({ params }: LoaderFunctionArgs) => {
|
||||
const query = apiKeyDetailQuery(id!)
|
||||
|
||||
return (
|
||||
queryClient.getQueryData<AdminApiKeyResponse>(query.queryKey) ??
|
||||
queryClient.getQueryData<HttpTypes.AdminApiKeyResponse>(query.queryKey) ??
|
||||
(await queryClient.fetchQuery(query))
|
||||
)
|
||||
}
|
||||
+12
-20
@@ -1,33 +1,25 @@
|
||||
import { Outlet, useLocation } from "react-router-dom"
|
||||
import { useLocation } from "react-router-dom"
|
||||
import { getApiKeyTypeFromPathname } from "../common/utils"
|
||||
import { ApiKeyManagementListTable } from "./components/api-key-management-list-table"
|
||||
|
||||
import after from "virtual:medusa/widgets/api_key/list/after"
|
||||
import before from "virtual:medusa/widgets/api_key/list/before"
|
||||
import { SingleColumnPage } from "../../../components/layout/pages"
|
||||
import { useDashboardExtension } from "../../../extensions"
|
||||
|
||||
export const ApiKeyManagementList = () => {
|
||||
const { pathname } = useLocation()
|
||||
const { getWidgets } = useDashboardExtension()
|
||||
|
||||
const keyType = getApiKeyTypeFromPathname(pathname)
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-y-2">
|
||||
{before.widgets.map((w, i) => {
|
||||
return (
|
||||
<div key={i}>
|
||||
<w.Component />
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
<SingleColumnPage
|
||||
hasOutlet
|
||||
widgets={{
|
||||
before: getWidgets("api_key.list.before"),
|
||||
after: getWidgets("api_key.list.after"),
|
||||
}}
|
||||
>
|
||||
<ApiKeyManagementListTable keyType={keyType} />
|
||||
{after.widgets.map((w, i) => {
|
||||
return (
|
||||
<div key={i}>
|
||||
<w.Component />
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
<Outlet />
|
||||
</div>
|
||||
</SingleColumnPage>
|
||||
)
|
||||
}
|
||||
@@ -7,12 +7,9 @@ import { CampaignPromotionSection } from "./components/campaign-promotion-sectio
|
||||
import { CampaignSpend } from "./components/campaign-spend"
|
||||
import { campaignLoader } from "./loader"
|
||||
|
||||
import after from "virtual:medusa/widgets/campaign/details/after"
|
||||
import before from "virtual:medusa/widgets/campaign/details/before"
|
||||
import sideAfter from "virtual:medusa/widgets/campaign/details/side/after"
|
||||
import sideBefore from "virtual:medusa/widgets/campaign/details/side/before"
|
||||
import { TwoColumnPageSkeleton } from "../../../components/common/skeleton"
|
||||
import { TwoColumnPage } from "../../../components/layout/pages"
|
||||
import { useDashboardExtension } from "../../../extensions"
|
||||
import { CampaignConfigurationSection } from "./components/campaign-configuration-section"
|
||||
|
||||
export const CampaignDetail = () => {
|
||||
@@ -27,6 +24,8 @@ export const CampaignDetail = () => {
|
||||
{ initialData }
|
||||
)
|
||||
|
||||
const { getWidgets } = useDashboardExtension()
|
||||
|
||||
if (isLoading || !campaign) {
|
||||
return (
|
||||
<TwoColumnPageSkeleton
|
||||
@@ -45,10 +44,10 @@ export const CampaignDetail = () => {
|
||||
return (
|
||||
<TwoColumnPage
|
||||
widgets={{
|
||||
after,
|
||||
before,
|
||||
sideAfter,
|
||||
sideBefore,
|
||||
after: getWidgets("campaign.details.after"),
|
||||
before: getWidgets("campaign.details.before"),
|
||||
sideAfter: getWidgets("campaign.details.side.after"),
|
||||
sideBefore: getWidgets("campaign.details.side.before"),
|
||||
}}
|
||||
hasOutlet
|
||||
showJSON
|
||||
|
||||
@@ -1,29 +1,19 @@
|
||||
import { Outlet } from "react-router-dom"
|
||||
|
||||
import { SingleColumnPage } from "../../../components/layout/pages"
|
||||
import { useDashboardExtension } from "../../../extensions"
|
||||
import { CampaignListTable } from "./components/campaign-list-table"
|
||||
|
||||
import after from "virtual:medusa/widgets/campaign/list/after"
|
||||
import before from "virtual:medusa/widgets/campaign/list/before"
|
||||
|
||||
export const CampaignList = () => {
|
||||
const { getWidgets } = useDashboardExtension()
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-y-2">
|
||||
{before.widgets.map((w, i) => {
|
||||
return (
|
||||
<div key={i}>
|
||||
<w.Component />
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
<SingleColumnPage
|
||||
widgets={{
|
||||
after: getWidgets("campaign.list.after"),
|
||||
before: getWidgets("campaign.list.before"),
|
||||
}}
|
||||
hasOutlet
|
||||
>
|
||||
<CampaignListTable />
|
||||
{after.widgets.map((w, i) => {
|
||||
return (
|
||||
<div key={i}>
|
||||
<w.Component />
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
<Outlet />
|
||||
</div>
|
||||
</SingleColumnPage>
|
||||
)
|
||||
}
|
||||
+17
-39
@@ -5,11 +5,9 @@ import { CategoryOrganizeSection } from "./components/category-organize-section"
|
||||
import { CategoryProductSection } from "./components/category-product-section"
|
||||
import { categoryLoader } from "./loader"
|
||||
|
||||
import after from "virtual:medusa/widgets/product_category/details/after"
|
||||
import before from "virtual:medusa/widgets/product_category/details/before"
|
||||
import sideAfter from "virtual:medusa/widgets/product_category/details/side/after"
|
||||
import sideBefore from "virtual:medusa/widgets/product_category/details/side/before"
|
||||
import { TwoColumnPageSkeleton } from "../../../components/common/skeleton"
|
||||
import { TwoColumnPage } from "../../../components/layout/pages"
|
||||
import { useDashboardExtension } from "../../../extensions"
|
||||
|
||||
export const CategoryDetail = () => {
|
||||
const { id } = useParams()
|
||||
@@ -18,6 +16,8 @@ export const CategoryDetail = () => {
|
||||
ReturnType<typeof categoryLoader>
|
||||
>
|
||||
|
||||
const { getWidgets } = useDashboardExtension()
|
||||
|
||||
const { product_category, isLoading, isError, error } = useProductCategory(
|
||||
id!,
|
||||
undefined,
|
||||
@@ -27,7 +27,14 @@ export const CategoryDetail = () => {
|
||||
)
|
||||
|
||||
if (isLoading || !product_category) {
|
||||
return <div>Loading...</div>
|
||||
return (
|
||||
<TwoColumnPageSkeleton
|
||||
mainSections={2}
|
||||
sidebarSections={1}
|
||||
showJSON
|
||||
showMetadata
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
@@ -37,50 +44,21 @@ export const CategoryDetail = () => {
|
||||
return (
|
||||
<TwoColumnPage
|
||||
widgets={{
|
||||
after,
|
||||
before,
|
||||
sideAfter,
|
||||
sideBefore,
|
||||
after: getWidgets("product_category.details.after"),
|
||||
before: getWidgets("product_category.details.before"),
|
||||
sideAfter: getWidgets("product_category.details.side.after"),
|
||||
sideBefore: getWidgets("product_category.details.side.before"),
|
||||
}}
|
||||
data={product_category}
|
||||
showJSON
|
||||
showMetadata
|
||||
hasOutlet
|
||||
data={product_category}
|
||||
>
|
||||
{before.widgets.map((w, i) => {
|
||||
return (
|
||||
<div key={i}>
|
||||
<w.Component data={product_category} />
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
<TwoColumnPage.Main>
|
||||
<CategoryGeneralSection category={product_category} />
|
||||
<CategoryProductSection category={product_category} />
|
||||
{after.widgets.map((w, i) => {
|
||||
return (
|
||||
<div key={i}>
|
||||
<w.Component data={product_category} />
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</TwoColumnPage.Main>
|
||||
<TwoColumnPage.Sidebar>
|
||||
{sideBefore.widgets.map((w, i) => {
|
||||
return (
|
||||
<div key={i}>
|
||||
<w.Component data={product_category} />
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
<CategoryOrganizeSection category={product_category} />
|
||||
{sideAfter.widgets.map((w, i) => {
|
||||
return (
|
||||
<div key={i}>
|
||||
<w.Component data={product_category} />
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</TwoColumnPage.Sidebar>
|
||||
</TwoColumnPage>
|
||||
)
|
||||
|
||||
@@ -1,28 +1,19 @@
|
||||
import { Outlet } from "react-router-dom"
|
||||
import { SingleColumnPage } from "../../../components/layout/pages"
|
||||
import { useDashboardExtension } from "../../../extensions"
|
||||
import { CategoryListTable } from "./components/category-list-table"
|
||||
|
||||
import after from "virtual:medusa/widgets/product_category/list/after"
|
||||
import before from "virtual:medusa/widgets/product_category/list/before"
|
||||
|
||||
export const CategoryList = () => {
|
||||
const { getWidgets } = useDashboardExtension()
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-y-2">
|
||||
{before.widgets.map((w, i) => {
|
||||
return (
|
||||
<div key={i}>
|
||||
<w.Component />
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
<SingleColumnPage
|
||||
widgets={{
|
||||
after: getWidgets("product_category.list.after"),
|
||||
before: getWidgets("product_category.list.before"),
|
||||
}}
|
||||
hasOutlet
|
||||
>
|
||||
<CategoryListTable />
|
||||
{after.widgets.map((w, i) => {
|
||||
return (
|
||||
<div key={i}>
|
||||
<w.Component />
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
<Outlet />
|
||||
</div>
|
||||
</SingleColumnPage>
|
||||
)
|
||||
}
|
||||
+20
-31
@@ -1,14 +1,13 @@
|
||||
import { Outlet, json, useLoaderData, useParams } from "react-router-dom"
|
||||
import { useLoaderData, useParams } from "react-router-dom"
|
||||
|
||||
import { JsonViewSection } from "../../../components/common/json-view-section"
|
||||
import { SingleColumnPageSkeleton } from "../../../components/common/skeleton"
|
||||
import { SingleColumnPage } from "../../../components/layout/pages"
|
||||
import { useDashboardExtension } from "../../../extensions"
|
||||
import { useCollection } from "../../../hooks/api/collections"
|
||||
import { CollectionGeneralSection } from "./components/collection-general-section"
|
||||
import { CollectionProductSection } from "./components/collection-product-section"
|
||||
import { collectionLoader } from "./loader"
|
||||
|
||||
import after from "virtual:medusa/widgets/product_collection/details/after"
|
||||
import before from "virtual:medusa/widgets/product_collection/details/before"
|
||||
|
||||
export const CollectionDetail = () => {
|
||||
const initialData = useLoaderData() as Awaited<
|
||||
ReturnType<typeof collectionLoader>
|
||||
@@ -19,38 +18,28 @@ export const CollectionDetail = () => {
|
||||
initialData,
|
||||
})
|
||||
|
||||
if (isLoading) {
|
||||
return <div>Loading...</div>
|
||||
const { getWidgets } = useDashboardExtension()
|
||||
|
||||
if (isLoading || !collection) {
|
||||
return <SingleColumnPageSkeleton sections={2} showJSON showMetadata />
|
||||
}
|
||||
|
||||
if (isError || !collection) {
|
||||
if (error) {
|
||||
throw error
|
||||
}
|
||||
|
||||
throw json("An unknown error occurred", 500)
|
||||
if (isError) {
|
||||
throw error
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-y-3">
|
||||
{before.widgets.map((w, i) => {
|
||||
return (
|
||||
<div key={i}>
|
||||
<w.Component data={collection} />
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
<SingleColumnPage
|
||||
widgets={{
|
||||
after: getWidgets("product_collection.details.after"),
|
||||
before: getWidgets("product_collection.details.before"),
|
||||
}}
|
||||
showJSON
|
||||
showMetadata
|
||||
data={collection}
|
||||
>
|
||||
<CollectionGeneralSection collection={collection} />
|
||||
<CollectionProductSection collection={collection} />
|
||||
{after.widgets.map((w, i) => {
|
||||
return (
|
||||
<div key={i}>
|
||||
<w.Component data={collection} />
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
<JsonViewSection data={collection} />
|
||||
<Outlet />
|
||||
</div>
|
||||
</SingleColumnPage>
|
||||
)
|
||||
}
|
||||
+11
-22
@@ -1,29 +1,18 @@
|
||||
import { Outlet } from "react-router-dom"
|
||||
|
||||
import { SingleColumnPage } from "../../../components/layout/pages"
|
||||
import { useDashboardExtension } from "../../../extensions"
|
||||
import { CollectionListTable } from "./components/collection-list-table"
|
||||
|
||||
import after from "virtual:medusa/widgets/product_collection/list/after"
|
||||
import before from "virtual:medusa/widgets/product_collection/list/before"
|
||||
|
||||
export const CollectionList = () => {
|
||||
const { getWidgets } = useDashboardExtension()
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-y-2">
|
||||
{before.widgets.map((w, i) => {
|
||||
return (
|
||||
<div key={i}>
|
||||
<w.Component />
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
<SingleColumnPage
|
||||
widgets={{
|
||||
after: getWidgets("product_collection.list.after"),
|
||||
before: getWidgets("product_collection.list.before"),
|
||||
}}
|
||||
>
|
||||
<CollectionListTable />
|
||||
{after.widgets.map((w, i) => {
|
||||
return (
|
||||
<div key={i}>
|
||||
<w.Component />
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
<Outlet />
|
||||
</div>
|
||||
</SingleColumnPage>
|
||||
)
|
||||
}
|
||||
+5
-4
@@ -6,9 +6,8 @@ import { CustomerGroupCustomerSection } from "./components/customer-group-custom
|
||||
import { CustomerGroupGeneralSection } from "./components/customer-group-general-section"
|
||||
import { customerGroupLoader } from "./loader"
|
||||
|
||||
import after from "virtual:medusa/widgets/customer_group/details/after"
|
||||
import before from "virtual:medusa/widgets/customer_group/details/before"
|
||||
import { SingleColumnPageSkeleton } from "../../../components/common/skeleton"
|
||||
import { useDashboardExtension } from "../../../extensions"
|
||||
|
||||
export const CustomerGroupDetail = () => {
|
||||
const initialData = useLoaderData() as Awaited<
|
||||
@@ -24,6 +23,8 @@ export const CustomerGroupDetail = () => {
|
||||
{ initialData }
|
||||
)
|
||||
|
||||
const { getWidgets } = useDashboardExtension()
|
||||
|
||||
if (isLoading || !customer_group) {
|
||||
return <SingleColumnPageSkeleton sections={2} showJSON showMetadata />
|
||||
}
|
||||
@@ -35,8 +36,8 @@ export const CustomerGroupDetail = () => {
|
||||
return (
|
||||
<SingleColumnPage
|
||||
widgets={{
|
||||
before,
|
||||
after,
|
||||
before: getWidgets("customer_group.details.before"),
|
||||
after: getWidgets("customer_group.details.after"),
|
||||
}}
|
||||
showJSON
|
||||
showMetadata
|
||||
|
||||
+11
-22
@@ -1,29 +1,18 @@
|
||||
import { Outlet } from "react-router-dom"
|
||||
|
||||
import { SingleColumnPage } from "../../../components/layout/pages"
|
||||
import { useDashboardExtension } from "../../../extensions"
|
||||
import { CustomerGroupListTable } from "./components/customer-group-list-table"
|
||||
|
||||
import after from "virtual:medusa/widgets/customer_group/list/after"
|
||||
import before from "virtual:medusa/widgets/customer_group/list/before"
|
||||
|
||||
export const CustomerGroupsList = () => {
|
||||
const { getWidgets } = useDashboardExtension()
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-y-2">
|
||||
{before.widgets.map((w, i) => {
|
||||
return (
|
||||
<div key={i}>
|
||||
<w.Component />
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
<SingleColumnPage
|
||||
widgets={{
|
||||
after: getWidgets("customer_group.list.after"),
|
||||
before: getWidgets("customer_group.list.before"),
|
||||
}}
|
||||
>
|
||||
<CustomerGroupListTable />
|
||||
{after.widgets.map((w, i) => {
|
||||
return (
|
||||
<div key={i}>
|
||||
<w.Component />
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
<Outlet />
|
||||
</div>
|
||||
</SingleColumnPage>
|
||||
)
|
||||
}
|
||||
@@ -2,14 +2,12 @@ import { useLoaderData, useParams } from "react-router-dom"
|
||||
|
||||
import { SingleColumnPageSkeleton } from "../../../components/common/skeleton"
|
||||
import { SingleColumnPage } from "../../../components/layout/pages"
|
||||
import { useDashboardExtension } from "../../../extensions"
|
||||
import { useCustomer } from "../../../hooks/api/customers"
|
||||
import { CustomerGeneralSection } from "./components/customer-general-section"
|
||||
import { CustomerGroupSection } from "./components/customer-group-section"
|
||||
import { customerLoader } from "./loader"
|
||||
|
||||
import after from "virtual:medusa/widgets/customer/details/after"
|
||||
import before from "virtual:medusa/widgets/customer/details/before"
|
||||
|
||||
export const CustomerDetail = () => {
|
||||
const { id } = useParams()
|
||||
|
||||
@@ -20,6 +18,8 @@ export const CustomerDetail = () => {
|
||||
initialData,
|
||||
})
|
||||
|
||||
const { getWidgets } = useDashboardExtension()
|
||||
|
||||
if (isLoading || !customer) {
|
||||
return <SingleColumnPageSkeleton sections={2} showJSON showMetadata />
|
||||
}
|
||||
@@ -31,8 +31,8 @@ export const CustomerDetail = () => {
|
||||
return (
|
||||
<SingleColumnPage
|
||||
widgets={{
|
||||
before,
|
||||
after,
|
||||
before: getWidgets("customer.details.before"),
|
||||
after: getWidgets("customer.details.after"),
|
||||
}}
|
||||
data={customer}
|
||||
hasOutlet
|
||||
|
||||
@@ -1,29 +1,18 @@
|
||||
import { Outlet } from "react-router-dom"
|
||||
|
||||
import { SingleColumnPage } from "../../../components/layout/pages"
|
||||
import { useDashboardExtension } from "../../../extensions"
|
||||
import { CustomerListTable } from "./components/customer-list-table"
|
||||
|
||||
import after from "virtual:medusa/widgets/customer/list/after"
|
||||
import before from "virtual:medusa/widgets/customer/list/before"
|
||||
|
||||
export const CustomersList = () => {
|
||||
const { getWidgets } = useDashboardExtension()
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-y-2">
|
||||
{before.widgets.map((w, i) => {
|
||||
return (
|
||||
<div key={i}>
|
||||
<w.Component />
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
<SingleColumnPage
|
||||
widgets={{
|
||||
after: getWidgets("customer.list.after"),
|
||||
before: getWidgets("customer.list.before"),
|
||||
}}
|
||||
>
|
||||
<CustomerListTable />
|
||||
{after.widgets.map((w, i) => {
|
||||
return (
|
||||
<div key={i}>
|
||||
<w.Component />
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
<Outlet />
|
||||
</div>
|
||||
</SingleColumnPage>
|
||||
)
|
||||
}
|
||||
+8
-10
@@ -10,10 +10,7 @@ import { InventoryItemReservationsSection } from "./components/inventory-item-re
|
||||
import { InventoryItemVariantsSection } from "./components/inventory-item-variants/variants-section"
|
||||
import { inventoryItemLoader } from "./loader"
|
||||
|
||||
import after from "virtual:medusa/widgets/inventory_item/details/after"
|
||||
import before from "virtual:medusa/widgets/inventory_item/details/before"
|
||||
import sideAfter from "virtual:medusa/widgets/inventory_item/details/side/after"
|
||||
import sideBefore from "virtual:medusa/widgets/inventory_item/details/side/before"
|
||||
import { useDashboardExtension } from "../../../extensions"
|
||||
|
||||
export const InventoryDetail = () => {
|
||||
const { id } = useParams()
|
||||
@@ -37,13 +34,15 @@ export const InventoryDetail = () => {
|
||||
}
|
||||
)
|
||||
|
||||
const { getWidgets } = useDashboardExtension()
|
||||
|
||||
if (isLoading || !inventory_item) {
|
||||
return (
|
||||
<TwoColumnPageSkeleton
|
||||
showJSON
|
||||
showMetadata
|
||||
mainSections={3}
|
||||
sidebarSections={2}
|
||||
showMetadata
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -55,15 +54,14 @@ export const InventoryDetail = () => {
|
||||
return (
|
||||
<TwoColumnPage
|
||||
widgets={{
|
||||
after,
|
||||
before,
|
||||
sideAfter,
|
||||
sideBefore,
|
||||
after: getWidgets("inventory_item.details.after"),
|
||||
before: getWidgets("inventory_item.details.before"),
|
||||
sideAfter: getWidgets("inventory_item.details.side.after"),
|
||||
sideBefore: getWidgets("inventory_item.details.side.before"),
|
||||
}}
|
||||
data={inventory_item}
|
||||
showJSON
|
||||
showMetadata
|
||||
hasOutlet
|
||||
>
|
||||
<TwoColumnPage.Main>
|
||||
<InventoryItemGeneralSection inventoryItem={inventory_item} />
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
import { Outlet, useLoaderData, useParams } from "react-router-dom"
|
||||
import { useLoaderData, useParams } from "react-router-dom"
|
||||
|
||||
import { JsonViewSection } from "../../../components/common/json-view-section"
|
||||
import { useStockLocation } from "../../../hooks/api/stock-locations"
|
||||
import { LocationGeneralSection } from "./components/location-general-section"
|
||||
import LocationsSalesChannelsSection from "./components/location-sales-channels-section/locations-sales-channels-section"
|
||||
import { locationLoader } from "./loader"
|
||||
|
||||
import after from "virtual:medusa/widgets/location/details/after"
|
||||
import before from "virtual:medusa/widgets/location/details/before"
|
||||
import sideAfter from "virtual:medusa/widgets/location/details/side/after"
|
||||
import sideBefore from "virtual:medusa/widgets/location/details/side/before"
|
||||
import { TwoColumnPageSkeleton } from "../../../components/common/skeleton"
|
||||
import { TwoColumnPage } from "../../../components/layout/pages"
|
||||
import { useDashboardExtension } from "../../../extensions"
|
||||
import LocationsFulfillmentProvidersSection from "./components/location-fulfillment-providers-section/location-fulfillment-providers-section"
|
||||
import { detailsFields } from "./const"
|
||||
|
||||
@@ -26,9 +24,12 @@ export const LocationDetail = () => {
|
||||
error,
|
||||
} = useStockLocation(location_id!, { fields: detailsFields }, { initialData })
|
||||
|
||||
// TODO: Move to loading.tsx and set as Suspense fallback for the route
|
||||
const { getWidgets } = useDashboardExtension()
|
||||
|
||||
if (isLoading || !location) {
|
||||
return <div>Loading...</div>
|
||||
return (
|
||||
<TwoColumnPageSkeleton mainSections={3} sidebarSections={2} showJSON />
|
||||
)
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
@@ -36,53 +37,24 @@ export const LocationDetail = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-y-3">
|
||||
{before.widgets.map((w, i) => {
|
||||
return (
|
||||
<div key={i}>
|
||||
<w.Component data={location} />
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
<div className="flex flex-col gap-y-3 xl:flex-row xl:items-start xl:gap-x-4">
|
||||
<div className="flex w-full flex-col gap-y-3">
|
||||
<LocationGeneralSection location={location} />
|
||||
{after.widgets.map((w, i) => {
|
||||
return (
|
||||
<div key={i}>
|
||||
<w.Component data={location} />
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
<div className="hidden xl:block">
|
||||
<JsonViewSection data={location} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex w-full max-w-[100%] flex-col gap-y-3 xl:mt-0 xl:max-w-[400px]">
|
||||
{sideBefore.widgets.map((w, i) => {
|
||||
return (
|
||||
<div key={i}>
|
||||
<w.Component data={location} />
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
|
||||
<LocationsSalesChannelsSection location={location} />
|
||||
<LocationsFulfillmentProvidersSection location={location} />
|
||||
|
||||
{sideAfter.widgets.map((w, i) => {
|
||||
return (
|
||||
<div key={i}>
|
||||
<w.Component data={location} />
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
<div className="xl:hidden">
|
||||
<JsonViewSection data={location} />
|
||||
</div>
|
||||
</div>
|
||||
<Outlet />
|
||||
</div>
|
||||
</div>
|
||||
<TwoColumnPage
|
||||
widgets={{
|
||||
after: getWidgets("location.details.after"),
|
||||
before: getWidgets("location.details.before"),
|
||||
sideAfter: getWidgets("location.details.side.after"),
|
||||
sideBefore: getWidgets("location.details.side.before"),
|
||||
}}
|
||||
data={location}
|
||||
showJSON
|
||||
hasOutlet
|
||||
>
|
||||
<TwoColumnPage.Main>
|
||||
<LocationGeneralSection location={location} />
|
||||
</TwoColumnPage.Main>
|
||||
<TwoColumnPage.Sidebar>
|
||||
<LocationsSalesChannelsSection location={location} />
|
||||
<LocationsFulfillmentProvidersSection location={location} />
|
||||
</TwoColumnPage.Sidebar>
|
||||
</TwoColumnPage>
|
||||
)
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ArrowUpRightOnBox, Buildings, ShoppingBag } from "@medusajs/icons"
|
||||
import { ArrowUpRightOnBox, ShoppingBag } from "@medusajs/icons"
|
||||
import { Container, Heading, Text } from "@medusajs/ui"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { Link, Outlet, useLoaderData } from "react-router-dom"
|
||||
import { Link, useLoaderData } from "react-router-dom"
|
||||
|
||||
import { useStockLocations } from "../../../hooks/api/stock-locations"
|
||||
import LocationListItem from "./components/location-list-item/location-list-item"
|
||||
@@ -9,9 +9,9 @@ import { LOCATION_LIST_FIELDS } from "./constants"
|
||||
import { shippingListLoader } from "./loader"
|
||||
|
||||
import { ReactNode } from "react"
|
||||
import after from "virtual:medusa/widgets/location/list/after"
|
||||
import before from "virtual:medusa/widgets/location/list/before"
|
||||
import { IconAvatar } from "../../../components/common/icon-avatar"
|
||||
import { TwoColumnPage } from "../../../components/layout/pages"
|
||||
import { useDashboardExtension } from "../../../extensions"
|
||||
import { LocationListHeader } from "./components/location-list-header"
|
||||
|
||||
export function LocationList() {
|
||||
@@ -30,42 +30,34 @@ export function LocationList() {
|
||||
{ initialData }
|
||||
)
|
||||
|
||||
const { getWidgets } = useDashboardExtension()
|
||||
|
||||
if (isError) {
|
||||
throw error
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-y-2">
|
||||
<div className="flex flex-col gap-x-4 gap-y-3 xl:flex-row xl:items-start">
|
||||
<div className="flex w-full flex-col gap-y-3">
|
||||
{before.widgets.map((w, i) => {
|
||||
return (
|
||||
<div key={i}>
|
||||
<w.Component />
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
<LocationListHeader />
|
||||
<div className="flex flex-col gap-3 lg:col-span-2">
|
||||
{stockLocations.map((location) => (
|
||||
<LocationListItem key={location.id} location={location} />
|
||||
))}
|
||||
</div>
|
||||
{after.widgets.map((w, i) => {
|
||||
return (
|
||||
<div key={i}>
|
||||
<w.Component />
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
<TwoColumnPage
|
||||
widgets={{
|
||||
after: getWidgets("location.details.after"),
|
||||
before: getWidgets("location.details.before"),
|
||||
sideAfter: getWidgets("location.details.side.after"),
|
||||
sideBefore: getWidgets("location.details.side.before"),
|
||||
}}
|
||||
showJSON
|
||||
>
|
||||
<TwoColumnPage.Main>
|
||||
<LocationListHeader />
|
||||
<div className="flex flex-col gap-3 lg:col-span-2">
|
||||
{stockLocations.map((location) => (
|
||||
<LocationListItem key={location.id} location={location} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex w-full max-w-[100%] flex-col gap-y-3 xl:mt-0 xl:max-w-[400px]">
|
||||
<LinksSection />
|
||||
</div>
|
||||
</div>
|
||||
<Outlet />
|
||||
</div>
|
||||
</TwoColumnPage.Main>
|
||||
<TwoColumnPage.Sidebar>
|
||||
<LinksSection />
|
||||
</TwoColumnPage.Sidebar>
|
||||
</TwoColumnPage>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -127,15 +119,6 @@ const LinksSection = () => {
|
||||
)}
|
||||
icon={<ShoppingBag />}
|
||||
/>
|
||||
|
||||
<SidebarLink
|
||||
to="/settings/locations/shipping-option-types"
|
||||
labelKey={t("stockLocations.sidebar.shippingOptionTypes.label")}
|
||||
descriptionKey={t(
|
||||
"stockLocations.sidebar.shippingOptionTypes.description"
|
||||
)}
|
||||
icon={<Buildings />}
|
||||
/>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
@@ -6,14 +6,10 @@ import { Link, useLocation, useNavigate } from "react-router-dom"
|
||||
import * as z from "zod"
|
||||
|
||||
import { Form } from "../../components/common/form"
|
||||
import { LogoBox } from "../../components/common/logo-box"
|
||||
import { useSignInWithEmailPass } from "../../hooks/api/auth"
|
||||
import { useSignInWithEmailPassword } from "../../hooks/api/auth"
|
||||
|
||||
import after from "virtual:medusa/widgets/login/after"
|
||||
import before from "virtual:medusa/widgets/login/before"
|
||||
import { isFetchError } from "../../lib/is-fetch-error"
|
||||
import AvatarBox from "../../components/common/logo-box/avatar-box"
|
||||
import { useDashboardExtension } from "../../extensions"
|
||||
import { useSignInWithEmailPass } from "../../hooks/api"
|
||||
import { isFetchError } from "../../lib/is-fetch-error"
|
||||
|
||||
const LoginSchema = z.object({
|
||||
email: z.string().email(),
|
||||
@@ -24,6 +20,7 @@ export const Login = () => {
|
||||
const { t } = useTranslation()
|
||||
const location = useLocation()
|
||||
const navigate = useNavigate()
|
||||
const { getWidgets } = useDashboardExtension()
|
||||
|
||||
const from = location.state?.from?.pathname || "/orders"
|
||||
|
||||
@@ -38,34 +35,34 @@ export const Login = () => {
|
||||
const { mutateAsync, isPending } = useSignInWithEmailPass()
|
||||
|
||||
const handleSubmit = form.handleSubmit(async ({ email, password }) => {
|
||||
try {
|
||||
await mutateAsync({
|
||||
await mutateAsync(
|
||||
{
|
||||
email,
|
||||
password,
|
||||
})
|
||||
},
|
||||
{
|
||||
onError: (error) => {
|
||||
if (isFetchError(error)) {
|
||||
if (error.status === 401) {
|
||||
form.setError("email", {
|
||||
type: "manual",
|
||||
message: error.message,
|
||||
})
|
||||
|
||||
navigate(from, { replace: true })
|
||||
} catch (error) {
|
||||
if (isFetchError(error)) {
|
||||
if (error.status === 401) {
|
||||
form.setError("email", {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
form.setError("root.serverError", {
|
||||
type: "manual",
|
||||
message: error.message,
|
||||
})
|
||||
|
||||
form.setError("password", {
|
||||
type: "manual",
|
||||
message: t("errors.invalidCredentials"),
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
},
|
||||
onSuccess: () => {
|
||||
navigate(from, { replace: true })
|
||||
},
|
||||
}
|
||||
|
||||
form.setError("root.serverError", {
|
||||
type: "manual",
|
||||
message: t("errors.serverError"),
|
||||
})
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
const serverError = form.formState.errors?.root?.serverError?.message
|
||||
@@ -84,12 +81,8 @@ export const Login = () => {
|
||||
</Text>
|
||||
</div>
|
||||
<div className="flex w-full flex-col gap-y-3">
|
||||
{before.widgets.map((w, i) => {
|
||||
return (
|
||||
<div key={i}>
|
||||
<w.Component />
|
||||
</div>
|
||||
)
|
||||
{getWidgets("login.before").map((Component, i) => {
|
||||
return <Component key={i} />
|
||||
})}
|
||||
<Form {...form}>
|
||||
<form
|
||||
@@ -145,7 +138,7 @@ export const Login = () => {
|
||||
)}
|
||||
{serverError && (
|
||||
<Alert
|
||||
className="p-2 bg-ui-bg-base items-center"
|
||||
className="bg-ui-bg-base items-center p-2"
|
||||
dismissible
|
||||
variant="error"
|
||||
>
|
||||
@@ -157,12 +150,8 @@ export const Login = () => {
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
{after.widgets.map((w, i) => {
|
||||
return (
|
||||
<div key={i}>
|
||||
<w.Component />
|
||||
</div>
|
||||
)
|
||||
{getWidgets("login.after").map((Component, i) => {
|
||||
return <Component key={i} />
|
||||
})}
|
||||
</div>
|
||||
<span className="text-ui-fg-muted txt-small my-6">
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
import { Outlet, useLoaderData, useParams } from "react-router-dom"
|
||||
import { useLoaderData, useParams } from "react-router-dom"
|
||||
|
||||
import { JsonViewSection } from "../../../components/common/json-view-section"
|
||||
import { TwoColumnPageSkeleton } from "../../../components/common/skeleton"
|
||||
import { TwoColumnPage } from "../../../components/layout/pages"
|
||||
import { useDashboardExtension } from "../../../extensions"
|
||||
import { useOrder, useOrderPreview } from "../../../hooks/api/orders"
|
||||
import { ActiveOrderClaimSection } from "./components/active-order-claim-section"
|
||||
import { ActiveOrderExchangeSection } from "./components/active-order-exchange-section"
|
||||
import { ActiveOrderReturnSection } from "./components/active-order-return-section"
|
||||
import { OrderActiveEditSection } from "./components/order-active-edit-section"
|
||||
import { OrderActivitySection } from "./components/order-activity-section"
|
||||
import { OrderCustomerSection } from "./components/order-customer-section"
|
||||
import { OrderFulfillmentSection } from "./components/order-fulfillment-section"
|
||||
@@ -11,19 +17,11 @@ import { OrderSummarySection } from "./components/order-summary-section"
|
||||
import { DEFAULT_FIELDS } from "./constants"
|
||||
import { orderLoader } from "./loader"
|
||||
|
||||
import after from "virtual:medusa/widgets/order/details/after"
|
||||
import before from "virtual:medusa/widgets/order/details/before"
|
||||
import sideAfter from "virtual:medusa/widgets/order/details/side/after"
|
||||
import sideBefore from "virtual:medusa/widgets/order/details/side/before"
|
||||
import { ActiveOrderClaimSection } from "./components/active-order-claim-section"
|
||||
import { ActiveOrderExchangeSection } from "./components/active-order-exchange-section"
|
||||
import { ActiveOrderReturnSection } from "./components/active-order-return-section"
|
||||
import { OrderActiveEditSection } from "./components/order-active-edit-section"
|
||||
|
||||
export const OrderDetail = () => {
|
||||
const initialData = useLoaderData() as Awaited<ReturnType<typeof orderLoader>>
|
||||
|
||||
const { id } = useParams()
|
||||
const { getWidgets } = useDashboardExtension()
|
||||
|
||||
const { order, isLoading, isError, error } = useOrder(
|
||||
id!,
|
||||
@@ -55,7 +53,9 @@ export const OrderDetail = () => {
|
||||
)
|
||||
|
||||
if (isLoading || !order || isPreviewLoading) {
|
||||
return <div>Loading...</div>
|
||||
return (
|
||||
<TwoColumnPageSkeleton mainSections={4} sidebarSections={2} showJSON />
|
||||
)
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
@@ -63,58 +63,31 @@ export const OrderDetail = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-y-3">
|
||||
{before.widgets.map((w, i) => {
|
||||
return (
|
||||
<div key={i}>
|
||||
<w.Component data={order} />
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
<div className="flex flex-col gap-x-4 lg:flex-row xl:items-start">
|
||||
<div className="flex w-full flex-col gap-y-3">
|
||||
<OrderActiveEditSection order={order} />
|
||||
<ActiveOrderClaimSection orderPreview={orderPreview!} />
|
||||
<ActiveOrderExchangeSection orderPreview={orderPreview!} />
|
||||
<ActiveOrderReturnSection orderPreview={orderPreview!} />
|
||||
<OrderGeneralSection order={order} />
|
||||
<OrderSummarySection order={order} />
|
||||
<OrderPaymentSection order={order} />
|
||||
<OrderFulfillmentSection order={order} />
|
||||
{after.widgets.map((w, i) => {
|
||||
return (
|
||||
<div key={i}>
|
||||
<w.Component data={order} />
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
<div className="hidden xl:block">
|
||||
<JsonViewSection data={order} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-2 flex w-full max-w-[100%] flex-col gap-y-3 xl:mt-0 xl:max-w-[400px]">
|
||||
{sideBefore.widgets.map((w, i) => {
|
||||
return (
|
||||
<div key={i}>
|
||||
<w.Component data={order} />
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
<OrderCustomerSection order={order} />
|
||||
<OrderActivitySection order={order} />
|
||||
{sideAfter.widgets.map((w, i) => {
|
||||
return (
|
||||
<div key={i}>
|
||||
<w.Component data={order} />
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
<div className="xl:hidden">
|
||||
<JsonViewSection data={order} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Outlet />
|
||||
</div>
|
||||
<TwoColumnPage
|
||||
widgets={{
|
||||
after: getWidgets("order.details.after"),
|
||||
before: getWidgets("order.details.before"),
|
||||
sideAfter: getWidgets("order.details.side.after"),
|
||||
sideBefore: getWidgets("order.details.side.before"),
|
||||
}}
|
||||
data={order}
|
||||
showJSON
|
||||
hasOutlet
|
||||
>
|
||||
<TwoColumnPage.Main>
|
||||
<OrderActiveEditSection order={order} />
|
||||
<ActiveOrderClaimSection orderPreview={orderPreview!} />
|
||||
<ActiveOrderExchangeSection orderPreview={orderPreview!} />
|
||||
<ActiveOrderReturnSection orderPreview={orderPreview!} />
|
||||
<OrderGeneralSection order={order} />
|
||||
<OrderSummarySection order={order} />
|
||||
<OrderPaymentSection order={order} />
|
||||
<OrderFulfillmentSection order={order} />
|
||||
</TwoColumnPage.Main>
|
||||
<TwoColumnPage.Sidebar>
|
||||
<OrderCustomerSection order={order} />
|
||||
<OrderActivitySection order={order} />
|
||||
</TwoColumnPage.Sidebar>
|
||||
</TwoColumnPage>
|
||||
)
|
||||
}
|
||||
@@ -1,26 +1,20 @@
|
||||
import { OrderListTable } from "./components/order-list-table"
|
||||
|
||||
import after from "virtual:medusa/widgets/order/list/after"
|
||||
import before from "virtual:medusa/widgets/order/list/before"
|
||||
import { SingleColumnPage } from "../../../components/layout/pages"
|
||||
import { useDashboardExtension } from "../../../extensions"
|
||||
|
||||
export const OrderList = () => {
|
||||
const { getWidgets } = useDashboardExtension()
|
||||
|
||||
return (
|
||||
<div className="flex w-full flex-col gap-y-3">
|
||||
{before.widgets.map((w, i) => {
|
||||
return (
|
||||
<div key={i}>
|
||||
<w.Component />
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
<SingleColumnPage
|
||||
widgets={{
|
||||
after: getWidgets("order.list.after"),
|
||||
before: getWidgets("order.list.before"),
|
||||
}}
|
||||
hasOutlet={false}
|
||||
>
|
||||
<OrderListTable />
|
||||
{after.widgets.map((w, i) => {
|
||||
return (
|
||||
<div key={i}>
|
||||
<w.Component />
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</SingleColumnPage>
|
||||
)
|
||||
}
|
||||
+26
-53
@@ -1,23 +1,24 @@
|
||||
import { Outlet, useParams } from "react-router-dom"
|
||||
import { useParams } from "react-router-dom"
|
||||
|
||||
import { JsonViewSection } from "../../../components/common/json-view-section"
|
||||
import { usePriceList } from "../../../hooks/api/price-lists"
|
||||
import { PriceListConfigurationSection } from "./components/price-list-configuration-section"
|
||||
import { PriceListGeneralSection } from "./components/price-list-general-section"
|
||||
import { PriceListProductSection } from "./components/price-list-product-section"
|
||||
|
||||
import after from "virtual:medusa/widgets/price_list/details/after"
|
||||
import before from "virtual:medusa/widgets/price_list/details/before"
|
||||
import sideAfter from "virtual:medusa/widgets/price_list/details/side/after"
|
||||
import sideBefore from "virtual:medusa/widgets/price_list/details/side/before"
|
||||
import { TwoColumnPageSkeleton } from "../../../components/common/skeleton"
|
||||
import { TwoColumnPage } from "../../../components/layout/pages"
|
||||
import { useDashboardExtension } from "../../../extensions"
|
||||
|
||||
export const PriceListDetails = () => {
|
||||
const { id } = useParams()
|
||||
|
||||
const { price_list, isLoading, isError, error } = usePriceList(id!)
|
||||
const { getWidgets } = useDashboardExtension()
|
||||
|
||||
if (isLoading || !price_list) {
|
||||
return <div>Loading...</div>
|
||||
return (
|
||||
<TwoColumnPageSkeleton mainSections={2} sidebarSections={1} showJSON />
|
||||
)
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
@@ -25,51 +26,23 @@ export const PriceListDetails = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-y-2">
|
||||
{before.widgets.map((w, i) => {
|
||||
return (
|
||||
<div key={i}>
|
||||
<w.Component data={price_list} />
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
<div className="flex flex-col gap-x-4 xl:flex-row xl:items-start">
|
||||
<div className="flex flex-1 flex-col gap-y-3">
|
||||
<PriceListGeneralSection priceList={price_list} />
|
||||
<PriceListProductSection priceList={price_list} />
|
||||
{after.widgets.map((w, i) => {
|
||||
return (
|
||||
<div key={i}>
|
||||
<w.Component data={price_list} />
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
<div className="hidden xl:block">
|
||||
<JsonViewSection data={price_list} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-2 flex w-full max-w-[100%] flex-col gap-y-2 xl:mt-0 xl:max-w-[440px]">
|
||||
{sideBefore.widgets.map((w, i) => {
|
||||
return (
|
||||
<div key={i}>
|
||||
<w.Component data={price_list} />
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
<PriceListConfigurationSection priceList={price_list} />
|
||||
{sideAfter.widgets.map((w, i) => {
|
||||
return (
|
||||
<div key={i}>
|
||||
<w.Component data={price_list} />
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
<div className="xl:hidden">
|
||||
<JsonViewSection data={price_list} />
|
||||
</div>
|
||||
</div>
|
||||
<Outlet />
|
||||
</div>
|
||||
</div>
|
||||
<TwoColumnPage
|
||||
widgets={{
|
||||
after: getWidgets("price_list.details.after"),
|
||||
before: getWidgets("price_list.details.before"),
|
||||
sideAfter: getWidgets("price_list.details.side.after"),
|
||||
sideBefore: getWidgets("price_list.details.side.before"),
|
||||
}}
|
||||
data={price_list}
|
||||
showJSON
|
||||
>
|
||||
<TwoColumnPage.Main>
|
||||
<PriceListGeneralSection priceList={price_list} />
|
||||
<PriceListProductSection priceList={price_list} />
|
||||
</TwoColumnPage.Main>
|
||||
<TwoColumnPage.Sidebar>
|
||||
<PriceListConfigurationSection priceList={price_list} />
|
||||
</TwoColumnPage.Sidebar>
|
||||
</TwoColumnPage>
|
||||
)
|
||||
}
|
||||
+11
-22
@@ -1,29 +1,18 @@
|
||||
import { Outlet } from "react-router-dom"
|
||||
|
||||
import { SingleColumnPage } from "../../../components/layout/pages"
|
||||
import { useDashboardExtension } from "../../../extensions"
|
||||
import { PriceListListTable } from "./components/price-list-list-table"
|
||||
|
||||
import after from "virtual:medusa/widgets/price_list/list/after"
|
||||
import before from "virtual:medusa/widgets/price_list/list/before"
|
||||
|
||||
export const PriceListList = () => {
|
||||
const { getWidgets } = useDashboardExtension()
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-y-2">
|
||||
{before.widgets.map((w, i) => {
|
||||
return (
|
||||
<div key={i}>
|
||||
<w.Component />
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
<SingleColumnPage
|
||||
widgets={{
|
||||
after: getWidgets("price_list.list.after"),
|
||||
before: getWidgets("price_list.list.before"),
|
||||
}}
|
||||
>
|
||||
<PriceListListTable />
|
||||
{after.widgets.map((w, i) => {
|
||||
return (
|
||||
<div key={i}>
|
||||
<w.Component />
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
<Outlet />
|
||||
</div>
|
||||
</SingleColumnPage>
|
||||
)
|
||||
}
|
||||
+11
-4
@@ -2,14 +2,12 @@ import { useLoaderData, useParams } from "react-router-dom"
|
||||
|
||||
import { SingleColumnPageSkeleton } from "../../../components/common/skeleton"
|
||||
import { SingleColumnPage } from "../../../components/layout/pages"
|
||||
import { useDashboardExtension } from "../../../extensions"
|
||||
import { useProductTag } from "../../../hooks/api"
|
||||
import { ProductTagGeneralSection } from "./components/product-tag-general-section"
|
||||
import { ProductTagProductSection } from "./components/product-tag-product-section"
|
||||
import { productTagLoader } from "./loader"
|
||||
|
||||
import after from "virtual:medusa/widgets/product_tag/details/after"
|
||||
import before from "virtual:medusa/widgets/product_tag/details/before"
|
||||
|
||||
export const ProductTagDetail = () => {
|
||||
const { id } = useParams()
|
||||
|
||||
@@ -17,6 +15,8 @@ export const ProductTagDetail = () => {
|
||||
ReturnType<typeof productTagLoader>
|
||||
>
|
||||
|
||||
const { getWidgets } = useDashboardExtension()
|
||||
|
||||
const { product_tag, isPending, isError, error } = useProductTag(
|
||||
id!,
|
||||
undefined,
|
||||
@@ -34,7 +34,14 @@ export const ProductTagDetail = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<SingleColumnPage widgets={{ after, before }} showJSON data={product_tag}>
|
||||
<SingleColumnPage
|
||||
widgets={{
|
||||
after: getWidgets("product_tag.details.after"),
|
||||
before: getWidgets("product_tag.details.before"),
|
||||
}}
|
||||
showJSON
|
||||
data={product_tag}
|
||||
>
|
||||
<ProductTagGeneralSection productTag={product_tag} />
|
||||
<ProductTagProductSection productTag={product_tag} />
|
||||
</SingleColumnPage>
|
||||
|
||||
+5
-5
@@ -1,18 +1,18 @@
|
||||
import { SingleColumnPage } from "../../../components/layout/pages"
|
||||
import { useDashboardExtension } from "../../../extensions"
|
||||
import { ProductTagListTable } from "./components/product-tag-list-table"
|
||||
|
||||
import after from "virtual:medusa/widgets/product_tag/list/after"
|
||||
import before from "virtual:medusa/widgets/product_tag/list/before"
|
||||
|
||||
export const ProductTagList = () => {
|
||||
const { getWidgets } = useDashboardExtension()
|
||||
|
||||
return (
|
||||
<SingleColumnPage
|
||||
showMetadata={false}
|
||||
showJSON={false}
|
||||
hasOutlet
|
||||
widgets={{
|
||||
after,
|
||||
before,
|
||||
after: getWidgets("product_tag.list.after"),
|
||||
before: getWidgets("product_tag.list.before"),
|
||||
}}
|
||||
>
|
||||
<ProductTagListTable />
|
||||
|
||||
+17
-24
@@ -1,14 +1,13 @@
|
||||
import { Outlet, useLoaderData, useParams } from "react-router-dom"
|
||||
import { useLoaderData, useParams } from "react-router-dom"
|
||||
|
||||
import { JsonViewSection } from "../../../components/common/json-view-section"
|
||||
import { SingleColumnPageSkeleton } from "../../../components/common/skeleton"
|
||||
import { SingleColumnPage } from "../../../components/layout/pages"
|
||||
import { useDashboardExtension } from "../../../extensions"
|
||||
import { useProductType } from "../../../hooks/api/product-types"
|
||||
import { ProductTypeGeneralSection } from "./components/product-type-general-section"
|
||||
import { ProductTypeProductSection } from "./components/product-type-product-section"
|
||||
import { productTypeLoader } from "./loader"
|
||||
|
||||
import after from "virtual:medusa/widgets/product_type/details/after"
|
||||
import before from "virtual:medusa/widgets/product_type/details/before"
|
||||
|
||||
export const ProductTypeDetail = () => {
|
||||
const { id } = useParams()
|
||||
const initialData = useLoaderData() as Awaited<
|
||||
@@ -23,8 +22,10 @@ export const ProductTypeDetail = () => {
|
||||
}
|
||||
)
|
||||
|
||||
const { getWidgets } = useDashboardExtension()
|
||||
|
||||
if (isPending || !product_type) {
|
||||
return null
|
||||
return <SingleColumnPageSkeleton sections={2} showJSON showMetadata />
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
@@ -32,25 +33,17 @@ export const ProductTypeDetail = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-y-3">
|
||||
{before.widgets.map((w, i) => {
|
||||
return (
|
||||
<div key={i}>
|
||||
<w.Component data={product_type} />
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
<SingleColumnPage
|
||||
widgets={{
|
||||
after: getWidgets("product_type.details.after"),
|
||||
before: getWidgets("product_type.details.before"),
|
||||
}}
|
||||
showJSON
|
||||
showMetadata
|
||||
data={product_type}
|
||||
>
|
||||
<ProductTypeGeneralSection productType={product_type} />
|
||||
<ProductTypeProductSection productType={product_type} />
|
||||
{after.widgets.map((w, i) => {
|
||||
return (
|
||||
<div key={i}>
|
||||
<w.Component data={product_type} />
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
<JsonViewSection data={product_type} />
|
||||
<Outlet />
|
||||
</div>
|
||||
</SingleColumnPage>
|
||||
)
|
||||
}
|
||||
+11
-21
@@ -1,28 +1,18 @@
|
||||
import { Outlet } from "react-router-dom"
|
||||
import { SingleColumnPage } from "../../../components/layout/pages"
|
||||
import { useDashboardExtension } from "../../../extensions"
|
||||
import { ProductTypeListTable } from "./components/product-type-list-table"
|
||||
|
||||
import after from "virtual:medusa/widgets/product_type/list/after"
|
||||
import before from "virtual:medusa/widgets/product_type/list/before"
|
||||
|
||||
export const ProductTypeList = () => {
|
||||
const { getWidgets } = useDashboardExtension()
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-y-3">
|
||||
{before.widgets.map((w, i) => {
|
||||
return (
|
||||
<div key={i}>
|
||||
<w.Component />
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
<SingleColumnPage
|
||||
widgets={{
|
||||
after: getWidgets("product_type.list.after"),
|
||||
before: getWidgets("product_type.list.before"),
|
||||
}}
|
||||
>
|
||||
<ProductTypeListTable />
|
||||
{after.widgets.map((w, i) => {
|
||||
return (
|
||||
<div key={i}>
|
||||
<w.Component />
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
<Outlet />
|
||||
</div>
|
||||
</SingleColumnPage>
|
||||
)
|
||||
}
|
||||
+16
-9
@@ -1,15 +1,15 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { HttpTypes } from "@medusajs/types"
|
||||
import { Button, Input } 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 { CountrySelect } from "../../../../../components/inputs/country-select"
|
||||
import { RouteDrawer, useRouteModal } from "../../../../../components/modals"
|
||||
import {
|
||||
RouteDrawer,
|
||||
useRouteModal,
|
||||
} from "../../../../../components/modals"
|
||||
FormExtensionZone,
|
||||
useDashboardExtension,
|
||||
useExtendableForm,
|
||||
} from "../../../../../extensions"
|
||||
import { useUpdateProduct } from "../../../../../hooks/api/products"
|
||||
|
||||
type ProductAttributesFormProps = {
|
||||
@@ -42,8 +42,12 @@ export const ProductAttributesForm = ({
|
||||
}: ProductAttributesFormProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { handleSuccess } = useRouteModal()
|
||||
const { getFormConfigs, getFormFields } = useDashboardExtension()
|
||||
|
||||
const form = useForm<zod.infer<typeof ProductAttributesSchema>>({
|
||||
const configs = getFormConfigs("product", "attributes")
|
||||
const fields = getFormFields("product", "attributes")
|
||||
|
||||
const form = useExtendableForm({
|
||||
defaultValues: {
|
||||
height: product.height ? product.height : null,
|
||||
width: product.width ? product.width : null,
|
||||
@@ -53,10 +57,12 @@ export const ProductAttributesForm = ({
|
||||
hs_code: product.hs_code || "",
|
||||
origin_country: product.origin_country || "",
|
||||
},
|
||||
resolver: zodResolver(ProductAttributesSchema),
|
||||
schema: ProductAttributesSchema,
|
||||
configs: configs,
|
||||
data: product,
|
||||
})
|
||||
|
||||
const { mutateAsync, isLoading } = useUpdateProduct(product.id)
|
||||
const { mutateAsync, isPending } = useUpdateProduct(product.id)
|
||||
|
||||
const handleSubmit = form.handleSubmit(async (data) => {
|
||||
await mutateAsync(
|
||||
@@ -244,6 +250,7 @@ export const ProductAttributesForm = ({
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<FormExtensionZone fields={fields} form={form} />
|
||||
</div>
|
||||
</div>
|
||||
</RouteDrawer.Body>
|
||||
@@ -254,7 +261,7 @@ export const ProductAttributesForm = ({
|
||||
{t("actions.cancel")}
|
||||
</Button>
|
||||
</RouteDrawer.Close>
|
||||
<Button size="small" type="submit" isLoading={isLoading}>
|
||||
<Button size="small" type="submit" isLoading={isPending}>
|
||||
{t("actions.save")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
+7
-2
@@ -4,13 +4,16 @@ import { useParams } from "react-router-dom"
|
||||
|
||||
import { RouteDrawer } from "../../../components/modals"
|
||||
import { useProduct } from "../../../hooks/api/products"
|
||||
import { PRODUCT_DETAIL_FIELDS } from "../product-detail/constants"
|
||||
import { ProductAttributesForm } from "./components/product-attributes-form"
|
||||
|
||||
export const ProductAttributes = () => {
|
||||
const { id } = useParams()
|
||||
const { t } = useTranslation()
|
||||
|
||||
const { product, isLoading, isError, error } = useProduct(id!)
|
||||
const { product, isLoading, isError, error } = useProduct(id!, {
|
||||
fields: PRODUCT_DETAIL_FIELDS,
|
||||
})
|
||||
|
||||
if (isError) {
|
||||
throw error
|
||||
@@ -19,7 +22,9 @@ export const ProductAttributes = () => {
|
||||
return (
|
||||
<RouteDrawer>
|
||||
<RouteDrawer.Header>
|
||||
<Heading>{t("products.editAttributes")}</Heading>
|
||||
<RouteDrawer.Title asChild>
|
||||
<Heading>{t("products.editAttributes")}</Heading>
|
||||
</RouteDrawer.Title>
|
||||
</RouteDrawer.Header>
|
||||
{!isLoading && product && <ProductAttributesForm product={product} />}
|
||||
</RouteDrawer>
|
||||
|
||||
+8
@@ -3,6 +3,10 @@ import { UseFormReturn } from "react-hook-form"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
import { Divider } from "../../../../../components/common/divider"
|
||||
import {
|
||||
FormExtensionZone,
|
||||
useDashboardExtension,
|
||||
} from "../../../../../extensions"
|
||||
import { ProductCreateSchemaType } from "../../types"
|
||||
import { ProductCreateGeneralSection } from "./components/product-create-details-general-section"
|
||||
import { ProductCreateMediaSection } from "./components/product-create-details-media-section"
|
||||
@@ -13,12 +17,16 @@ type ProductAttributesProps = {
|
||||
}
|
||||
|
||||
export const ProductCreateDetailsForm = ({ form }: ProductAttributesProps) => {
|
||||
const { getFormFields } = useDashboardExtension()
|
||||
const fields = getFormFields("product", "create", "general")
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center p-16">
|
||||
<div className="flex w-full max-w-[720px] flex-col gap-y-8">
|
||||
<Header />
|
||||
<div className="flex flex-col gap-y-6">
|
||||
<ProductCreateGeneralSection form={form} />
|
||||
<FormExtensionZone fields={fields} form={form} />
|
||||
<ProductCreateMediaSection form={form} />
|
||||
</div>
|
||||
<Divider />
|
||||
|
||||
+10
-5
@@ -1,13 +1,16 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { HttpTypes } from "@medusajs/types"
|
||||
import { Button, ProgressStatus, ProgressTabs, toast } from "@medusajs/ui"
|
||||
import { useEffect, useMemo, useState } from "react"
|
||||
import { useForm, useWatch } from "react-hook-form"
|
||||
import { useWatch } from "react-hook-form"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import {
|
||||
RouteFocusModal,
|
||||
useRouteModal,
|
||||
} from "../../../../../components/modals"
|
||||
import {
|
||||
useDashboardExtension,
|
||||
useExtendableForm,
|
||||
} from "../../../../../extensions"
|
||||
import { useCreateProduct } from "../../../../../hooks/api/products"
|
||||
import { sdk } from "../../../../../lib/client"
|
||||
import { isFetchError } from "../../../../../lib/is-fetch-error"
|
||||
@@ -15,7 +18,6 @@ import {
|
||||
PRODUCT_CREATE_FORM_DEFAULTS,
|
||||
ProductCreateSchema,
|
||||
} from "../../constants"
|
||||
import { ProductCreateSchemaType } from "../../types"
|
||||
import { normalizeProductFormValues } from "../../utils"
|
||||
import { ProductCreateDetailsForm } from "../product-create-details-form"
|
||||
import { ProductCreateInventoryKitForm } from "../product-create-inventory-kit-form"
|
||||
@@ -56,15 +58,18 @@ export const ProductCreateForm = ({
|
||||
|
||||
const { t } = useTranslation()
|
||||
const { handleSuccess } = useRouteModal()
|
||||
const { getFormConfigs } = useDashboardExtension()
|
||||
const configs = getFormConfigs("product", "create")
|
||||
|
||||
const form = useForm<ProductCreateSchemaType>({
|
||||
const form = useExtendableForm({
|
||||
defaultValues: {
|
||||
...PRODUCT_CREATE_FORM_DEFAULTS,
|
||||
sales_channels: defaultChannel
|
||||
? [{ id: defaultChannel.id, name: defaultChannel.name }]
|
||||
: [],
|
||||
},
|
||||
resolver: zodResolver(ProductCreateSchema),
|
||||
schema: ProductCreateSchema,
|
||||
configs,
|
||||
})
|
||||
|
||||
const { mutateAsync, isPending } = useCreateProduct()
|
||||
|
||||
+7
-1
@@ -2,6 +2,10 @@ import { useState } from "react"
|
||||
import { UseFormReturn } from "react-hook-form"
|
||||
|
||||
import { SplitView } from "../../../../../components/layout/split-view"
|
||||
import {
|
||||
FormExtensionZone,
|
||||
useDashboardExtension,
|
||||
} from "../../../../../extensions"
|
||||
import { ProductCreateSchemaType } from "../../types"
|
||||
import { ProductCreateDetailsContext } from "./components/product-create-organize-context"
|
||||
import { ProductCreateOrganizationSection } from "./components/product-create-organize-section"
|
||||
@@ -13,6 +17,8 @@ type ProductAttributesProps = {
|
||||
|
||||
export const ProductCreateOrganizeForm = ({ form }: ProductAttributesProps) => {
|
||||
const [open, setOpen] = useState(false)
|
||||
const { getFormFields } = useDashboardExtension()
|
||||
const fields = getFormFields("product", "create", "organize")
|
||||
|
||||
return (
|
||||
<ProductCreateDetailsContext.Provider
|
||||
@@ -23,7 +29,7 @@ export const ProductCreateOrganizeForm = ({ form }: ProductAttributesProps) => {
|
||||
<div className="flex flex-col items-center p-16">
|
||||
<div className="flex w-full max-w-[720px] flex-col gap-y-8">
|
||||
<ProductCreateOrganizationSection form={form} />
|
||||
|
||||
<FormExtensionZone fields={fields} form={form} />
|
||||
{/* TODO: WHERE DO WE SET PRODUCT ATTRIBUTES? -> the plan is to moved that to Inventory UI */}
|
||||
{/* <Divider />*/}
|
||||
{/* <ProductCreateAttributeSection form={form} />*/}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { HttpTypes } from "@medusajs/types"
|
||||
import { ProductCreateSchemaType } from "./types"
|
||||
import { castNumber } from "../../../lib/cast-number"
|
||||
import { ProductCreateSchemaType } from "./types"
|
||||
|
||||
export const normalizeProductFormValues = (
|
||||
values: ProductCreateSchemaType & {
|
||||
|
||||
+7
-2
@@ -1,10 +1,11 @@
|
||||
import { PencilSquare } from "@medusajs/icons"
|
||||
import { Container, Heading, Text } from "@medusajs/ui"
|
||||
import { HttpTypes } from "@medusajs/types"
|
||||
import { Container, Heading } from "@medusajs/ui"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||
import { SectionRow } from "../../../../../components/common/section"
|
||||
import { useDashboardExtension } from "../../../../../extensions"
|
||||
import { getFormattedCountry } from "../../../../../lib/addresses"
|
||||
import { HttpTypes } from "@medusajs/types"
|
||||
|
||||
type ProductAttributeSectionProps = {
|
||||
product: HttpTypes.AdminProduct
|
||||
@@ -14,6 +15,7 @@ export const ProductAttributeSection = ({
|
||||
product,
|
||||
}: ProductAttributeSectionProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { getDisplays } = useDashboardExtension()
|
||||
|
||||
return (
|
||||
<Container className="divide-y p-0">
|
||||
@@ -43,6 +45,9 @@ export const ProductAttributeSection = ({
|
||||
title={t("fields.countryOfOrigin")}
|
||||
value={getFormattedCountry(product.origin_country)}
|
||||
/>
|
||||
{getDisplays("product", "attributes").map((Component, i) => {
|
||||
return <Component key={i} data={product} />
|
||||
})}
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
+9
-1
@@ -1,11 +1,13 @@
|
||||
import { PencilSquare, Trash } from "@medusajs/icons"
|
||||
import { HttpTypes } from "@medusajs/types"
|
||||
import { Container, Heading, StatusBadge, usePrompt } from "@medusajs/ui"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { useNavigate } from "react-router-dom"
|
||||
|
||||
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||
import { SectionRow } from "../../../../../components/common/section"
|
||||
import { useDashboardExtension } from "../../../../../extensions"
|
||||
import { useDeleteProduct } from "../../../../../hooks/api/products"
|
||||
import { HttpTypes } from "@medusajs/types"
|
||||
|
||||
const productStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
@@ -32,6 +34,9 @@ export const ProductGeneralSection = ({
|
||||
const { t } = useTranslation()
|
||||
const prompt = usePrompt()
|
||||
const navigate = useNavigate()
|
||||
const { getDisplays } = useDashboardExtension()
|
||||
|
||||
const displays = getDisplays("product", "general")
|
||||
|
||||
const { mutateAsync } = useDeleteProduct(product.id)
|
||||
|
||||
@@ -96,6 +101,9 @@ export const ProductGeneralSection = ({
|
||||
title={t("fields.discountable")}
|
||||
value={product.discountable ? t("fields.true") : t("fields.false")}
|
||||
/>
|
||||
{displays.map((Component, index) => {
|
||||
return <Component key={index} data={product} />
|
||||
})}
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
+6
@@ -5,6 +5,7 @@ import { useTranslation } from "react-i18next"
|
||||
import { Link } from "react-router-dom"
|
||||
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||
import { SectionRow } from "../../../../../components/common/section"
|
||||
import { useDashboardExtension } from "../../../../../extensions"
|
||||
|
||||
type ProductOrganizationSectionProps = {
|
||||
product: HttpTypes.AdminProduct
|
||||
@@ -14,6 +15,7 @@ export const ProductOrganizationSection = ({
|
||||
product,
|
||||
}: ProductOrganizationSectionProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { getDisplays } = useDashboardExtension()
|
||||
|
||||
return (
|
||||
<Container className="divide-y p-0">
|
||||
@@ -84,6 +86,10 @@ export const ProductOrganizationSection = ({
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
|
||||
{getDisplays("product", "organize").map((Component, i) => {
|
||||
return <Component key={i} data={product} />
|
||||
})}
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
+10
-6
@@ -5,7 +5,10 @@ import { createColumnHelper } from "@tanstack/react-table"
|
||||
import { useMemo } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||
import {
|
||||
Action,
|
||||
ActionMenu,
|
||||
} from "../../../../../components/common/action-menu"
|
||||
import { PlaceholderCell } from "../../../../../components/table/table-cells/common/placeholder-cell"
|
||||
import { useDeleteVariant } from "../../../../../hooks/api/products"
|
||||
|
||||
@@ -89,7 +92,7 @@ const VariantActions = ({
|
||||
icon: <Component />,
|
||||
}
|
||||
: false,
|
||||
].filter(Boolean),
|
||||
].filter(Boolean) as Action[],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
@@ -104,10 +107,11 @@ export const useProductVariantTableColumns = (
|
||||
const { t } = useTranslation()
|
||||
|
||||
const optionColumns = useMemo(() => {
|
||||
if (!product) {
|
||||
if (!product?.options) {
|
||||
return []
|
||||
}
|
||||
return product.options?.map((option) => {
|
||||
|
||||
return product.options.map((option) => {
|
||||
return columnHelper.display({
|
||||
id: option.id,
|
||||
header: () => (
|
||||
@@ -116,8 +120,8 @@ export const useProductVariantTableColumns = (
|
||||
</div>
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const variantOpt: any = row.original.options.find(
|
||||
(opt: any) => opt.option_id === option.id
|
||||
const variantOpt = row.original.options?.find(
|
||||
(opt) => opt.option_id === option.id
|
||||
)
|
||||
if (!variantOpt) {
|
||||
return <PlaceholderCell />
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
export const PRODUCT_DETAIL_FIELDS = "*categories"
|
||||
import { getLinkedFields } from "../../../extensions"
|
||||
|
||||
export const PRODUCT_DETAIL_FIELDS = getLinkedFields("product", "*categories")
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useLoaderData, useParams } from "react-router-dom"
|
||||
|
||||
import { TwoColumnPageSkeleton } from "../../../components/common/skeleton"
|
||||
import { TwoColumnPage } from "../../../components/layout/pages"
|
||||
import { useProduct } from "../../../hooks/api/products"
|
||||
import { ProductAttributeSection } from "./components/product-attribute-section"
|
||||
@@ -12,11 +13,7 @@ import { ProductVariantSection } from "./components/product-variant-section"
|
||||
import { PRODUCT_DETAIL_FIELDS } from "./constants"
|
||||
import { productLoader } from "./loader"
|
||||
|
||||
import after from "virtual:medusa/widgets/product/details/after"
|
||||
import before from "virtual:medusa/widgets/product/details/before"
|
||||
import sideAfter from "virtual:medusa/widgets/product/details/side/after"
|
||||
import sideBefore from "virtual:medusa/widgets/product/details/side/before"
|
||||
import { TwoColumnPageSkeleton } from "../../../components/common/skeleton"
|
||||
import { useDashboardExtension } from "../../../extensions"
|
||||
|
||||
export const ProductDetail = () => {
|
||||
const initialData = useLoaderData() as Awaited<
|
||||
@@ -32,6 +29,13 @@ export const ProductDetail = () => {
|
||||
}
|
||||
)
|
||||
|
||||
const { getWidgets } = useDashboardExtension()
|
||||
|
||||
const after = getWidgets("product.details.after")
|
||||
const before = getWidgets("product.details.before")
|
||||
const sideAfter = getWidgets("product.details.side.after")
|
||||
const sideBefore = getWidgets("product.details.side.before")
|
||||
|
||||
if (isLoading || !product) {
|
||||
return (
|
||||
<TwoColumnPageSkeleton
|
||||
|
||||
+16
-8
@@ -1,19 +1,20 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { Button, Input, Select, Text, Textarea } from "@medusajs/ui"
|
||||
import { useForm } from "react-hook-form"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import * as zod from "zod"
|
||||
|
||||
import { HttpTypes } from "@medusajs/types"
|
||||
import { Form } from "../../../../../components/common/form"
|
||||
import { SwitchBox } from "../../../../../components/common/switch-box"
|
||||
import {
|
||||
RouteDrawer,
|
||||
useRouteModal,
|
||||
} from "../../../../../components/modals"
|
||||
import { RouteDrawer, useRouteModal } from "../../../../../components/modals"
|
||||
import { useExtendableForm } from "../../../../../extensions/forms/hooks"
|
||||
import { useUpdateProduct } from "../../../../../hooks/api/products"
|
||||
import { transformNullableFormData } from "../../../../../lib/form-helpers"
|
||||
|
||||
import {
|
||||
FormExtensionZone,
|
||||
useDashboardExtension,
|
||||
} from "../../../../../extensions"
|
||||
|
||||
type EditProductFormProps = {
|
||||
product: HttpTypes.AdminProduct
|
||||
}
|
||||
@@ -32,7 +33,11 @@ export const EditProductForm = ({ product }: EditProductFormProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { handleSuccess } = useRouteModal()
|
||||
|
||||
const form = useForm<zod.infer<typeof EditProductSchema>>({
|
||||
const { getFormFields, getFormConfigs } = useDashboardExtension()
|
||||
const fields = getFormFields("product", "edit")
|
||||
const configs = getFormConfigs("product", "edit")
|
||||
|
||||
const form = useExtendableForm({
|
||||
defaultValues: {
|
||||
status: product.status,
|
||||
title: product.title,
|
||||
@@ -42,7 +47,9 @@ export const EditProductForm = ({ product }: EditProductFormProps) => {
|
||||
description: product.description || "",
|
||||
discountable: product.discountable,
|
||||
},
|
||||
resolver: zodResolver(EditProductSchema),
|
||||
schema: EditProductSchema,
|
||||
configs: configs,
|
||||
data: product,
|
||||
})
|
||||
|
||||
const { mutateAsync, isPending } = useUpdateProduct(product.id)
|
||||
@@ -208,6 +215,7 @@ export const EditProductForm = ({ product }: EditProductFormProps) => {
|
||||
label={t("fields.discountable")}
|
||||
description={t("products.discountableHint")}
|
||||
/>
|
||||
<FormExtensionZone fields={fields} form={form} />
|
||||
</div>
|
||||
</RouteDrawer.Body>
|
||||
<RouteDrawer.Footer>
|
||||
|
||||
@@ -4,13 +4,16 @@ import { useParams } from "react-router-dom"
|
||||
|
||||
import { RouteDrawer } from "../../../components/modals"
|
||||
import { useProduct } from "../../../hooks/api/products"
|
||||
import { PRODUCT_DETAIL_FIELDS } from "../product-detail/constants"
|
||||
import { EditProductForm } from "./components/edit-product-form"
|
||||
|
||||
export const ProductEdit = () => {
|
||||
const { id } = useParams()
|
||||
const { t } = useTranslation()
|
||||
|
||||
const { product, isLoading, isError, error } = useProduct(id!)
|
||||
const { product, isLoading, isError, error } = useProduct(id!, {
|
||||
fields: PRODUCT_DETAIL_FIELDS,
|
||||
})
|
||||
|
||||
if (isError) {
|
||||
throw error
|
||||
@@ -19,7 +22,9 @@ export const ProductEdit = () => {
|
||||
return (
|
||||
<RouteDrawer>
|
||||
<RouteDrawer.Header>
|
||||
<Heading>{t("products.editProduct")}</Heading>
|
||||
<RouteDrawer.Title asChild>
|
||||
<Heading>{t("products.editProduct")}</Heading>
|
||||
</RouteDrawer.Title>
|
||||
</RouteDrawer.Header>
|
||||
{!isLoading && product && <EditProductForm product={product} />}
|
||||
</RouteDrawer>
|
||||
|
||||
@@ -1,26 +1,18 @@
|
||||
import { SingleColumnPage } from "../../../components/layout/pages"
|
||||
import { useDashboardExtension } from "../../../extensions"
|
||||
import { ProductListTable } from "./components/product-list-table"
|
||||
|
||||
import after from "virtual:medusa/widgets/product/list/after"
|
||||
import before from "virtual:medusa/widgets/product/list/before"
|
||||
|
||||
export const ProductList = () => {
|
||||
const { getWidgets } = useDashboardExtension()
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-y-2">
|
||||
{before.widgets.map((w, i) => {
|
||||
return (
|
||||
<div key={i}>
|
||||
<w.Component />
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
<SingleColumnPage
|
||||
widgets={{
|
||||
after: getWidgets("product.list.after"),
|
||||
before: getWidgets("product.list.before"),
|
||||
}}
|
||||
>
|
||||
<ProductListTable />
|
||||
{after.widgets.map((w, i) => {
|
||||
return (
|
||||
<div key={i}>
|
||||
<w.Component />
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</SingleColumnPage>
|
||||
)
|
||||
}
|
||||
+15
-4
@@ -1,12 +1,16 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { HttpTypes } from "@medusajs/types"
|
||||
import { Button, toast } 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 { Combobox } from "../../../../../components/inputs/combobox"
|
||||
import { RouteDrawer, useRouteModal } from "../../../../../components/modals"
|
||||
import {
|
||||
FormExtensionZone,
|
||||
useDashboardExtension,
|
||||
useExtendableForm,
|
||||
} from "../../../../../extensions"
|
||||
import { useUpdateProduct } from "../../../../../hooks/api/products"
|
||||
import { useComboboxData } from "../../../../../hooks/use-combobox-data"
|
||||
import { sdk } from "../../../../../lib/client"
|
||||
@@ -28,6 +32,10 @@ export const ProductOrganizationForm = ({
|
||||
}: ProductOrganizationFormProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { handleSuccess } = useRouteModal()
|
||||
const { getFormConfigs, getFormFields } = useDashboardExtension()
|
||||
|
||||
const configs = getFormConfigs("product", "organize")
|
||||
const fields = getFormFields("product", "organize")
|
||||
|
||||
const collections = useComboboxData({
|
||||
queryKey: ["product_collections"],
|
||||
@@ -59,14 +67,16 @@ export const ProductOrganizationForm = ({
|
||||
})),
|
||||
})
|
||||
|
||||
const form = useForm<zod.infer<typeof ProductOrganizationSchema>>({
|
||||
const form = useExtendableForm({
|
||||
defaultValues: {
|
||||
type_id: product.type_id || "",
|
||||
collection_id: product.collection_id || "",
|
||||
category_ids: product.categories?.map((c) => c.id) || [],
|
||||
tag_ids: product.tags?.map((t) => t.id) || [],
|
||||
},
|
||||
resolver: zodResolver(ProductOrganizationSchema),
|
||||
schema: ProductOrganizationSchema,
|
||||
configs: configs,
|
||||
data: product,
|
||||
})
|
||||
|
||||
const { mutateAsync, isPending } = useUpdateProduct(product.id)
|
||||
@@ -190,6 +200,7 @@ export const ProductOrganizationForm = ({
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<FormExtensionZone fields={fields} form={form} />
|
||||
</div>
|
||||
</RouteDrawer.Body>
|
||||
<RouteDrawer.Footer>
|
||||
|
||||
+3
-1
@@ -22,7 +22,9 @@ export const ProductOrganization = () => {
|
||||
return (
|
||||
<RouteDrawer>
|
||||
<RouteDrawer.Header>
|
||||
<Heading>{t("products.organization.edit.header")}</Heading>
|
||||
<RouteDrawer.Title asChild>
|
||||
<Heading>{t("products.organization.edit.header")}</Heading>
|
||||
</RouteDrawer.Title>
|
||||
</RouteDrawer.Header>
|
||||
{!isLoading && product && <ProductOrganizationForm product={product} />}
|
||||
</RouteDrawer>
|
||||
|
||||
+2
-2
@@ -1,12 +1,12 @@
|
||||
import { PencilSquare } from "@medusajs/icons"
|
||||
import { UserDTO } from "@medusajs/types"
|
||||
import { HttpTypes } from "@medusajs/types"
|
||||
import { Container, Heading, StatusBadge, Text } from "@medusajs/ui"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||
import { languages } from "../../../../../i18n/languages"
|
||||
|
||||
type ProfileGeneralSectionProps = {
|
||||
user: UserDTO
|
||||
user: HttpTypes.AdminUser
|
||||
}
|
||||
|
||||
export const ProfileGeneralSection = ({ user }: ProfileGeneralSectionProps) => {
|
||||
|
||||
@@ -1,40 +1,30 @@
|
||||
import { Outlet, json } from "react-router-dom"
|
||||
|
||||
import { useMe } from "../../../hooks/api/users"
|
||||
import { ProfileGeneralSection } from "./components/profile-general-section"
|
||||
|
||||
import after from "virtual:medusa/widgets/profile/details/after"
|
||||
import before from "virtual:medusa/widgets/profile/details/before"
|
||||
import { SingleColumnPageSkeleton } from "../../../components/common/skeleton"
|
||||
import { SingleColumnPage } from "../../../components/layout/pages"
|
||||
import { useDashboardExtension } from "../../../extensions"
|
||||
|
||||
export const ProfileDetail = () => {
|
||||
const { user, isPending: isLoading, isError, error } = useMe()
|
||||
const { getWidgets } = useDashboardExtension()
|
||||
|
||||
if (isLoading) {
|
||||
return <div>Loading...</div>
|
||||
if (isLoading || !user) {
|
||||
return <SingleColumnPageSkeleton sections={1} />
|
||||
}
|
||||
|
||||
if (isError || !user) {
|
||||
if (error) {
|
||||
throw error
|
||||
}
|
||||
|
||||
throw json("An unknown error has occurred", 500)
|
||||
if (isError) {
|
||||
throw error
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-y-2">
|
||||
{before.widgets.map((w, i) => (
|
||||
<div key={i}>
|
||||
<w.Component data={user} />
|
||||
</div>
|
||||
))}
|
||||
<SingleColumnPage
|
||||
widgets={{
|
||||
after: getWidgets("profile.details.after"),
|
||||
before: getWidgets("profile.details.before"),
|
||||
}}
|
||||
>
|
||||
<ProfileGeneralSection user={user} />
|
||||
{after.widgets.map((w, i) => (
|
||||
<div key={i}>
|
||||
<w.Component data={user} />
|
||||
</div>
|
||||
))}
|
||||
<Outlet />
|
||||
</div>
|
||||
</SingleColumnPage>
|
||||
)
|
||||
}
|
||||
+7
-9
@@ -2,17 +2,13 @@ import { useLoaderData, useParams } from "react-router-dom"
|
||||
|
||||
import { TwoColumnPageSkeleton } from "../../../components/common/skeleton"
|
||||
import { TwoColumnPage } from "../../../components/layout/pages"
|
||||
import { useDashboardExtension } from "../../../extensions"
|
||||
import { usePromotion, usePromotionRules } from "../../../hooks/api/promotions"
|
||||
import { CampaignSection } from "./components/campaign-section"
|
||||
import { PromotionConditionsSection } from "./components/promotion-conditions-section"
|
||||
import { PromotionGeneralSection } from "./components/promotion-general-section"
|
||||
import { promotionLoader } from "./loader"
|
||||
|
||||
import after from "virtual:medusa/widgets/promotion/details/after"
|
||||
import before from "virtual:medusa/widgets/promotion/details/before"
|
||||
import sideAfter from "virtual:medusa/widgets/promotion/details/side/after"
|
||||
import sideBefore from "virtual:medusa/widgets/promotion/details/side/before"
|
||||
|
||||
export const PromotionDetail = () => {
|
||||
const initialData = useLoaderData() as Awaited<
|
||||
ReturnType<typeof promotionLoader>
|
||||
@@ -30,6 +26,8 @@ export const PromotionDetail = () => {
|
||||
const { rules: targetRules } = usePromotionRules(id!, "target-rules", query)
|
||||
const { rules: buyRules } = usePromotionRules(id!, "buy-rules", query)
|
||||
|
||||
const { getWidgets } = useDashboardExtension()
|
||||
|
||||
if (isLoading || !promotion) {
|
||||
return (
|
||||
<TwoColumnPageSkeleton mainSections={3} sidebarSections={1} showJSON />
|
||||
@@ -40,10 +38,10 @@ export const PromotionDetail = () => {
|
||||
<TwoColumnPage
|
||||
data={promotion}
|
||||
widgets={{
|
||||
after,
|
||||
before,
|
||||
sideAfter,
|
||||
sideBefore,
|
||||
after: getWidgets("promotion.details.after"),
|
||||
before: getWidgets("promotion.details.before"),
|
||||
sideAfter: getWidgets("promotion.details.side.after"),
|
||||
sideBefore: getWidgets("promotion.details.side.before"),
|
||||
}}
|
||||
hasOutlet
|
||||
showJSON
|
||||
|
||||
@@ -1,26 +1,18 @@
|
||||
import { SingleColumnPage } from "../../../components/layout/pages"
|
||||
import { useDashboardExtension } from "../../../extensions"
|
||||
import { PromotionListTable } from "./components/promotion-list-table"
|
||||
|
||||
import after from "virtual:medusa/widgets/promotion/list/after"
|
||||
import before from "virtual:medusa/widgets/promotion/list/before"
|
||||
|
||||
export const PromotionsList = () => {
|
||||
const { getWidgets } = useDashboardExtension()
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-y-2">
|
||||
{before.widgets.map((w, i) => {
|
||||
return (
|
||||
<div key={i}>
|
||||
<w.Component />
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
<SingleColumnPage
|
||||
widgets={{
|
||||
before: getWidgets("promotion.list.before"),
|
||||
after: getWidgets("promotion.list.after"),
|
||||
}}
|
||||
>
|
||||
<PromotionListTable />
|
||||
{after.widgets.map((w, i) => {
|
||||
return (
|
||||
<div key={i}>
|
||||
<w.Component />
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</SingleColumnPage>
|
||||
)
|
||||
}
|
||||
@@ -1,16 +1,14 @@
|
||||
import { Outlet, useLoaderData, useParams } from "react-router-dom"
|
||||
import { useLoaderData, useParams } from "react-router-dom"
|
||||
|
||||
import { JsonViewSection } from "../../../components/common/json-view-section"
|
||||
import { useRegion } from "../../../hooks/api/regions"
|
||||
import { RegionCountrySection } from "./components/region-country-section"
|
||||
import { RegionGeneralSection } from "./components/region-general-section"
|
||||
import { regionLoader } from "./loader"
|
||||
|
||||
import after from "virtual:medusa/widgets/region/details/after"
|
||||
import before from "virtual:medusa/widgets/region/details/before"
|
||||
import { usePricePreferences } from "../../../hooks/api/price-preferences"
|
||||
import { SingleColumnPageSkeleton } from "../../../components/common/skeleton"
|
||||
import { SingleColumnPage } from "../../../components/layout/pages"
|
||||
import { UserGeneralSection } from "../../users/user-detail/components/user-general-section"
|
||||
import { useDashboardExtension } from "../../../extensions"
|
||||
import { usePricePreferences } from "../../../hooks/api/price-preferences"
|
||||
|
||||
export const RegionDetail = () => {
|
||||
const initialData = useLoaderData() as Awaited<
|
||||
@@ -44,9 +42,10 @@ export const RegionDetail = () => {
|
||||
{ enabled: !!region }
|
||||
)
|
||||
|
||||
// TODO: Move to loading.tsx and set as Suspense fallback for the route
|
||||
const { getWidgets } = useDashboardExtension()
|
||||
|
||||
if (isLoading || isLoadingPreferences || !region) {
|
||||
return <div>Loading...</div>
|
||||
return <SingleColumnPageSkeleton sections={2} showJSON showMetadata />
|
||||
}
|
||||
|
||||
if (isRegionError) {
|
||||
@@ -60,33 +59,18 @@ export const RegionDetail = () => {
|
||||
return (
|
||||
<SingleColumnPage
|
||||
widgets={{
|
||||
before,
|
||||
after,
|
||||
before: getWidgets("region.details.before"),
|
||||
after: getWidgets("region.details.after"),
|
||||
}}
|
||||
data={region}
|
||||
hasOutlet
|
||||
showMetadata
|
||||
showJSON
|
||||
>
|
||||
{before.widgets.map((w, i) => {
|
||||
return (
|
||||
<div key={i}>
|
||||
<w.Component data={region} />
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
<RegionGeneralSection
|
||||
region={region}
|
||||
pricePreferences={pricePreferences ?? []}
|
||||
/>
|
||||
<RegionCountrySection region={region} />
|
||||
{after.widgets.map((w, i) => {
|
||||
return (
|
||||
<div key={i}>
|
||||
<w.Component data={region} />
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</SingleColumnPage>
|
||||
)
|
||||
}
|
||||
@@ -1,29 +1,18 @@
|
||||
import { Outlet } from "react-router-dom"
|
||||
|
||||
import { SingleColumnPage } from "../../../components/layout/pages"
|
||||
import { useDashboardExtension } from "../../../extensions"
|
||||
import { RegionListTable } from "./components/region-list-table"
|
||||
|
||||
import after from "virtual:medusa/widgets/region/list/after"
|
||||
import before from "virtual:medusa/widgets/region/list/before"
|
||||
|
||||
export const RegionList = () => {
|
||||
const { getWidgets } = useDashboardExtension()
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-y-2">
|
||||
{before.widgets.map((w, i) => {
|
||||
return (
|
||||
<div key={i}>
|
||||
<w.Component />
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
<SingleColumnPage
|
||||
widgets={{
|
||||
before: getWidgets("region.list.before"),
|
||||
after: getWidgets("region.list.after"),
|
||||
}}
|
||||
>
|
||||
<RegionListTable />
|
||||
{after.widgets.map((w, i) => {
|
||||
return (
|
||||
<div key={i}>
|
||||
<w.Component />
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
<Outlet />
|
||||
</div>
|
||||
</SingleColumnPage>
|
||||
)
|
||||
}
|
||||
+20
-29
@@ -1,16 +1,13 @@
|
||||
import { useLoaderData, useParams } from "react-router-dom"
|
||||
|
||||
import { TwoColumnPageSkeleton } from "../../../components/common/skeleton"
|
||||
import { TwoColumnPage } from "../../../components/layout/pages"
|
||||
import { useDashboardExtension } from "../../../extensions"
|
||||
import { useReservationItem } from "../../../hooks/api/reservations"
|
||||
import { InventoryItemGeneralSection } from "../../inventory/inventory-detail/components/inventory-item-general-section"
|
||||
import { ReservationGeneralSection } from "./components/reservation-general-section"
|
||||
import { reservationItemLoader } from "./loader"
|
||||
|
||||
import after from "virtual:medusa/widgets/reservation/details/after"
|
||||
import before from "virtual:medusa/widgets/reservation/details/before"
|
||||
import sideAfter from "virtual:medusa/widgets/reservation/details/side/after"
|
||||
import sideBefore from "virtual:medusa/widgets/reservation/details/side/before"
|
||||
import { TwoColumnPage } from "../../../components/layout/pages"
|
||||
import { InventoryItemGeneralSection } from "../../inventory/inventory-detail/components/inventory-item-general-section"
|
||||
|
||||
export const ReservationDetail = () => {
|
||||
const { id } = useParams()
|
||||
|
||||
@@ -20,14 +17,23 @@ export const ReservationDetail = () => {
|
||||
|
||||
const { reservation, isLoading, isError, error } = useReservationItem(
|
||||
id!,
|
||||
{},
|
||||
undefined,
|
||||
{
|
||||
initialData,
|
||||
}
|
||||
)
|
||||
|
||||
const { getWidgets } = useDashboardExtension()
|
||||
|
||||
if (isLoading || !reservation) {
|
||||
return <div>Loading...</div>
|
||||
return (
|
||||
<TwoColumnPageSkeleton
|
||||
mainSections={1}
|
||||
sidebarSections={1}
|
||||
showJSON
|
||||
showMetadata
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
@@ -37,36 +43,21 @@ export const ReservationDetail = () => {
|
||||
return (
|
||||
<TwoColumnPage
|
||||
widgets={{
|
||||
after,
|
||||
before,
|
||||
sideAfter,
|
||||
sideBefore,
|
||||
before: getWidgets("reservation.details.before"),
|
||||
after: getWidgets("reservation.details.after"),
|
||||
sideBefore: getWidgets("reservation.details.side.before"),
|
||||
sideAfter: getWidgets("reservation.details.side.after"),
|
||||
}}
|
||||
data={reservation}
|
||||
showJSON
|
||||
showMetadata
|
||||
hasOutlet
|
||||
>
|
||||
{before.widgets.map((w, i) => {
|
||||
return (
|
||||
<div key={i}>
|
||||
<w.Component data={reservation} />
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
<TwoColumnPage.Main>
|
||||
<ReservationGeneralSection reservation={reservation} />
|
||||
{after.widgets.map((w, i) => {
|
||||
return (
|
||||
<div key={i}>
|
||||
<w.Component data={reservation} />
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</TwoColumnPage.Main>
|
||||
<TwoColumnPage.Sidebar>
|
||||
<InventoryItemGeneralSection
|
||||
inventoryItem={reservation.inventory_item!}
|
||||
inventoryItem={reservation.inventory_item}
|
||||
/>
|
||||
</TwoColumnPage.Sidebar>
|
||||
</TwoColumnPage>
|
||||
|
||||
+11
-21
@@ -1,28 +1,18 @@
|
||||
import { Outlet } from "react-router-dom"
|
||||
import { SingleColumnPage } from "../../../components/layout/pages"
|
||||
import { useDashboardExtension } from "../../../extensions"
|
||||
import { ReservationListTable } from "./components/reservation-list-table"
|
||||
|
||||
import after from "virtual:medusa/widgets/reservation/list/after"
|
||||
import before from "virtual:medusa/widgets/reservation/list/before"
|
||||
|
||||
export const ReservationList = () => {
|
||||
const { getWidgets } = useDashboardExtension()
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-y-2">
|
||||
{before.widgets.map((w, i) => {
|
||||
return (
|
||||
<div key={i}>
|
||||
<w.Component />
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
<SingleColumnPage
|
||||
widgets={{
|
||||
before: getWidgets("reservation.list.before"),
|
||||
after: getWidgets("reservation.list.after"),
|
||||
}}
|
||||
>
|
||||
<ReservationListTable />
|
||||
{after.widgets.map((w, i) => {
|
||||
return (
|
||||
<div key={i}>
|
||||
<w.Component key={i} />
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
<Outlet />
|
||||
</div>
|
||||
</SingleColumnPage>
|
||||
)
|
||||
}
|
||||
+5
-5
@@ -1,18 +1,18 @@
|
||||
import { SingleColumnPage } from "../../../components/layout/pages"
|
||||
import { useDashboardExtension } from "../../../extensions"
|
||||
import { ReturnReasonListTable } from "./components/return-reason-list-table"
|
||||
|
||||
import after from "virtual:medusa/widgets/return_reason/list/after"
|
||||
import before from "virtual:medusa/widgets/return_reason/list/before"
|
||||
|
||||
export const ReturnReasonList = () => {
|
||||
const { getWidgets } = useDashboardExtension()
|
||||
|
||||
return (
|
||||
<SingleColumnPage
|
||||
showMetadata={false}
|
||||
showJSON={false}
|
||||
hasOutlet
|
||||
widgets={{
|
||||
after,
|
||||
before,
|
||||
after: getWidgets("return_reason.list.after"),
|
||||
before: getWidgets("return_reason.list.before"),
|
||||
}}
|
||||
>
|
||||
<ReturnReasonListTable />
|
||||
|
||||
+11
-26
@@ -1,15 +1,13 @@
|
||||
import { Outlet, useLoaderData, useParams } from "react-router-dom"
|
||||
import { useLoaderData, useParams } from "react-router-dom"
|
||||
|
||||
import { JsonViewSection } from "../../../components/common/json-view-section"
|
||||
import { SingleColumnPageSkeleton } from "../../../components/common/skeleton"
|
||||
import { SingleColumnPage } from "../../../components/layout/pages"
|
||||
import { useDashboardExtension } from "../../../extensions"
|
||||
import { useSalesChannel } from "../../../hooks/api/sales-channels"
|
||||
import { SalesChannelGeneralSection } from "./components/sales-channel-general-section"
|
||||
import { SalesChannelProductSection } from "./components/sales-channel-product-section"
|
||||
import { salesChannelLoader } from "./loader"
|
||||
|
||||
import after from "virtual:medusa/widgets/sales_channel/details/after"
|
||||
import before from "virtual:medusa/widgets/sales_channel/details/before"
|
||||
import { SingleColumnPage } from "../../../components/layout/pages"
|
||||
|
||||
export const SalesChannelDetail = () => {
|
||||
const initialData = useLoaderData() as Awaited<
|
||||
ReturnType<typeof salesChannelLoader>
|
||||
@@ -20,37 +18,24 @@ export const SalesChannelDetail = () => {
|
||||
initialData,
|
||||
})
|
||||
|
||||
const { getWidgets } = useDashboardExtension()
|
||||
|
||||
if (isLoading || !sales_channel) {
|
||||
return <div>Loading...</div>
|
||||
return <SingleColumnPageSkeleton sections={2} showJSON showMetadata />
|
||||
}
|
||||
|
||||
return (
|
||||
<SingleColumnPage
|
||||
widgets={{
|
||||
before,
|
||||
after,
|
||||
before: getWidgets("sales_channel.details.before"),
|
||||
after: getWidgets("sales_channel.details.after"),
|
||||
}}
|
||||
data={sales_channel}
|
||||
hasOutlet
|
||||
showMetadata
|
||||
showJSON
|
||||
showMetadata
|
||||
data={sales_channel}
|
||||
>
|
||||
{before.widgets.map((w, i) => {
|
||||
return (
|
||||
<div key={i}>
|
||||
<w.Component data={sales_channel} />
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
<SalesChannelGeneralSection salesChannel={sales_channel} />
|
||||
<SalesChannelProductSection salesChannel={sales_channel} />
|
||||
{after.widgets.map((w, i) => {
|
||||
return (
|
||||
<div key={i}>
|
||||
<w.Component data={sales_channel} />
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</SingleColumnPage>
|
||||
)
|
||||
}
|
||||
+5
-4
@@ -1,15 +1,16 @@
|
||||
import { SalesChannelListTable } from "./components/sales-channel-list-table"
|
||||
|
||||
import after from "virtual:medusa/widgets/sales_channel/list/after"
|
||||
import before from "virtual:medusa/widgets/sales_channel/list/before"
|
||||
import { SingleColumnPage } from "../../../components/layout/pages"
|
||||
import { useDashboardExtension } from "../../../extensions"
|
||||
|
||||
export const SalesChannelList = () => {
|
||||
const { getWidgets } = useDashboardExtension()
|
||||
|
||||
return (
|
||||
<SingleColumnPage
|
||||
widgets={{
|
||||
before,
|
||||
after,
|
||||
before: getWidgets("sales_channel.list.before"),
|
||||
after: getWidgets("sales_channel.list.after"),
|
||||
}}
|
||||
hasOutlet
|
||||
>
|
||||
|
||||
+16
-30
@@ -1,15 +1,11 @@
|
||||
import { useParams } from "react-router-dom"
|
||||
|
||||
import { JsonViewSection } from "../../../components/common/json-view-section"
|
||||
import {
|
||||
GeneralSectionSkeleton,
|
||||
JsonViewSectionSkeleton,
|
||||
} from "../../../components/common/skeleton"
|
||||
import { SingleColumnPageSkeleton } from "../../../components/common/skeleton"
|
||||
import { useShippingProfile } from "../../../hooks/api/shipping-profiles"
|
||||
import { ShippingProfileGeneralSection } from "./components/shipping-profile-general-section"
|
||||
|
||||
import after from "virtual:medusa/widgets/shipping_profile/details/after"
|
||||
import before from "virtual:medusa/widgets/shipping_profile/details/before"
|
||||
import { SingleColumnPage } from "../../../components/layout/pages"
|
||||
import { useDashboardExtension } from "../../../extensions"
|
||||
|
||||
export const ShippingProfileDetail = () => {
|
||||
const { id } = useParams()
|
||||
@@ -18,13 +14,10 @@ export const ShippingProfileDetail = () => {
|
||||
id!
|
||||
)
|
||||
|
||||
const { getWidgets } = useDashboardExtension()
|
||||
|
||||
if (isLoading || !shipping_profile) {
|
||||
return (
|
||||
<div className="flex flex-col gap-y-2">
|
||||
<GeneralSectionSkeleton rowCount={1} />
|
||||
<JsonViewSectionSkeleton />
|
||||
</div>
|
||||
)
|
||||
return <SingleColumnPageSkeleton sections={1} showJSON showMetadata />
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
@@ -32,23 +25,16 @@ export const ShippingProfileDetail = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-y-3">
|
||||
{before.widgets.map((w, i) => {
|
||||
return (
|
||||
<div key={i}>
|
||||
<w.Component data={shipping_profile} />
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
<SingleColumnPage
|
||||
widgets={{
|
||||
before: getWidgets("shipping_profile.details.before"),
|
||||
after: getWidgets("shipping_profile.details.after"),
|
||||
}}
|
||||
showMetadata
|
||||
showJSON
|
||||
data={shipping_profile}
|
||||
>
|
||||
<ShippingProfileGeneralSection profile={shipping_profile} />
|
||||
{after.widgets.map((w, i) => {
|
||||
return (
|
||||
<div key={i}>
|
||||
<w.Component data={shipping_profile} />
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
<JsonViewSection data={shipping_profile} />
|
||||
</div>
|
||||
</SingleColumnPage>
|
||||
)
|
||||
}
|
||||
+11
-22
@@ -1,29 +1,18 @@
|
||||
import { Outlet } from "react-router-dom"
|
||||
|
||||
import { SingleColumnPage } from "../../../components/layout/pages"
|
||||
import { useDashboardExtension } from "../../../extensions"
|
||||
import { ShippingProfileListTable } from "./components/shipping-profile-list-table"
|
||||
|
||||
import after from "virtual:medusa/widgets/shipping_profile/list/after"
|
||||
import before from "virtual:medusa/widgets/shipping_profile/list/before"
|
||||
|
||||
export const ShippingProfileList = () => {
|
||||
const { getWidgets } = useDashboardExtension()
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-y-3">
|
||||
{before.widgets.map((w, i) => {
|
||||
return (
|
||||
<div key={i}>
|
||||
<w.Component />
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
<SingleColumnPage
|
||||
widgets={{
|
||||
before: getWidgets("shipping_profile.list.before"),
|
||||
after: getWidgets("shipping_profile.list.after"),
|
||||
}}
|
||||
>
|
||||
<ShippingProfileListTable />
|
||||
{after.widgets.map((w, i) => {
|
||||
return (
|
||||
<div key={i}>
|
||||
<w.Component />
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
<Outlet />
|
||||
</div>
|
||||
</SingleColumnPage>
|
||||
)
|
||||
}
|
||||
@@ -5,10 +5,9 @@ import { StoreCurrencySection } from "./components/store-currency-section/store-
|
||||
import { StoreGeneralSection } from "./components/store-general-section/index.ts"
|
||||
import { storeLoader } from "./loader.ts"
|
||||
|
||||
import after from "virtual:medusa/widgets/store/details/after"
|
||||
import before from "virtual:medusa/widgets/store/details/before"
|
||||
import { SingleColumnPageSkeleton } from "../../../components/common/skeleton/skeleton.tsx"
|
||||
import { SingleColumnPage } from "../../../components/layout/pages/index.ts"
|
||||
import { useDashboardExtension } from "../../../extensions/index.ts"
|
||||
|
||||
export const StoreDetail = () => {
|
||||
const initialData = useLoaderData() as Awaited<ReturnType<typeof storeLoader>>
|
||||
@@ -17,6 +16,8 @@ export const StoreDetail = () => {
|
||||
initialData,
|
||||
})
|
||||
|
||||
const { getWidgets } = useDashboardExtension()
|
||||
|
||||
if (isPending || !store) {
|
||||
return <SingleColumnPageSkeleton sections={2} showJSON showMetadata />
|
||||
}
|
||||
@@ -28,8 +29,8 @@ export const StoreDetail = () => {
|
||||
return (
|
||||
<SingleColumnPage
|
||||
widgets={{
|
||||
before,
|
||||
after,
|
||||
before: getWidgets("store.details.before"),
|
||||
after: getWidgets("store.details.after"),
|
||||
}}
|
||||
data={store}
|
||||
hasOutlet
|
||||
|
||||
+7
-6
@@ -6,8 +6,8 @@ import { TaxRegionDetailSection } from "./components/tax-region-detail-section"
|
||||
import { TaxRegionProvinceSection } from "./components/tax-region-province-section"
|
||||
|
||||
import { useState } from "react"
|
||||
import after from "virtual:medusa/widgets/tax/details/after"
|
||||
import before from "virtual:medusa/widgets/tax/details/before"
|
||||
import { SingleColumnPageSkeleton } from "../../../components/common/skeleton"
|
||||
import { useDashboardExtension } from "../../../extensions"
|
||||
import { TaxRegionOverrideSection } from "./components/tax-region-override-section"
|
||||
import { TaxRegionSublevelAlert } from "./components/tax-region-sublevel-alert"
|
||||
import { taxRegionLoader } from "./loader"
|
||||
@@ -27,8 +27,10 @@ export const TaxRegionDetail = () => {
|
||||
error,
|
||||
} = useTaxRegion(id!, undefined, { initialData })
|
||||
|
||||
const { getWidgets } = useDashboardExtension()
|
||||
|
||||
if (isLoading || !taxRegion) {
|
||||
return <div>Loading...</div>
|
||||
return <SingleColumnPageSkeleton sections={4} showJSON />
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
@@ -39,11 +41,10 @@ export const TaxRegionDetail = () => {
|
||||
<SingleColumnPage
|
||||
data={taxRegion}
|
||||
showJSON
|
||||
hasOutlet
|
||||
// showMetadata // TOOD -> enable when tax region update is added to the API
|
||||
widgets={{
|
||||
after,
|
||||
before,
|
||||
after: getWidgets("tax.details.after"),
|
||||
before: getWidgets("tax.details.before"),
|
||||
}}
|
||||
>
|
||||
<TaxRegionSublevelAlert
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { SingleColumnPage } from "../../../components/layout/pages"
|
||||
import { useDashboardExtension } from "../../../extensions"
|
||||
import { TaxRegionListView } from "./components/tax-region-list-view"
|
||||
|
||||
import after from "virtual:medusa/widgets/tax/list/after"
|
||||
import before from "virtual:medusa/widgets/tax/list/before"
|
||||
|
||||
export const TaxRegionsList = () => {
|
||||
const { getWidgets } = useDashboardExtension()
|
||||
|
||||
return (
|
||||
<SingleColumnPage
|
||||
widgets={{
|
||||
before,
|
||||
after,
|
||||
before: getWidgets("tax.list.before"),
|
||||
after: getWidgets("tax.list.after"),
|
||||
}}
|
||||
hasOutlet
|
||||
>
|
||||
|
||||
+7
-6
@@ -4,8 +4,8 @@ import { SingleColumnPage } from "../../../components/layout/pages"
|
||||
import { useTaxRegion } from "../../../hooks/api/tax-regions"
|
||||
import { TaxRegionProvinceDetailSection } from "./components/tax-region-province-detail-section"
|
||||
|
||||
import after from "virtual:medusa/widgets/tax/details/after"
|
||||
import before from "virtual:medusa/widgets/tax/details/before"
|
||||
import { SingleColumnPageSkeleton } from "../../../components/common/skeleton"
|
||||
import { useDashboardExtension } from "../../../extensions"
|
||||
import { TaxRegionProvinceOverrideSection } from "./components/tax-region-province-override-section"
|
||||
import { taxRegionLoader } from "./loader"
|
||||
|
||||
@@ -23,8 +23,10 @@ export const TaxRegionDetail = () => {
|
||||
error,
|
||||
} = useTaxRegion(province_id!, undefined, { initialData })
|
||||
|
||||
const { getWidgets } = useDashboardExtension()
|
||||
|
||||
if (isLoading || !taxRegion) {
|
||||
return <div>Loading...</div>
|
||||
return <SingleColumnPageSkeleton sections={2} showJSON />
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
@@ -35,10 +37,9 @@ export const TaxRegionDetail = () => {
|
||||
<SingleColumnPage
|
||||
data={taxRegion}
|
||||
showJSON
|
||||
hasOutlet
|
||||
widgets={{
|
||||
after,
|
||||
before,
|
||||
after: getWidgets("tax.details.after"),
|
||||
before: getWidgets("tax.details.before"),
|
||||
}}
|
||||
>
|
||||
<TaxRegionProvinceDetailSection taxRegion={taxRegion} />
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
import { PencilSquare, Trash } from "@medusajs/icons"
|
||||
import { UserDTO } from "@medusajs/types"
|
||||
import { HttpTypes } from "@medusajs/types"
|
||||
import { Container, Heading, Text, usePrompt } from "@medusajs/ui"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { useNavigate } from "react-router-dom"
|
||||
@@ -7,7 +7,7 @@ import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||
import { useDeleteUser } from "../../../../../hooks/api/users"
|
||||
|
||||
type UserGeneralSectionProps = {
|
||||
user: UserDTO
|
||||
user: HttpTypes.AdminUser
|
||||
}
|
||||
|
||||
export const UserGeneralSection = ({ user }: UserGeneralSectionProps) => {
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import { Outlet, json, useLoaderData, useParams } from "react-router-dom"
|
||||
import { useLoaderData, useParams } from "react-router-dom"
|
||||
|
||||
import { JsonViewSection } from "../../../components/common/json-view-section"
|
||||
import { useUser } from "../../../hooks/api/users"
|
||||
import { UserGeneralSection } from "./components/user-general-section"
|
||||
import { userLoader } from "./loader"
|
||||
|
||||
import after from "virtual:medusa/widgets/user/details/after"
|
||||
import before from "virtual:medusa/widgets/user/details/before"
|
||||
import { SingleColumnPageSkeleton } from "../../../components/common/skeleton"
|
||||
import { SingleColumnPage } from "../../../components/layout/pages"
|
||||
import { useDashboardExtension } from "../../../extensions"
|
||||
|
||||
export const UserDetail = () => {
|
||||
const initialData = useLoaderData() as Awaited<ReturnType<typeof userLoader>>
|
||||
@@ -22,40 +21,27 @@ export const UserDetail = () => {
|
||||
initialData,
|
||||
})
|
||||
|
||||
if (isLoading) {
|
||||
return <div>Loading...</div>
|
||||
const { getWidgets } = useDashboardExtension()
|
||||
|
||||
if (isLoading || !user) {
|
||||
return <SingleColumnPageSkeleton sections={1} showJSON showMetadata />
|
||||
}
|
||||
|
||||
if (isError || !user) {
|
||||
if (error) {
|
||||
throw error
|
||||
}
|
||||
|
||||
throw json("An unknown error has occured", 500)
|
||||
if (isError) {
|
||||
throw error
|
||||
}
|
||||
|
||||
return (
|
||||
<SingleColumnPage
|
||||
widgets={{
|
||||
before,
|
||||
after,
|
||||
}}
|
||||
data={user}
|
||||
hasOutlet
|
||||
showMetadata
|
||||
showJSON
|
||||
showMetadata
|
||||
widgets={{
|
||||
after: getWidgets("user.details.after"),
|
||||
before: getWidgets("user.details.before"),
|
||||
}}
|
||||
>
|
||||
{before.widgets.map((w, i) => (
|
||||
<div key={i}>
|
||||
<w.Component data={user} />
|
||||
</div>
|
||||
))}
|
||||
<UserGeneralSection user={user} />
|
||||
{after.widgets.map((w, i) => (
|
||||
<div key={i}>
|
||||
<w.Component data={user} />
|
||||
</div>
|
||||
))}
|
||||
</SingleColumnPage>
|
||||
)
|
||||
}
|
||||
@@ -1,25 +1,18 @@
|
||||
import { Outlet } from "react-router-dom"
|
||||
|
||||
import { SingleColumnPage } from "../../../components/layout/pages"
|
||||
import { useDashboardExtension } from "../../../extensions"
|
||||
import { UserListTable } from "./components/user-list-table"
|
||||
|
||||
import after from "virtual:medusa/widgets/user/list/after"
|
||||
import before from "virtual:medusa/widgets/user/list/before"
|
||||
|
||||
export const UserList = () => {
|
||||
const { getWidgets } = useDashboardExtension()
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-y-2">
|
||||
{before.widgets.map((w, i) => (
|
||||
<div key={i}>
|
||||
<w.Component />
|
||||
</div>
|
||||
))}
|
||||
<SingleColumnPage
|
||||
widgets={{
|
||||
after: getWidgets("user.list.after"),
|
||||
before: getWidgets("user.list.before"),
|
||||
}}
|
||||
>
|
||||
<UserListTable />
|
||||
{after.widgets.map((w, i) => (
|
||||
<div key={i}>
|
||||
<w.Component />
|
||||
</div>
|
||||
))}
|
||||
<Outlet />
|
||||
</div>
|
||||
</SingleColumnPage>
|
||||
)
|
||||
}
|
||||
+15
-22
@@ -1,23 +1,24 @@
|
||||
import { useParams } from "react-router-dom"
|
||||
|
||||
import { JsonViewSection } from "../../../components/common/json-view-section"
|
||||
import { SingleColumnPageSkeleton } from "../../../components/common/skeleton"
|
||||
import { SingleColumnPage } from "../../../components/layout/pages"
|
||||
import { useDashboardExtension } from "../../../extensions"
|
||||
import { useWorkflowExecution } from "../../../hooks/api/workflow-executions"
|
||||
import { WorkflowExecutionGeneralSection } from "./components/workflow-execution-general-section"
|
||||
import { WorkflowExecutionHistorySection } from "./components/workflow-execution-history-section"
|
||||
import { WorkflowExecutionPayloadSection } from "./components/workflow-execution-payload-section"
|
||||
import { WorkflowExecutionTimelineSection } from "./components/workflow-execution-timeline-section"
|
||||
|
||||
import after from "virtual:medusa/widgets/workflow/details/after"
|
||||
import before from "virtual:medusa/widgets/workflow/details/before"
|
||||
|
||||
export const ExecutionDetail = () => {
|
||||
const { id } = useParams()
|
||||
|
||||
const { workflow_execution, isLoading, isError, error } =
|
||||
useWorkflowExecution(id!)
|
||||
|
||||
const { getWidgets } = useDashboardExtension()
|
||||
|
||||
if (isLoading || !workflow_execution) {
|
||||
return <div>Loading...</div>
|
||||
return <SingleColumnPageSkeleton sections={4} showJSON />
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
@@ -25,26 +26,18 @@ export const ExecutionDetail = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-y-2">
|
||||
{before.widgets.map((w, i) => {
|
||||
return (
|
||||
<div key={i}>
|
||||
<w.Component execution={workflow_execution} />
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
<SingleColumnPage
|
||||
widgets={{
|
||||
after: getWidgets("workflow.details.after"),
|
||||
before: getWidgets("workflow.details.before"),
|
||||
}}
|
||||
data={workflow_execution}
|
||||
showJSON
|
||||
>
|
||||
<WorkflowExecutionGeneralSection execution={workflow_execution} />
|
||||
<WorkflowExecutionTimelineSection execution={workflow_execution} />
|
||||
<WorkflowExecutionPayloadSection execution={workflow_execution} />
|
||||
<WorkflowExecutionHistorySection execution={workflow_execution} />
|
||||
{after.widgets.map((w, i) => {
|
||||
return (
|
||||
<div key={i}>
|
||||
<w.Component execution={workflow_execution} />
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
<JsonViewSection data={workflow_execution} />
|
||||
</div>
|
||||
</SingleColumnPage>
|
||||
)
|
||||
}
|
||||
Loaded 100 of 159 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user