fix(dashboard): Minor cleanup and improve text legibility (#7045)

**What**
- Cleans up some artifacts from the V1 -> V2 migrations.
- Removes the MedusaProvider from the root in favor of a plain QueryClient.
- Applies font styles to make the text in admin resemble designs in Figma more closely.
This commit is contained in:
Kasper Fabricius Kristensen authored and GitHub committed 2024-04-11 08:44:13 +00:00
1 parent 27387b7cf1
commit 51acd1da5b
17 files changed
+96 -320

No files matched your search

+4 -10
View File
@@ -1,24 +1,18 @@
import { Toaster } from "@medusajs/ui" import { Toaster } from "@medusajs/ui"
import { MedusaProvider } from "medusa-react" import { QueryClientProvider } from "@tanstack/react-query"
import { queryClient } from "./lib/medusa"
import { RouterProvider } from "./providers/router-provider" import { RouterProvider } from "./providers/router-provider"
import { ThemeProvider } from "./providers/theme-provider" import { ThemeProvider } from "./providers/theme-provider"
import { MEDUSA_BACKEND_URL, queryClient } from "./lib/medusa"
function App() { function App() {
return ( return (
<MedusaProvider <QueryClientProvider client={queryClient}>
baseUrl={MEDUSA_BACKEND_URL}
queryClientProviderProps={{
client: queryClient,
}}
>
<ThemeProvider> <ThemeProvider>
<RouterProvider /> <RouterProvider />
<Toaster /> <Toaster />
</ThemeProvider> </ThemeProvider>
</MedusaProvider> </QueryClientProvider>
) )
} }
@@ -0,0 +1 @@
export * from "./protected-route"
@@ -0,0 +1,30 @@
import { Spinner } from "@medusajs/icons"
import { Navigate, Outlet, useLocation } from "react-router-dom"
import { useMe } from "../../../hooks/api/users"
import { SearchProvider } from "../../../providers/search-provider"
import { SidebarProvider } from "../../../providers/sidebar-provider"
export const ProtectedRoute = () => {
const { user, isLoading } = useMe()
const location = useLocation()
if (isLoading) {
return (
<div className="flex min-h-screen items-center justify-center">
<Spinner className="text-ui-fg-interactive animate-spin" />
</div>
)
}
if (!user) {
return <Navigate to="/login" state={{ from: location }} replace />
}
return (
<SidebarProvider>
<SearchProvider>
<Outlet />
</SearchProvider>
</SidebarProvider>
)
}
@@ -5,6 +5,9 @@ import { useAdminGetSession } from "medusa-react"
import { SearchProvider } from "../../../providers/search-provider" import { SearchProvider } from "../../../providers/search-provider"
import { SidebarProvider } from "../../../providers/sidebar-provider" import { SidebarProvider } from "../../../providers/sidebar-provider"
/**
* @deprecated - Delete once all V1 domains have been migrated to V2.
*/
export const ProtectedRoute = () => { export const ProtectedRoute = () => {
const { user, isLoading } = useAdminGetSession() const { user, isLoading } = useAdminGetSession()
const location = useLocation() const location = useLocation()
@@ -1 +0,0 @@
export * from "./main-layout"
@@ -1,205 +0,0 @@
import {
Buildings,
ChevronDownMini,
CurrencyDollar,
MinusMini,
ReceiptPercent,
ShoppingCart,
SquaresPlus,
Tag,
Users,
} from "@medusajs/icons"
import { Avatar, Text } from "@medusajs/ui"
import * as Collapsible from "@radix-ui/react-collapsible"
import { useTranslation } from "react-i18next"
import { Skeleton } from "../../common/skeleton"
import { NavItem, NavItemProps } from "../../layout/nav-item"
import { Shell } from "../../layout/shell"
import extensions from "medusa-admin:routes/links"
import { useStore } from "../../../hooks/api/store"
export const MainLayout = () => {
return (
<Shell>
<MainSidebar />
</Shell>
)
}
const MainSidebar = () => {
return (
<aside className="flex flex-1 flex-col justify-between overflow-y-auto">
<div className="flex flex-1 flex-col">
<div className="bg-ui-bg-subtle sticky top-0">
<Header />
<div className="px-3">
<div className="border-ui-border-strong h-px w-full border-b border-dashed" />
</div>
</div>
<CoreRouteSection />
<ExtensionRouteSection />
</div>
</aside>
)
}
const Header = () => {
const { store, isError, error } = useStore()
const name = store?.name
const fallback = store?.name?.slice(0, 1).toUpperCase()
if (isError) {
throw error
}
return (
<div className="w-full px-3 py-2">
<div className="flex items-center p-1 md:pr-2">
<div className="flex items-center gap-x-3">
{fallback ? (
<Avatar variant="squared" fallback={fallback} />
) : (
<Skeleton className="h-8 w-8 rounded-md" />
)}
{name ? (
<Text size="small" weight="plus" leading="compact">
{store.name}
</Text>
) : (
<Skeleton className="h-[9px] w-[120px]" />
)}
</div>
</div>
</div>
)
}
const useCoreRoutes = (): Omit<NavItemProps, "pathname">[] => {
const { t } = useTranslation()
return [
{
icon: <ShoppingCart />,
label: t("orders.domain"),
to: "/orders",
items: [
{
label: t("draftOrders.domain"),
to: "/draft-orders",
},
],
},
{
icon: <Tag />,
label: t("products.domain"),
to: "/products",
items: [
{
label: t("collections.domain"),
to: "/collections",
},
{
label: t("categories.domain"),
to: "/categories",
},
{
label: t("giftCards.domain"),
to: "/gift-cards",
},
],
},
{
icon: <Buildings />,
label: t("inventory.domain"),
to: "/inventory",
items: [
{
label: t("reservations.domain"),
to: "/reservations",
},
],
},
{
icon: <Users />,
label: t("customers.domain"),
to: "/customers",
items: [
{
label: t("customerGroups.domain"),
to: "/customer-groups",
},
],
},
{
icon: <ReceiptPercent />,
label: t("promotions.domain"),
to: "/promotions",
},
{
icon: <CurrencyDollar />,
label: t("pricing.domain"),
to: "/pricing",
},
]
}
const CoreRouteSection = () => {
const coreRoutes = useCoreRoutes()
return (
<nav className="flex flex-col gap-y-1 py-2">
{coreRoutes.map((route) => {
return <NavItem key={route.to} {...route} />
})}
</nav>
)
}
const ExtensionRouteSection = () => {
if (!extensions.links || extensions.links.length === 0) {
return null
}
return (
<div>
<div className="px-3">
<div className="border-ui-border-strong h-px w-full border-b border-dashed" />
</div>
<div className="flex flex-col gap-y-1 py-2">
<Collapsible.Root defaultOpen>
<div className="px-4">
<Collapsible.Trigger asChild className="group/trigger">
<button className="text-ui-fg-subtle flex w-full items-center justify-between px-2">
<Text size="xsmall" weight="plus" leading="compact">
Extensions
</Text>
<div className="text-ui-fg-muted">
<ChevronDownMini className="group-data-[state=open]/trigger:hidden" />
<MinusMini className="group-data-[state=closed]/trigger:hidden" />
</div>
</button>
</Collapsible.Trigger>
</div>
<Collapsible.Content>
<div className="flex flex-col gap-y-1 py-1 pb-4">
{extensions.links.map((link) => {
return (
<NavItem
key={link.path}
to={link.path}
label={link.label}
icon={link.icon ? <link.icon /> : <SquaresPlus />}
type="extension"
/>
)
})}
</div>
</Collapsible.Content>
</Collapsible.Root>
</div>
</div>
)
}
@@ -11,12 +11,12 @@ import {
} from "@medusajs/icons" } from "@medusajs/icons"
import { Avatar, Text } from "@medusajs/ui" import { Avatar, Text } from "@medusajs/ui"
import * as Collapsible from "@radix-ui/react-collapsible" import * as Collapsible from "@radix-ui/react-collapsible"
import { useAdminStore } from "medusa-react"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
import { useStore } from "../../../hooks/api/store"
import { Skeleton } from "../../common/skeleton" import { Skeleton } from "../../common/skeleton"
import { NavItem, NavItemProps } from "../nav-item" import { NavItem, NavItemProps } from "../../layout/nav-item"
import { Shell } from "../shell" import { Shell } from "../../layout/shell"
import extensions from "medusa-admin:routes/links" import extensions from "medusa-admin:routes/links"
@@ -46,7 +46,7 @@ const MainSidebar = () => {
} }
const Header = () => { const Header = () => {
const { store, isError, error } = useAdminStore() const { store, isError, error } = useStore()
const name = store?.name const name = store?.name
const fallback = store?.name?.slice(0, 1).toUpperCase() const fallback = store?.name?.slice(0, 1).toUpperCase()
@@ -135,8 +135,8 @@ const useCoreRoutes = (): Omit<NavItemProps, "pathname">[] => {
}, },
{ {
icon: <ReceiptPercent />, icon: <ReceiptPercent />,
label: t("discounts.domain"), label: t("promotions.domain"),
to: "/discounts", to: "/promotions",
}, },
{ {
icon: <CurrencyDollar />, icon: <CurrencyDollar />,
@@ -105,7 +105,6 @@ const Breadcrumbs = () => {
</span> </span>
</div> </div>
)} )}
{/* {!isLast && <TriangleRightMini className="-mt-0.5 mx-2" />} */}
{!isLast && <span className="mx-2 -mt-0.5">›</span>} {!isLast && <span className="mx-2 -mt-0.5">›</span>}
</li> </li>
) )
@@ -140,7 +139,7 @@ const UserBadge = () => {
<button <button
disabled={!user} disabled={!user}
className={clx( className={clx(
"shadow-borders-base flex max-w-[192px] select-none items-center gap-x-2 overflow-hidden text-ellipsis whitespace-nowrap rounded-full py-1 pl-1 pr-2.5" "shadow-borders-base flex max-w-[192px] select-none items-center gap-x-2 overflow-hidden text-ellipsis whitespace-nowrap rounded-full py-1 pl-1 pr-2.5 outline-none"
)} )}
> >
{fallback ? ( {fallback ? (
@@ -384,7 +383,7 @@ const MobileSidebarContainer = ({ children }: PropsWithChildren) => {
<Dialog.Root open={mobile} onOpenChange={() => toggle("mobile")}> <Dialog.Root open={mobile} onOpenChange={() => toggle("mobile")}>
<Dialog.Portal> <Dialog.Portal>
<Dialog.Overlay className="bg-ui-bg-overlay fixed inset-0" /> <Dialog.Overlay className="bg-ui-bg-overlay fixed inset-0" />
<Dialog.Content className="bg-ui-bg-subtle fixed inset-y-0 left-0 h-screen w-[220px] border-r"> <Dialog.Content className="bg-ui-bg-subtle fixed inset-y-0 left-0 h-screen w-full max-w-[240px] border-r">
{children} {children}
</Dialog.Content> </Dialog.Content>
</Dialog.Portal> </Dialog.Portal>
+2 -1
View File
@@ -28,7 +28,8 @@
} }
:root { :root {
@apply bg-ui-bg-subtle text-ui-fg-base; @apply bg-ui-bg-subtle text-ui-fg-base antialiased;
text-rendering: optimizeLegibility;
} }
} }
@@ -1,20 +0,0 @@
type Options = {
leading?: boolean
}
export const debounce = (
func: (...args: any[]) => void,
delay: number,
{ leading }: Options = {}
): ((...args: any[]) => void) => {
let timerId: NodeJS.Timeout | undefined
return (...args) => {
if (!timerId && leading) {
func(...args)
}
clearTimeout(timerId)
timerId = setTimeout(() => func(...args), delay)
}
}
@@ -1,4 +0,0 @@
export function upperCaseFirst(str: string): string {
return str.charAt(0).toUpperCase() + str.slice(1)
}
@@ -1,8 +1,8 @@
import { createContext } from "react"; import { createContext } from "react"
import { Feature } from "./types"; import { Feature } from "./types"
type FeatureContextValue = { type FeatureContextValue = {
isFeatureEnabled: (feature: Feature) => boolean; isFeatureEnabled: (feature: Feature) => boolean
}; }
export const FeatureContext = createContext<FeatureContextValue | null>(null); export const FeatureContext = createContext<FeatureContextValue | null>(null)
@@ -1,33 +1,33 @@
import { useAdminStore } from "medusa-react"; import { useAdminStore } from "medusa-react"
import { PropsWithChildren, useEffect, useState } from "react"; import { PropsWithChildren, useEffect, useState } from "react"
import { FeatureContext } from "./feature-context"; import { FeatureContext } from "./feature-context"
import { Feature } from "./types"; import { Feature } from "./types"
export const FeatureProvider = ({ children }: PropsWithChildren) => { export const FeatureProvider = ({ children }: PropsWithChildren) => {
const { store, isLoading } = useAdminStore(); const { store, isLoading } = useAdminStore()
const [features, setFeatures] = useState<Feature[]>([]); const [features, setFeatures] = useState<Feature[]>([])
useEffect(() => { useEffect(() => {
if (!store || isLoading) { if (!store || isLoading) {
return; return
} }
const flags = store.feature_flags const flags = store.feature_flags
.filter((f) => f.value === true) .filter((f) => f.value === true)
.map((f) => f.key); .map((f) => f.key)
const modules = store.modules.map((m) => m.module); const modules = store.modules.map((m) => m.module)
const enabled = flags.concat(modules); const enabled = flags.concat(modules)
setFeatures(enabled as Feature[]); setFeatures(enabled as Feature[])
}, [store, isLoading]); }, [store, isLoading])
function isFeatureEnabled(feature: Feature) { function isFeatureEnabled(feature: Feature) {
return features.includes(feature); return features.includes(feature)
} }
return ( return (
<FeatureContext.Provider value={{ isFeatureEnabled }}> <FeatureContext.Provider value={{ isFeatureEnabled }}>
{children} {children}
</FeatureContext.Provider> </FeatureContext.Provider>
); )
}; }
@@ -5,12 +5,12 @@ const featureFlags = [
"publishable_api_keys", "publishable_api_keys",
"sales_channels", "sales_channels",
"tax_inclusive_pricing", "tax_inclusive_pricing",
] as const; ] as const
type FeatureFlag = (typeof featureFlags)[number]; type FeatureFlag = (typeof featureFlags)[number]
const modules = ["inventory"] as const; const modules = ["inventory"] as const
type Module = (typeof modules)[number]; type Module = (typeof modules)[number]
export type Feature = FeatureFlag | Module; export type Feature = FeatureFlag | Module
@@ -1,3 +1,10 @@
import { AdminCustomersRes } from "@medusajs/client-types"
import {
AdminCollectionsRes,
AdminProductsRes,
AdminPromotionRes,
AdminRegionsRes,
} from "@medusajs/medusa"
import { import {
AdminApiKeyResponse, AdminApiKeyResponse,
AdminCustomerGroupResponse, AdminCustomerGroupResponse,
@@ -5,49 +12,13 @@ import {
SalesChannelDTO, SalesChannelDTO,
UserDTO, UserDTO,
} from "@medusajs/types" } from "@medusajs/types"
import { import { Outlet, RouteObject } from "react-router-dom"
AdminCollectionsRes,
AdminProductsRes,
AdminPromotionRes,
AdminRegionsRes,
} from "@medusajs/medusa"
import { Navigate, Outlet, RouteObject, useLocation } from "react-router-dom"
import { AdminCustomersRes } from "@medusajs/client-types" import { ProtectedRoute } from "../../components/authentication/protected-route"
import { ErrorBoundary } from "../../components/error/error-boundary" import { ErrorBoundary } from "../../components/error/error-boundary"
import { InventoryItemRes } from "../../types/api-responses" import { MainLayout } from "../../components/layout/main-layout"
import { MainLayout } from "../../components/layout-v2/main-layout"
import { PriceListRes } from "../../types/api-responses"
import { SearchProvider } from "../search-provider"
import { SettingsLayout } from "../../components/layout/settings-layout" import { SettingsLayout } from "../../components/layout/settings-layout"
import { SidebarProvider } from "../sidebar-provider" import { InventoryItemRes, PriceListRes } from "../../types/api-responses"
import { Spinner } from "@medusajs/icons"
import { useMe } from "../../hooks/api/users"
export const ProtectedRoute = () => {
const { user, isLoading } = useMe()
const location = useLocation()
if (isLoading) {
return (
<div className="flex min-h-screen items-center justify-center">
<Spinner className="text-ui-fg-interactive animate-spin" />
</div>
)
}
if (!user) {
return <Navigate to="/login" state={{ from: location }} replace />
}
return (
<SidebarProvider>
<SearchProvider>
<Outlet />
</SearchProvider>
</SidebarProvider>
)
}
/** /**
* Experimental V2 routes. * Experimental V2 routes.
@@ -473,9 +444,6 @@ export const v2Routes: RouteObject[] = [
{ {
path: "/settings", path: "/settings",
element: <SettingsLayout />, element: <SettingsLayout />,
handle: {
crumb: () => "Settings",
},
children: [ children: [
{ {
index: true, index: true,
@@ -1,10 +1,13 @@
import { PropsWithChildren, useState } from "react" import { PropsWithChildren, useEffect, useState } from "react"
import { useLocation } from "react-router-dom"
import { SidebarContext } from "./sidebar-context" import { SidebarContext } from "./sidebar-context"
export const SidebarProvider = ({ children }: PropsWithChildren) => { export const SidebarProvider = ({ children }: PropsWithChildren) => {
const [desktop, setDesktop] = useState(true) const [desktop, setDesktop] = useState(true)
const [mobile, setMobile] = useState(false) const [mobile, setMobile] = useState(false)
const { pathname } = useLocation()
const toggle = (view: "desktop" | "mobile") => { const toggle = (view: "desktop" | "mobile") => {
if (view === "desktop") { if (view === "desktop") {
setDesktop(!desktop) setDesktop(!desktop)
@@ -13,6 +16,13 @@ export const SidebarProvider = ({ children }: PropsWithChildren) => {
} }
} }
// close the mobile sidebar on route change
// this is to prevent the sidebar from staying open
// when navigating to a new page
useEffect(() => {
setMobile(false)
}, [pathname])
return ( return (
<SidebarContext.Provider value={{ desktop, mobile, toggle }}> <SidebarContext.Provider value={{ desktop, mobile, toggle }}>
{children} {children}
+1 -1
View File
@@ -1,7 +1,7 @@
// / <reference types="vite/client" /> // / <reference types="vite/client" />
interface ImportMetaEnv { interface ImportMetaEnv {
readonly MEDUSA_ADMIN_BACKEND_URL: string readonly VITE_MEDUSA_ADMIN_BACKEND_URL: string
readonly VITE_MEDUSA_V2: "true" | "false" readonly VITE_MEDUSA_V2: "true" | "false"
} }