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
2024-04-11 10:44:13 +02:00
committed by GitHub
parent 27387b7cf1
commit 51acd1da5b
17 changed files with 96 additions and 320 deletions

View File

@@ -1,24 +1,18 @@
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 { ThemeProvider } from "./providers/theme-provider"
import { MEDUSA_BACKEND_URL, queryClient } from "./lib/medusa"
function App() {
return (
<MedusaProvider
baseUrl={MEDUSA_BACKEND_URL}
queryClientProviderProps={{
client: queryClient,
}}
>
<QueryClientProvider client={queryClient}>
<ThemeProvider>
<RouterProvider />
<Toaster />
</ThemeProvider>
</MedusaProvider>
</QueryClientProvider>
)
}

View File

@@ -0,0 +1 @@
export * from "./protected-route"

View File

@@ -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>
)
}

View File

@@ -5,6 +5,9 @@ import { useAdminGetSession } from "medusa-react"
import { SearchProvider } from "../../../providers/search-provider"
import { SidebarProvider } from "../../../providers/sidebar-provider"
/**
* @deprecated - Delete once all V1 domains have been migrated to V2.
*/
export const ProtectedRoute = () => {
const { user, isLoading } = useAdminGetSession()
const location = useLocation()

View File

@@ -1 +0,0 @@
export * from "./main-layout"

View File

@@ -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>
)
}

View File

@@ -11,12 +11,12 @@ import {
} from "@medusajs/icons"
import { Avatar, Text } from "@medusajs/ui"
import * as Collapsible from "@radix-ui/react-collapsible"
import { useAdminStore } from "medusa-react"
import { useTranslation } from "react-i18next"
import { useStore } from "../../../hooks/api/store"
import { Skeleton } from "../../common/skeleton"
import { NavItem, NavItemProps } from "../nav-item"
import { Shell } from "../shell"
import { NavItem, NavItemProps } from "../../layout/nav-item"
import { Shell } from "../../layout/shell"
import extensions from "medusa-admin:routes/links"
@@ -46,7 +46,7 @@ const MainSidebar = () => {
}
const Header = () => {
const { store, isError, error } = useAdminStore()
const { store, isError, error } = useStore()
const name = store?.name
const fallback = store?.name?.slice(0, 1).toUpperCase()
@@ -135,8 +135,8 @@ const useCoreRoutes = (): Omit<NavItemProps, "pathname">[] => {
},
{
icon: <ReceiptPercent />,
label: t("discounts.domain"),
to: "/discounts",
label: t("promotions.domain"),
to: "/promotions",
},
{
icon: <CurrencyDollar />,

View File

@@ -105,7 +105,6 @@ const Breadcrumbs = () => {
</span>
</div>
)}
{/* {!isLast && <TriangleRightMini className="-mt-0.5 mx-2" />} */}
{!isLast && <span className="mx-2 -mt-0.5"></span>}
</li>
)
@@ -140,7 +139,7 @@ const UserBadge = () => {
<button
disabled={!user}
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 ? (
@@ -384,7 +383,7 @@ const MobileSidebarContainer = ({ children }: PropsWithChildren) => {
<Dialog.Root open={mobile} onOpenChange={() => toggle("mobile")}>
<Dialog.Portal>
<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}
</Dialog.Content>
</Dialog.Portal>

View File

@@ -28,7 +28,8 @@
}
:root {
@apply bg-ui-bg-subtle text-ui-fg-base;
@apply bg-ui-bg-subtle text-ui-fg-base antialiased;
text-rendering: optimizeLegibility;
}
}

View File

@@ -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)
}
}

View File

@@ -1,4 +0,0 @@
export function upperCaseFirst(str: string): string {
return str.charAt(0).toUpperCase() + str.slice(1)
}

View File

@@ -1,8 +1,8 @@
import { createContext } from "react";
import { Feature } from "./types";
import { createContext } from "react"
import { Feature } from "./types"
type FeatureContextValue = {
isFeatureEnabled: (feature: Feature) => boolean;
};
isFeatureEnabled: (feature: Feature) => boolean
}
export const FeatureContext = createContext<FeatureContextValue | null>(null);
export const FeatureContext = createContext<FeatureContextValue | null>(null)

View File

@@ -1,33 +1,33 @@
import { useAdminStore } from "medusa-react";
import { PropsWithChildren, useEffect, useState } from "react";
import { FeatureContext } from "./feature-context";
import { Feature } from "./types";
import { useAdminStore } from "medusa-react"
import { PropsWithChildren, useEffect, useState } from "react"
import { FeatureContext } from "./feature-context"
import { Feature } from "./types"
export const FeatureProvider = ({ children }: PropsWithChildren) => {
const { store, isLoading } = useAdminStore();
const [features, setFeatures] = useState<Feature[]>([]);
const { store, isLoading } = useAdminStore()
const [features, setFeatures] = useState<Feature[]>([])
useEffect(() => {
if (!store || isLoading) {
return;
return
}
const flags = store.feature_flags
.filter((f) => f.value === true)
.map((f) => f.key);
const modules = store.modules.map((m) => m.module);
const enabled = flags.concat(modules);
.map((f) => f.key)
const modules = store.modules.map((m) => m.module)
const enabled = flags.concat(modules)
setFeatures(enabled as Feature[]);
}, [store, isLoading]);
setFeatures(enabled as Feature[])
}, [store, isLoading])
function isFeatureEnabled(feature: Feature) {
return features.includes(feature);
return features.includes(feature)
}
return (
<FeatureContext.Provider value={{ isFeatureEnabled }}>
{children}
</FeatureContext.Provider>
);
};
)
}

View File

@@ -5,12 +5,12 @@ const featureFlags = [
"publishable_api_keys",
"sales_channels",
"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

View File

@@ -1,3 +1,10 @@
import { AdminCustomersRes } from "@medusajs/client-types"
import {
AdminCollectionsRes,
AdminProductsRes,
AdminPromotionRes,
AdminRegionsRes,
} from "@medusajs/medusa"
import {
AdminApiKeyResponse,
AdminCustomerGroupResponse,
@@ -5,49 +12,13 @@ import {
SalesChannelDTO,
UserDTO,
} from "@medusajs/types"
import {
AdminCollectionsRes,
AdminProductsRes,
AdminPromotionRes,
AdminRegionsRes,
} from "@medusajs/medusa"
import { Navigate, Outlet, RouteObject, useLocation } from "react-router-dom"
import { Outlet, RouteObject } 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 { InventoryItemRes } from "../../types/api-responses"
import { MainLayout } from "../../components/layout-v2/main-layout"
import { PriceListRes } from "../../types/api-responses"
import { SearchProvider } from "../search-provider"
import { MainLayout } from "../../components/layout/main-layout"
import { SettingsLayout } from "../../components/layout/settings-layout"
import { SidebarProvider } from "../sidebar-provider"
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>
)
}
import { InventoryItemRes, PriceListRes } from "../../types/api-responses"
/**
* Experimental V2 routes.
@@ -473,9 +444,6 @@ export const v2Routes: RouteObject[] = [
{
path: "/settings",
element: <SettingsLayout />,
handle: {
crumb: () => "Settings",
},
children: [
{
index: true,

View File

@@ -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"
export const SidebarProvider = ({ children }: PropsWithChildren) => {
const [desktop, setDesktop] = useState(true)
const [mobile, setMobile] = useState(false)
const { pathname } = useLocation()
const toggle = (view: "desktop" | "mobile") => {
if (view === "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 (
<SidebarContext.Provider value={{ desktop, mobile, toggle }}>
{children}

View File

@@ -1,7 +1,7 @@
// / <reference types="vite/client" />
interface ImportMetaEnv {
readonly MEDUSA_ADMIN_BACKEND_URL: string
readonly VITE_MEDUSA_ADMIN_BACKEND_URL: string
readonly VITE_MEDUSA_V2: "true" | "false"
}