feat(dashboard,ui): Streamline spacing and sizing (#6061)
This commit is contained in:
1 parent
5dacd4ac9f
commit
a2c149e7e5
266 files changed
+10702
-4610
No files matched your search
@@ -0,0 +1,9 @@
|
|||||||
|
---
|
||||||
|
"@medusajs/ui": minor
|
||||||
|
"@medusajs/ui-preset": minor
|
||||||
|
"@medusajs/icons": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
feat(ui): Updates spacing and sizing of components. Introduces new `size` variants for some components, such as `Button`, `IconButton`, and `Avatar`. Change most `:focus` styles to `:focus-visible` styles, to prevenent focus styles from being visible when not needed, such as on button clicks.
|
||||||
|
feat(ui-preset): Publishes latest updates to our design system styles, as well as adding new colors. Noticable changes include changing `ui-code-text-*` styles to `ui-code-fg-*` for better consistency.
|
||||||
|
feat(icons): Updates the `LockClosedSolid` and `LockOpenSolid` icons, and introduces four new icons: `LockClosedSolidMini`, `TriangleLeftMini`, `TriangleRightMini`, and `TriangleMini`.
|
||||||
+2
-2
@@ -227,7 +227,7 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
files: ["packages/admin-next/dashboard/**/*"],
|
files: ["packages/admin-next/dashboard/src/**/*.{ts,tsx}"],
|
||||||
env: { browser: true, es2020: true, node: true },
|
env: { browser: true, es2020: true, node: true },
|
||||||
extends: [
|
extends: [
|
||||||
"eslint:recommended",
|
"eslint:recommended",
|
||||||
@@ -236,7 +236,7 @@ module.exports = {
|
|||||||
],
|
],
|
||||||
parser: "@typescript-eslint/parser",
|
parser: "@typescript-eslint/parser",
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
project: "./packages/admin-next/dashboard/tsconfig.json",
|
project: "tsconfig.json",
|
||||||
},
|
},
|
||||||
plugins: ["react-refresh"],
|
plugins: ["react-refresh"],
|
||||||
rules: {
|
rules: {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"generate:countries": "node ./scripts/generate-countries.js && prettier --write ./src/lib/countries.ts",
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0"
|
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0"
|
||||||
@@ -23,6 +24,7 @@
|
|||||||
"@tanstack/react-table": "8.10.7",
|
"@tanstack/react-table": "8.10.7",
|
||||||
"@uiw/react-json-view": "2.0.0-alpha.10",
|
"@uiw/react-json-view": "2.0.0-alpha.10",
|
||||||
"cmdk": "^0.2.0",
|
"cmdk": "^0.2.0",
|
||||||
|
"date-fns": "^3.2.0",
|
||||||
"i18next": "23.7.11",
|
"i18next": "23.7.11",
|
||||||
"i18next-browser-languagedetector": "7.2.0",
|
"i18next-browser-languagedetector": "7.2.0",
|
||||||
"i18next-http-backend": "2.4.2",
|
"i18next-http-backend": "2.4.2",
|
||||||
@@ -36,15 +38,15 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@medusajs/medusa": "workspace:^",
|
"@medusajs/medusa": "workspace:^",
|
||||||
|
"@medusajs/types": "workspace:^",
|
||||||
"@medusajs/ui-preset": "workspace:^",
|
"@medusajs/ui-preset": "workspace:^",
|
||||||
"@medusajs/vite-plugin-extension": "workspace:^",
|
"@medusajs/vite-plugin-extension": "workspace:^",
|
||||||
"@types/react": "18.2.43",
|
"@types/react": "18.2.43",
|
||||||
"@types/react-dom": "18.2.17",
|
"@types/react-dom": "18.2.17",
|
||||||
"@typescript-eslint/eslint-plugin": "6.14.0",
|
|
||||||
"@typescript-eslint/parser": "6.14.0",
|
|
||||||
"@vitejs/plugin-react": "4.2.1",
|
"@vitejs/plugin-react": "4.2.1",
|
||||||
"autoprefixer": "10.4.16",
|
"autoprefixer": "10.4.16",
|
||||||
"postcss": "8.4.32",
|
"postcss": "8.4.32",
|
||||||
|
"prettier": "^3.1.1",
|
||||||
"tailwindcss": "3.3.6",
|
"tailwindcss": "3.3.6",
|
||||||
"typescript": "5.2.2",
|
"typescript": "5.2.2",
|
||||||
"vite": "5.0.10"
|
"vite": "5.0.10"
|
||||||
|
|||||||
@@ -1,12 +1,25 @@
|
|||||||
{
|
{
|
||||||
"$schema": "../$schema.json",
|
"$schema": "../$schema.json",
|
||||||
"general": {
|
"general": {
|
||||||
|
"ascending": "Ascending",
|
||||||
|
"descending": "Descending",
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"save": "Save",
|
"save": "Save",
|
||||||
"create": "Create",
|
"create": "Create",
|
||||||
"delete": "Delete",
|
"delete": "Delete",
|
||||||
"edit": "Edit",
|
"edit": "Edit",
|
||||||
|
"add": "Add",
|
||||||
|
"continue": "Continue",
|
||||||
|
"start": "Start",
|
||||||
|
"end": "End",
|
||||||
|
"apply": "Apply",
|
||||||
|
"range": "Range",
|
||||||
"search": "Search",
|
"search": "Search",
|
||||||
|
"of": "of",
|
||||||
|
"results": "results",
|
||||||
|
"pages": "pages",
|
||||||
|
"next": "Next",
|
||||||
|
"prev": "Prev",
|
||||||
"extensions": "Extensions",
|
"extensions": "Extensions",
|
||||||
"settings": "Settings",
|
"settings": "Settings",
|
||||||
"general": "General",
|
"general": "General",
|
||||||
@@ -14,17 +27,25 @@
|
|||||||
"enabled": "Enabled",
|
"enabled": "Enabled",
|
||||||
"disabled": "Disabled",
|
"disabled": "Disabled",
|
||||||
"remove": "Remove",
|
"remove": "Remove",
|
||||||
|
"admin": "Admin",
|
||||||
|
"store": "Store",
|
||||||
"countSelected": "{{count}} selected",
|
"countSelected": "{{count}} selected",
|
||||||
"plusCountMore": "+ {{count}} more",
|
"plusCountMore": "+ {{count}} more",
|
||||||
"areYouSure": "Are you sure?",
|
"areYouSure": "Are you sure?",
|
||||||
"noRecordsFound": "No records found"
|
"noRecordsFound": "No records found",
|
||||||
|
"typeToConfirm": "Please type {val} to confirm:",
|
||||||
|
"noResultsMessage": "Try changing the filters or search query",
|
||||||
|
"noRecordsTitle": "No records",
|
||||||
|
"noRecordsMessage": "There are no records to show",
|
||||||
|
"unsavedChangesTitle": "Are you sure you want to leave this page?",
|
||||||
|
"unsavedChangesDescription": "You have unsaved changes that will be lost if you leave this page."
|
||||||
},
|
},
|
||||||
"products": {
|
"products": {
|
||||||
"domain": "Products",
|
"domain": "Products",
|
||||||
"variants": "Variants",
|
"variants": "Variants",
|
||||||
"availableInSalesChannels": "Available in <0>{{x}}</0> of <1>{{y}}</1> sales channels",
|
"availableInSalesChannels": "Available in <0>{{x}}</0> of <1>{{y}}</1> sales channels",
|
||||||
"inStockVariants_one": "{{inventory}} in stock for {{count}} variant",
|
"variantCount_one": "{{count}} variant",
|
||||||
"inStockVariants_other": "{{inventory}} in stock for {{count}} variants",
|
"variantCount_other": "{{count}} variants",
|
||||||
"productStatus": {
|
"productStatus": {
|
||||||
"draft": "Draft",
|
"draft": "Draft",
|
||||||
"published": "Published",
|
"published": "Published",
|
||||||
@@ -65,9 +86,11 @@
|
|||||||
"profile": {
|
"profile": {
|
||||||
"domain": "Profile",
|
"domain": "Profile",
|
||||||
"manageYourProfileDetails": "Manage your profile details",
|
"manageYourProfileDetails": "Manage your profile details",
|
||||||
"editProfileDetails": "Edit Profile Details",
|
"editProfile": "Edit profile",
|
||||||
"languageHint": "The language you want to use in the admin dashboard. This will not change the language of your store.",
|
"languageHint": "The language you want to use in the admin dashboard. This will not change the language of your store.",
|
||||||
"userInsightsHint": "Share usage insights and help us improve Medusa. You can read more about what we collect and how we use it in our <0>documentation</0>."
|
"userInsightsHint": "Share usage insights and help us improve Medusa. You can read more about what we collect and how we use it in our <0>documentation</0>.",
|
||||||
|
"language": "Language",
|
||||||
|
"usageInsights": "Usage insights"
|
||||||
},
|
},
|
||||||
"users": {
|
"users": {
|
||||||
"domain": "Users",
|
"domain": "Users",
|
||||||
@@ -81,38 +104,54 @@
|
|||||||
"store": {
|
"store": {
|
||||||
"domain": "Store",
|
"domain": "Store",
|
||||||
"manageYourStoresDetails": "Manage your store's details",
|
"manageYourStoresDetails": "Manage your store's details",
|
||||||
"editStoreDetails": "Edit Store Details",
|
"editStore": "Edit store",
|
||||||
"storeName": "Store name",
|
"defaultCurrency": "Default currency",
|
||||||
"swapLinkTemplate": "Swap link template",
|
"swapLinkTemplate": "Swap link template",
|
||||||
"paymentLinkTemplate": "Payment link template",
|
"paymentLinkTemplate": "Payment link template",
|
||||||
"inviteLinkTemplate": "Invite link template"
|
"inviteLinkTemplate": "Invite link template",
|
||||||
|
"currencies": "Currencies",
|
||||||
|
"addCurrencies": "Add currencies",
|
||||||
|
"removeCurrencyWarning_one": "You are about to remove {{count}} currency from your store. Ensure that you have removed all prices using the currency before proceeding.",
|
||||||
|
"removeCurrencyWarning_other": "You are about to remove {{count}} currencies from your store. Ensure that you have removed all prices using the currencies before proceeding.",
|
||||||
|
"currencyAlreadyAdded": "The currency has already been added to your store."
|
||||||
},
|
},
|
||||||
"regions": {
|
"regions": {
|
||||||
"domain": "Regions"
|
"domain": "Regions",
|
||||||
|
"createRegion": "Create Region",
|
||||||
|
"editRegion": "Edit Region",
|
||||||
|
"deleteRegionWarning": "You are about to delete the region {{name}}. This action cannot be undone.",
|
||||||
|
"taxInclusiveHint": "When enabled all prices in the region will be tax inclusive.",
|
||||||
|
"providersHint": "The providers that are available in the region."
|
||||||
|
},
|
||||||
|
"locations": {
|
||||||
|
"domain": "Locations",
|
||||||
|
"createLocation": "Create location",
|
||||||
|
"editLocation": "Edit location",
|
||||||
|
"addSalesChannels": "Add sales channels",
|
||||||
|
"detailsHint": "Specify the details of the location.",
|
||||||
|
"noLocationsFound": "No locations found",
|
||||||
|
"deleteLocationWarning": "You are about to delete the location {{name}}. This action cannot be undone."
|
||||||
},
|
},
|
||||||
"salesChannels": {
|
"salesChannels": {
|
||||||
"domain": "Sales Channels",
|
"domain": "Sales Channels",
|
||||||
|
"createSalesChannel": "Create Sales Channel",
|
||||||
|
"createSalesChannelHint": "Create a new sales channel to sell your products on.",
|
||||||
|
"enabledHint": "Specify if the sales channel is enabled or disabled.",
|
||||||
"removeProductsWarning_one": "You are about to remove {{count}} product from {{sales_channel}}.",
|
"removeProductsWarning_one": "You are about to remove {{count}} product from {{sales_channel}}.",
|
||||||
"removeProductsWarning_other": "You are about to remove {{count}} products from {{sales_channel}}.",
|
"removeProductsWarning_other": "You are about to remove {{count}} products from {{sales_channel}}.",
|
||||||
"addProducts": "Add Products",
|
"addProducts": "Add Products",
|
||||||
"editSalesChannel": "Edit Sales Channel",
|
"editSalesChannel": "Edit sales channel",
|
||||||
"isEnabledHint": "Specify if the sales channel is enabled or disabled.",
|
"productAlreadyAdded": "The product has already been added to the sales channel.",
|
||||||
"productAlreadyAdded": "The product has already been added to the sales channel."
|
"deleteSalesChannelWarning": "You are about to delete the sales channel {{name}}. This action cannot be undone."
|
||||||
},
|
|
||||||
"currencies": {
|
|
||||||
"domain": "Currencies",
|
|
||||||
"manageTheCurrencies": "Manage the currencies you want to use in your store",
|
|
||||||
"editCurrencyDetails": "Edit Currency Details",
|
|
||||||
"defaultCurrency": "Default Currency",
|
|
||||||
"defaultCurrencyHint": "The default currency of your store.",
|
|
||||||
"removeCurrenciesWarning_one": "You are about to remove {{count}} currency from your store. Ensure that you have removed all prices using the currency before proceeding.",
|
|
||||||
"removeCurrenciesWarning_other": "You are about to remove {{count}} currencies from your store. Ensure that you have removed all prices using the currencies before proceeding.",
|
|
||||||
"currencyAlreadyAdded": "The currency has already been added to your store."
|
|
||||||
},
|
},
|
||||||
"apiKeyManagement": {
|
"apiKeyManagement": {
|
||||||
"domain": "API Key Management",
|
"domain": "API Key Management",
|
||||||
"createAPublishableApiKey": "Create a publishable API key",
|
"createKey": "Create key",
|
||||||
"createKey": "Create Key"
|
"createPublishableApiKey": "Create Publishable API Key",
|
||||||
|
"revoke": "Revoke",
|
||||||
|
"publishableApiKeyHint": "Publishable API keys are used to limit the scope of requests to specific sales channels.",
|
||||||
|
"deleteKeyWarning": "You are about to delete the API key {{title}}. This action cannot be undone.",
|
||||||
|
"revokeKeyWarning": "You are about to revoke the API key {{title}}."
|
||||||
},
|
},
|
||||||
"fields": {
|
"fields": {
|
||||||
"name": "Name",
|
"name": "Name",
|
||||||
@@ -133,8 +172,28 @@
|
|||||||
"sales_channels": "Sales Channels",
|
"sales_channels": "Sales Channels",
|
||||||
"status": "Status",
|
"status": "Status",
|
||||||
"code": "Code",
|
"code": "Code",
|
||||||
|
"countries": "Countries",
|
||||||
|
"paymentProviders": "Payment Providers",
|
||||||
|
"fulfillmentProviders": "Fulfillment Providers",
|
||||||
|
"providers": "Providers",
|
||||||
"availability": "Availability",
|
"availability": "Availability",
|
||||||
"inventory": "Inventory",
|
"inventory": "Inventory",
|
||||||
"optional": "Optional"
|
"optional": "Optional",
|
||||||
|
"taxInclusivePricing": "Tax Inclusive Pricing",
|
||||||
|
"taxRate": "Tax Rate",
|
||||||
|
"taxCode": "Tax Code",
|
||||||
|
"currency": "Currency",
|
||||||
|
"address": "Address",
|
||||||
|
"address2": "Apartment, suite, etc.",
|
||||||
|
"city": "City",
|
||||||
|
"postalCode": "Postal Code",
|
||||||
|
"country": "Country",
|
||||||
|
"state": "State",
|
||||||
|
"province": "Province",
|
||||||
|
"company": "Company",
|
||||||
|
"phone": "Phone",
|
||||||
|
"metadata": "Metadata",
|
||||||
|
"selectCountry": "Select country",
|
||||||
|
"variants": "Variants"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
async function generateCountries() {
|
||||||
|
const { countries } = await import("@medusajs/medusa/dist/utils/countries.js")
|
||||||
|
const fs = await import("fs")
|
||||||
|
const path = await import("path")
|
||||||
|
|
||||||
|
const arr = countries.map((c) => {
|
||||||
|
const iso_2 = c.alpha2.toLowerCase()
|
||||||
|
const iso_3 = c.alpha3.toLowerCase()
|
||||||
|
const num_code = parseInt(c.numeric, 10)
|
||||||
|
const name = c.name.toUpperCase()
|
||||||
|
const display_name = c.name
|
||||||
|
|
||||||
|
return {
|
||||||
|
iso_2,
|
||||||
|
iso_3,
|
||||||
|
num_code,
|
||||||
|
name,
|
||||||
|
display_name,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const json = JSON.stringify(arr, null, 2)
|
||||||
|
|
||||||
|
const dest = path.join(__dirname, "../src/lib/countries.ts")
|
||||||
|
const destDir = path.dirname(dest)
|
||||||
|
|
||||||
|
const fileContent = `/** This file is auto-generated. Do not modify it manually. */\nimport type { Country } from "@medusajs/medusa"\n\nexport const countries: Omit<Country, "region" | "region_id" | "id">[] = ${json}`
|
||||||
|
|
||||||
|
if (!fs.existsSync(destDir)) {
|
||||||
|
fs.mkdirSync(destDir, { recursive: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.writeFileSync(dest, fileContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
;(async () => {
|
||||||
|
console.log("Generating countries")
|
||||||
|
try {
|
||||||
|
await generateCountries()
|
||||||
|
console.log("Countries generated")
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
}
|
||||||
|
})()
|
||||||
@@ -1,28 +1,22 @@
|
|||||||
import { Toaster } from "@medusajs/ui"
|
import { Toaster } from "@medusajs/ui"
|
||||||
import { MedusaProvider } from "medusa-react"
|
import { MedusaProvider } from "medusa-react"
|
||||||
|
|
||||||
import { AuthProvider } from "./providers/auth-provider"
|
|
||||||
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 { queryClient } from "./lib/medusa"
|
import { MEDUSA_BACKEND_URL, queryClient } from "./lib/medusa"
|
||||||
|
|
||||||
const BASE_URL =
|
|
||||||
import.meta.env.VITE_MEDUSA_ADMIN_BACKEND_URL || "http://localhost:9000"
|
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
<MedusaProvider
|
<MedusaProvider
|
||||||
baseUrl={BASE_URL}
|
baseUrl={MEDUSA_BACKEND_URL}
|
||||||
queryClientProviderProps={{
|
queryClientProviderProps={{
|
||||||
client: queryClient,
|
client: queryClient,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ThemeProvider>
|
<ThemeProvider>
|
||||||
<AuthProvider>
|
<RouterProvider />
|
||||||
<RouterProvider />
|
<Toaster />
|
||||||
<Toaster />
|
|
||||||
</AuthProvider>
|
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</MedusaProvider>
|
</MedusaProvider>
|
||||||
)
|
)
|
||||||
|
|||||||
+22
-16
@@ -1,25 +1,31 @@
|
|||||||
import { Spinner } from "@medusajs/icons";
|
import { Spinner } from "@medusajs/icons"
|
||||||
import { PropsWithChildren } from "react";
|
import { Navigate, Outlet, useLocation } from "react-router-dom"
|
||||||
import { Navigate, useLocation } from "react-router-dom";
|
|
||||||
|
|
||||||
import { useAuth } from "../../../providers/auth-provider";
|
import { useAdminGetSession } from "medusa-react"
|
||||||
|
import { SearchProvider } from "../../../providers/search-provider"
|
||||||
|
import { SidebarProvider } from "../../../providers/sidebar-provider"
|
||||||
|
|
||||||
export const RequireAuth = ({ children }: PropsWithChildren) => {
|
export const ProtectedRoute = () => {
|
||||||
const auth = useAuth();
|
const { user, isLoading } = useAdminGetSession()
|
||||||
const location = useLocation();
|
const location = useLocation()
|
||||||
|
|
||||||
if (auth.isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen flex items-center justify-center">
|
<div className="flex min-h-screen items-center justify-center">
|
||||||
<Spinner className="animate-spin text-ui-fg-interactive" />
|
<Spinner className="text-ui-fg-interactive animate-spin" />
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!auth.user) {
|
if (!user) {
|
||||||
console.log("redirecting");
|
return <Navigate to="/login" state={{ from: location }} replace />
|
||||||
return <Navigate to="/login" state={{ from: location }} replace />;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return children;
|
return (
|
||||||
};
|
<SidebarProvider>
|
||||||
|
<SearchProvider>
|
||||||
|
<Outlet />
|
||||||
|
</SearchProvider>
|
||||||
|
</SidebarProvider>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import { clx } from "@medusajs/ui"
|
||||||
|
import * as Popover from "@radix-ui/react-popover"
|
||||||
|
|
||||||
|
type ComboboxOption = {
|
||||||
|
value: string
|
||||||
|
label: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type ComboboxProps = {
|
||||||
|
size?: "base" | "small"
|
||||||
|
options: ComboboxOption[]
|
||||||
|
value: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Combobox = ({ size = "base" }: ComboboxProps) => {
|
||||||
|
return (
|
||||||
|
<Popover.Root>
|
||||||
|
<Popover.Trigger asChild>
|
||||||
|
<button
|
||||||
|
className={clx(
|
||||||
|
"bg-ui-bg-field shadow-buttons-neutral transition-fg flex w-full select-none items-center justify-between rounded-md outline-none",
|
||||||
|
"data-[placeholder]:text-ui-fg-muted text-ui-fg-base",
|
||||||
|
"hover:bg-ui-bg-field-hover",
|
||||||
|
"focus:shadow-borders-interactive-with-active data-[state=open]:!shadow-borders-interactive-with-active",
|
||||||
|
"aria-[invalid=true]:border-ui-border-error aria-[invalid=true]:shadow-borders-error",
|
||||||
|
"invalid::border-ui-border-error invalid:shadow-borders-error",
|
||||||
|
"disabled:!bg-ui-bg-disabled disabled:!text-ui-fg-disabled",
|
||||||
|
{
|
||||||
|
"h-8 px-2 py-1.5 txt-compact-small": size === "base",
|
||||||
|
"h-7 px-2 py-1 txt-compact-small": size === "small",
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
></button>
|
||||||
|
</Popover.Trigger>
|
||||||
|
</Popover.Root>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import { forwardRef } from "react"
|
||||||
|
|
||||||
|
import { TrianglesMini } from "@medusajs/icons"
|
||||||
|
import { clx } from "@medusajs/ui"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
import { countries } from "../../../lib/countries"
|
||||||
|
|
||||||
|
export const CountrySelect = forwardRef<
|
||||||
|
HTMLSelectElement,
|
||||||
|
React.ComponentPropsWithoutRef<"select"> & { placeholder?: string }
|
||||||
|
>(({ className, disabled, placeholder, ...props }, ref) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative">
|
||||||
|
<TrianglesMini
|
||||||
|
className={clx(
|
||||||
|
"absolute right-2 top-1/2 -translate-y-1/2 text-ui-fg-muted transition-fg pointer-events-none",
|
||||||
|
{
|
||||||
|
"text-ui-fg-disabled": disabled,
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<select
|
||||||
|
disabled={disabled}
|
||||||
|
className={clx(
|
||||||
|
"appearance-none bg-ui-bg-field shadow-buttons-neutral transition-fg flex w-full select-none items-center justify-between rounded-md outline-none px-2 py-1 txt-compact-small",
|
||||||
|
"placeholder:text-ui-fg-muted text-ui-fg-base",
|
||||||
|
"hover:bg-ui-bg-field-hover",
|
||||||
|
"focus-visible:shadow-borders-interactive-with-active data-[state=open]:!shadow-borders-interactive-with-active",
|
||||||
|
"aria-[invalid=true]:border-ui-border-error aria-[invalid=true]:shadow-borders-error",
|
||||||
|
"invalid::border-ui-border-error invalid:shadow-borders-error",
|
||||||
|
"disabled:!bg-ui-bg-disabled disabled:!text-ui-fg-disabled",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
ref={ref}
|
||||||
|
>
|
||||||
|
{/* Add an empty option so the first option is preselected */}
|
||||||
|
<option value="" disabled hidden className="text-ui-fg-muted">
|
||||||
|
{placeholder || t("fields.selectCountry")}
|
||||||
|
</option>
|
||||||
|
{countries.map((country) => {
|
||||||
|
return (
|
||||||
|
<option key={country.iso_2} value={country.iso_2}>
|
||||||
|
{country.display_name}
|
||||||
|
</option>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
CountrySelect.displayName = "CountrySelect"
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from "./country-select"
|
||||||
+1
@@ -15,6 +15,7 @@ export const DebouncedSearch = ({
|
|||||||
value: initialValue,
|
value: initialValue,
|
||||||
onChange,
|
onChange,
|
||||||
debounce = 500,
|
debounce = 500,
|
||||||
|
size = "small",
|
||||||
placeholder,
|
placeholder,
|
||||||
...props
|
...props
|
||||||
}: DebouncedSearchProps) => {
|
}: DebouncedSearchProps) => {
|
||||||
|
|||||||
+59
@@ -0,0 +1,59 @@
|
|||||||
|
import { ExclamationCircle, MagnifyingGlass } from "@medusajs/icons"
|
||||||
|
import { Button, Text } from "@medusajs/ui"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
import { Link } from "react-router-dom"
|
||||||
|
|
||||||
|
type NoResultsProps = {
|
||||||
|
title: string
|
||||||
|
message?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const NoResults = ({ title, message }: NoResultsProps) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex h-[400px] w-full items-center justify-center">
|
||||||
|
<div className="flex flex-col items-center gap-y-2">
|
||||||
|
<MagnifyingGlass />
|
||||||
|
<Text size="small" leading="compact" weight="plus">
|
||||||
|
{title}
|
||||||
|
</Text>
|
||||||
|
<Text size="small" className="text-ui-fg-subtle">
|
||||||
|
{message ?? t("general.noResultsMessage")}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
type NoRecordsProps = {
|
||||||
|
title?: string
|
||||||
|
message?: string
|
||||||
|
action?: {
|
||||||
|
to: string
|
||||||
|
label: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const NoRecords = ({ title, message, action }: NoRecordsProps) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex h-[400px] w-full flex-col items-center justify-center gap-y-6">
|
||||||
|
<div className="flex flex-col items-center gap-y-2">
|
||||||
|
<ExclamationCircle />
|
||||||
|
<Text size="small" leading="compact" weight="plus">
|
||||||
|
{title ?? t("general.noRecordsTitle")}
|
||||||
|
</Text>
|
||||||
|
<Text size="small" className="text-ui-fg-subtle">
|
||||||
|
{message ?? t("general.noRecordsMessage")}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
{action && (
|
||||||
|
<Link to={action.to}>
|
||||||
|
<Button variant="secondary">{action.label}</Button>
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from "./empty-table-content"
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from "./json-view-section"
|
||||||
+13
-5
@@ -21,23 +21,27 @@ type JsonViewProps = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Fix the positioning of the copy btn
|
// TODO: Fix the positioning of the copy btn
|
||||||
export const JsonView = ({ data, root }: JsonViewProps) => {
|
export const JsonViewSection = ({ data, root }: JsonViewProps) => {
|
||||||
const numberOfKeys = Object.keys(data).length
|
const numberOfKeys = Object.keys(data).length
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container className="flex items-center justify-between py-6">
|
<Container className="flex items-center justify-between px-6 py-4">
|
||||||
<div className="flex items-center gap-x-4">
|
<div className="flex items-center gap-x-4">
|
||||||
<Heading level="h2">JSON</Heading>
|
<Heading level="h2">JSON</Heading>
|
||||||
<Badge>{numberOfKeys} keys</Badge>
|
<Badge>{numberOfKeys} keys</Badge>
|
||||||
</div>
|
</div>
|
||||||
<Drawer>
|
<Drawer>
|
||||||
<Drawer.Trigger asChild>
|
<Drawer.Trigger asChild>
|
||||||
<IconButton variant="transparent" className="text-ui-fg-subtle">
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
variant="transparent"
|
||||||
|
className="text-ui-fg-subtle"
|
||||||
|
>
|
||||||
<ArrowsPointingOut />
|
<ArrowsPointingOut />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Drawer.Trigger>
|
</Drawer.Trigger>
|
||||||
<Drawer.Content className="border-ui-code-border bg-ui-code-bg-base text-ui-code-text-base dark overflow-hidden border shadow-none max-md:inset-x-2 max-md:max-w-[calc(100%-16px)]">
|
<Drawer.Content className="border-ui-code-border bg-ui-code-bg-base text-ui-code-text-base dark overflow-hidden border shadow-none max-md:inset-x-2 max-md:max-w-[calc(100%-16px)]">
|
||||||
<div className="bg-ui-code-bg-header border-ui-code-border flex items-center justify-between border-b px-8 py-6">
|
<div className="bg-ui-code-bg-header border-ui-code-border flex items-center justify-between border-b px-6 py-4">
|
||||||
<div className="flex items-center gap-x-4">
|
<div className="flex items-center gap-x-4">
|
||||||
<Heading>JSON</Heading>
|
<Heading>JSON</Heading>
|
||||||
<Badge>{numberOfKeys} keys</Badge>
|
<Badge>{numberOfKeys} keys</Badge>
|
||||||
@@ -45,7 +49,11 @@ export const JsonView = ({ data, root }: JsonViewProps) => {
|
|||||||
<div className="flex items-center gap-x-2">
|
<div className="flex items-center gap-x-2">
|
||||||
<Kbd>esc</Kbd>
|
<Kbd>esc</Kbd>
|
||||||
<Drawer.Close asChild>
|
<Drawer.Close asChild>
|
||||||
<IconButton variant="transparent" className="text-ui-fg-subtle">
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
variant="transparent"
|
||||||
|
className="text-ui-fg-subtle"
|
||||||
|
>
|
||||||
<XMarkMini />
|
<XMarkMini />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Drawer.Close>
|
</Drawer.Close>
|
||||||
@@ -1 +0,0 @@
|
|||||||
export * from "./json-view";
|
|
||||||
+2
-5
@@ -8,7 +8,7 @@ import { StatusBadge, Text } from "@medusajs/ui"
|
|||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
import { Thumbnail } from "../thumbnail"
|
import { Thumbnail } from "../thumbnail"
|
||||||
|
|
||||||
export const ProductInventoryCell = ({
|
export const ProductVariantCell = ({
|
||||||
variants,
|
variants,
|
||||||
}: {
|
}: {
|
||||||
variants: ProductVariant[] | null
|
variants: ProductVariant[] | null
|
||||||
@@ -23,13 +23,10 @@ export const ProductInventoryCell = ({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const inventory = variants.reduce((acc, v) => acc + v.inventory_quantity, 0)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Text size="small" className="text-ui-fg-base">
|
<Text size="small" className="text-ui-fg-base">
|
||||||
{t("products.inStockVariants", {
|
{t("products.variantCount", {
|
||||||
count: variants.length,
|
count: variants.length,
|
||||||
inventory: inventory,
|
|
||||||
})}
|
})}
|
||||||
</Text>
|
</Text>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from "./skeleton"
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { clx } from "@medusajs/ui"
|
||||||
|
|
||||||
|
type SkeletonProps = {
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Skeleton = ({ className }: SkeletonProps) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={clx(
|
||||||
|
"bg-ui-bg-component animate-pulse w-3 h-3 rounded-[4px]",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
+1
-5
@@ -1,5 +1,5 @@
|
|||||||
import type { AxiosError } from "axios"
|
|
||||||
import { Navigate, useLocation, useRouteError } from "react-router-dom"
|
import { Navigate, useLocation, useRouteError } from "react-router-dom"
|
||||||
|
import { isAxiosError } from "../../../lib/is-axios-error"
|
||||||
|
|
||||||
export const ErrorBoundary = () => {
|
export const ErrorBoundary = () => {
|
||||||
const error = useRouteError()
|
const error = useRouteError()
|
||||||
@@ -20,7 +20,3 @@ export const ErrorBoundary = () => {
|
|||||||
// TODO: Actual catch-all error page
|
// TODO: Actual catch-all error page
|
||||||
return <div>Dang!</div>
|
return <div>Dang!</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
const isAxiosError = (error: any): error is AxiosError => {
|
|
||||||
return error.isAxiosError
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import { Button, DropdownMenu } from "@medusajs/ui"
|
||||||
|
import { ReactNode } from "react"
|
||||||
|
import { useSearchParams } from "react-router-dom"
|
||||||
|
|
||||||
|
type FilterGroupProps = {
|
||||||
|
filters: {
|
||||||
|
[key: string]: ReactNode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FilterGroup = ({ filters }: FilterGroupProps) => {
|
||||||
|
const [searchParams] = useSearchParams()
|
||||||
|
const filterKeys = Object.keys(filters)
|
||||||
|
|
||||||
|
if (filterKeys.length === 0) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const isClearable = filterKeys.some((key) => searchParams.get(key))
|
||||||
|
const hasMore = !filterKeys.every((key) => searchParams.get(key))
|
||||||
|
const availableKeys = filterKeys.filter((key) => !searchParams.get(key))
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center flex-wrap gap-2">
|
||||||
|
{hasMore && <AddFilterMenu availableKeys={availableKeys} />}
|
||||||
|
{isClearable && (
|
||||||
|
<Button variant="transparent" size="small">
|
||||||
|
Clear all
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
type AddFilterMenuProps = {
|
||||||
|
availableKeys: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const AddFilterMenu = ({ availableKeys }: AddFilterMenuProps) => {
|
||||||
|
return (
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenu.Trigger asChild>
|
||||||
|
<Button variant="secondary" size="small">
|
||||||
|
Add filter
|
||||||
|
</Button>
|
||||||
|
</DropdownMenu.Trigger>
|
||||||
|
<DropdownMenu.Content>
|
||||||
|
{availableKeys.map((key) => (
|
||||||
|
<DropdownMenu.Item key={key}>{key}</DropdownMenu.Item>
|
||||||
|
))}
|
||||||
|
</DropdownMenu.Content>
|
||||||
|
</DropdownMenu>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from "./filter-group"
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from "./order-by"
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
import { ArrowUpDown } from "@medusajs/icons"
|
||||||
|
import { DropdownMenu, IconButton } from "@medusajs/ui"
|
||||||
|
import { useState } from "react"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
import { useSearchParams } from "react-router-dom"
|
||||||
|
|
||||||
|
type OrderByProps = {
|
||||||
|
keys: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
enum SortDirection {
|
||||||
|
ASC = "asc",
|
||||||
|
DESC = "desc",
|
||||||
|
}
|
||||||
|
|
||||||
|
type SortState = {
|
||||||
|
key?: string
|
||||||
|
dir: SortDirection
|
||||||
|
}
|
||||||
|
|
||||||
|
const initState = (params: URLSearchParams): SortState => {
|
||||||
|
const sortParam = params.get("order")
|
||||||
|
|
||||||
|
if (!sortParam) {
|
||||||
|
return {
|
||||||
|
dir: SortDirection.ASC,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const dir = sortParam.startsWith("-") ? SortDirection.DESC : SortDirection.ASC
|
||||||
|
const key = sortParam.replace("-", "")
|
||||||
|
|
||||||
|
return {
|
||||||
|
key,
|
||||||
|
dir,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatKey = (key: string) => {
|
||||||
|
const words = key.split("_")
|
||||||
|
const formattedWords = words.map((word, index) => {
|
||||||
|
if (index === 0) {
|
||||||
|
return word.charAt(0).toUpperCase() + word.slice(1)
|
||||||
|
} else {
|
||||||
|
return word
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return formattedWords.join(" ")
|
||||||
|
}
|
||||||
|
|
||||||
|
export const OrderBy = ({ keys }: OrderByProps) => {
|
||||||
|
const [searchParams, setSearchParams] = useSearchParams()
|
||||||
|
const [state, setState] = useState<{
|
||||||
|
key?: string
|
||||||
|
dir: SortDirection
|
||||||
|
}>(initState(searchParams))
|
||||||
|
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
const handleDirChange = (dir: string) => {
|
||||||
|
setState((prev) => ({
|
||||||
|
...prev,
|
||||||
|
dir: dir as SortDirection,
|
||||||
|
}))
|
||||||
|
updateOrderParam({
|
||||||
|
key: state.key,
|
||||||
|
dir: dir as SortDirection,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleKeyChange = (value: string) => {
|
||||||
|
setState((prev) => ({
|
||||||
|
...prev,
|
||||||
|
key: value,
|
||||||
|
}))
|
||||||
|
|
||||||
|
updateOrderParam({
|
||||||
|
key: value,
|
||||||
|
dir: state.dir,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateOrderParam = (state: SortState) => {
|
||||||
|
if (!state.key) {
|
||||||
|
setSearchParams((prev) => {
|
||||||
|
prev.delete("order")
|
||||||
|
return prev
|
||||||
|
})
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const orderParam =
|
||||||
|
state.dir === SortDirection.ASC ? state.key : `-${state.key}`
|
||||||
|
setSearchParams((prev) => {
|
||||||
|
prev.set("order", orderParam)
|
||||||
|
return prev
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenu.Trigger asChild>
|
||||||
|
<IconButton size="small">
|
||||||
|
<ArrowUpDown />
|
||||||
|
</IconButton>
|
||||||
|
</DropdownMenu.Trigger>
|
||||||
|
<DropdownMenu.Content>
|
||||||
|
<DropdownMenu.RadioGroup
|
||||||
|
value={state.key}
|
||||||
|
onValueChange={handleKeyChange}
|
||||||
|
>
|
||||||
|
{keys.map((key) => (
|
||||||
|
<DropdownMenu.RadioItem
|
||||||
|
key={key}
|
||||||
|
value={key}
|
||||||
|
onSelect={(event) => event.preventDefault()}
|
||||||
|
>
|
||||||
|
{formatKey(key)}
|
||||||
|
</DropdownMenu.RadioItem>
|
||||||
|
))}
|
||||||
|
</DropdownMenu.RadioGroup>
|
||||||
|
<DropdownMenu.Separator />
|
||||||
|
<DropdownMenu.RadioGroup
|
||||||
|
value={state.dir}
|
||||||
|
onValueChange={handleDirChange}
|
||||||
|
>
|
||||||
|
<DropdownMenu.RadioItem
|
||||||
|
className="flex items-center justify-between"
|
||||||
|
value="asc"
|
||||||
|
onSelect={(event) => event.preventDefault()}
|
||||||
|
>
|
||||||
|
{t("general.ascending")}
|
||||||
|
<DropdownMenu.Label>1 - 30</DropdownMenu.Label>
|
||||||
|
</DropdownMenu.RadioItem>
|
||||||
|
<DropdownMenu.RadioItem
|
||||||
|
className="flex items-center justify-between"
|
||||||
|
value="desc"
|
||||||
|
onSelect={(event) => event.preventDefault()}
|
||||||
|
>
|
||||||
|
{t("general.descending")}
|
||||||
|
<DropdownMenu.Label>30 - 1</DropdownMenu.Label>
|
||||||
|
</DropdownMenu.RadioItem>
|
||||||
|
</DropdownMenu.RadioGroup>
|
||||||
|
</DropdownMenu.Content>
|
||||||
|
</DropdownMenu>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from "./query"
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import { Input } from "@medusajs/ui"
|
||||||
|
import { debounce } from "lodash"
|
||||||
|
import { ChangeEvent, useCallback, useEffect, useState } from "react"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
import { useSearchParams } from "react-router-dom"
|
||||||
|
|
||||||
|
type QueryProps = {
|
||||||
|
placeholder?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Query = ({ placeholder }: QueryProps) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const placeholderText = placeholder || t("general.search")
|
||||||
|
|
||||||
|
const [searchParams, setSearchParams] = useSearchParams()
|
||||||
|
const [inputValue, setInputValue] = useState(searchParams.get("q") || "")
|
||||||
|
|
||||||
|
const updateSearchParams = (newValue: string) => {
|
||||||
|
if (!newValue) {
|
||||||
|
setSearchParams((prev) => {
|
||||||
|
prev.delete("q")
|
||||||
|
return prev
|
||||||
|
})
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setSearchParams((prev) => ({ ...prev, q: newValue || "" }))
|
||||||
|
}
|
||||||
|
|
||||||
|
const debouncedUpdate = useCallback(
|
||||||
|
debounce((newValue: string) => updateSearchParams(newValue), 500),
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
debouncedUpdate(inputValue)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
debouncedUpdate.cancel()
|
||||||
|
}
|
||||||
|
}, [inputValue, debouncedUpdate])
|
||||||
|
|
||||||
|
const handleInputChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setInputValue(event.target.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Input
|
||||||
|
type="search"
|
||||||
|
size="small"
|
||||||
|
value={inputValue}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
placeholder={placeholderText}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
import { Outlet, useLocation } from "react-router-dom"
|
|
||||||
import { Gutter } from "./gutter"
|
|
||||||
import { MainNav } from "./main-nav"
|
|
||||||
import { SettingsNav } from "./settings-nav"
|
|
||||||
import { Topbar } from "./topbar"
|
|
||||||
|
|
||||||
export const AppLayout = () => {
|
|
||||||
const location = useLocation()
|
|
||||||
|
|
||||||
const isSettings = location.pathname.startsWith("/settings")
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex h-screen flex-col items-start overflow-hidden md:flex-row">
|
|
||||||
<MainNav />
|
|
||||||
<div className="flex h-[calc(100vh-57px)] w-full md:h-screen">
|
|
||||||
{isSettings && <SettingsNav />}
|
|
||||||
<div className="flex h-full w-full flex-col items-center overflow-y-auto p-4">
|
|
||||||
<Gutter>
|
|
||||||
<Topbar />
|
|
||||||
<Outlet />
|
|
||||||
</Gutter>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
import { TriangleRightMini } from "@medusajs/icons";
|
|
||||||
import { clx } from "@medusajs/ui";
|
|
||||||
import { Link, UIMatch, useMatches } from "react-router-dom";
|
|
||||||
|
|
||||||
type BreadcrumbProps = React.ComponentPropsWithoutRef<"ol">;
|
|
||||||
|
|
||||||
export const Breadcrumbs = ({ className, ...props }: BreadcrumbProps) => {
|
|
||||||
const matches = useMatches() as unknown as UIMatch<
|
|
||||||
unknown,
|
|
||||||
{ crumb?: (data?: unknown) => string }
|
|
||||||
>[];
|
|
||||||
|
|
||||||
const crumbs = matches
|
|
||||||
.filter((match) => Boolean(match.handle?.crumb))
|
|
||||||
.map((match) => {
|
|
||||||
const handle = match.handle;
|
|
||||||
|
|
||||||
return {
|
|
||||||
label: handle.crumb!(match.data),
|
|
||||||
path: match.pathname,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
if (crumbs.length < 2) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ol
|
|
||||||
className={clx("flex items-center gap-x-1 text-ui-fg-muted", className)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{crumbs.map((crumb, index) => {
|
|
||||||
const isLast = index === crumbs.length - 1;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<li
|
|
||||||
key={index}
|
|
||||||
className="txt-compact-small-plus flex items-center gap-x-1"
|
|
||||||
>
|
|
||||||
{!isLast ? (
|
|
||||||
<Link to={crumb.path}>{crumb.label}</Link>
|
|
||||||
) : (
|
|
||||||
<span key={index}>{crumb.label}</span>
|
|
||||||
)}
|
|
||||||
{!isLast && <TriangleRightMini />}
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</ol>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import { PropsWithChildren } from "react";
|
|
||||||
|
|
||||||
export const Gutter = ({ children }: PropsWithChildren) => {
|
|
||||||
return (
|
|
||||||
<div className="w-full max-w-[1200px] flex flex-col gap-y-4">
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export * from "./app-layout";
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
import { QueryClient } from "@tanstack/react-query";
|
|
||||||
import { adminProductKeys } from "medusa-react";
|
|
||||||
import { LoaderFunctionArgs } from "react-router-dom";
|
|
||||||
import { medusa, queryClient } from "../../../lib/medusa";
|
|
||||||
|
|
||||||
const appLoaderQuery = (id: string) => ({
|
|
||||||
queryKey: adminProductKeys.detail(id),
|
|
||||||
queryFn: async () => medusa.admin.products.retrieve(id),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const productLoader = (client: QueryClient) => {
|
|
||||||
return async ({ params }: LoaderFunctionArgs) => {
|
|
||||||
const id = params?.id;
|
|
||||||
|
|
||||||
if (!id) {
|
|
||||||
throw new Error("No id provided");
|
|
||||||
}
|
|
||||||
|
|
||||||
const query = appLoaderQuery(id);
|
|
||||||
|
|
||||||
return (
|
|
||||||
queryClient.getQueryData(query.queryKey) ??
|
|
||||||
(await client.fetchQuery(query))
|
|
||||||
);
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -1,380 +0,0 @@
|
|||||||
import {
|
|
||||||
ArrowRightOnRectangle,
|
|
||||||
BookOpen,
|
|
||||||
BuildingStorefront,
|
|
||||||
Calendar,
|
|
||||||
ChevronDownMini,
|
|
||||||
CircleHalfSolid,
|
|
||||||
CogSixTooth,
|
|
||||||
CurrencyDollar,
|
|
||||||
EllipsisHorizontal,
|
|
||||||
MinusMini,
|
|
||||||
ReceiptPercent,
|
|
||||||
ShoppingCart,
|
|
||||||
Sidebar,
|
|
||||||
SquaresPlus,
|
|
||||||
Tag,
|
|
||||||
Users,
|
|
||||||
} from "@medusajs/icons"
|
|
||||||
import { Avatar, DropdownMenu, IconButton, Text } from "@medusajs/ui"
|
|
||||||
import * as Collapsible from "@radix-ui/react-collapsible"
|
|
||||||
import * as Dialog from "@radix-ui/react-dialog"
|
|
||||||
import { useAdminDeleteSession, useAdminStore } from "medusa-react"
|
|
||||||
import { Link, useLocation, useNavigate } from "react-router-dom"
|
|
||||||
|
|
||||||
import { useAuth } from "../../../providers/auth-provider"
|
|
||||||
import { useTheme } from "../../../providers/theme-provider"
|
|
||||||
|
|
||||||
import { Fragment, useEffect, useState } from "react"
|
|
||||||
import { Breadcrumbs } from "./breadcrumbs"
|
|
||||||
import { NavItem, NavItemProps } from "./nav-item"
|
|
||||||
import { Notifications } from "./notifications"
|
|
||||||
import { SearchToggle } from "./search-toggle"
|
|
||||||
import { Spacer } from "./spacer"
|
|
||||||
|
|
||||||
import extensions from "medusa-admin:routes/links"
|
|
||||||
import { useTranslation } from "react-i18next"
|
|
||||||
|
|
||||||
export const MainNav = () => {
|
|
||||||
return (
|
|
||||||
<Fragment>
|
|
||||||
<DesktopNav />
|
|
||||||
<MobileNav />
|
|
||||||
</Fragment>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const MobileNav = () => {
|
|
||||||
const [open, setOpen] = useState(false)
|
|
||||||
const location = useLocation()
|
|
||||||
|
|
||||||
// If the user navigates to a new route, we want to close the menu
|
|
||||||
useEffect(() => {
|
|
||||||
setOpen(false)
|
|
||||||
}, [location.pathname])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="bg-ui-bg-base border-ui-border-base flex h-[57px] w-full items-center justify-between border-b px-4 md:hidden">
|
|
||||||
<Dialog.Root open={open} onOpenChange={setOpen}>
|
|
||||||
<div className="flex items-center gap-x-2">
|
|
||||||
<Dialog.Trigger asChild>
|
|
||||||
<IconButton variant="transparent">
|
|
||||||
<Sidebar />
|
|
||||||
</IconButton>
|
|
||||||
</Dialog.Trigger>
|
|
||||||
<Breadcrumbs />
|
|
||||||
</div>
|
|
||||||
<Dialog.Portal>
|
|
||||||
<Dialog.Overlay className="bg-ui-bg-overlay fixed inset-0 lg:hidden" />
|
|
||||||
<Dialog.Content className="bg-ui-bg-subtle fixed inset-y-0 left-0 flex w-full flex-col overflow-y-auto sm:max-w-[240px] lg:hidden">
|
|
||||||
<div className="flex flex-1 flex-col">
|
|
||||||
<div className="sticky top-0">
|
|
||||||
<Header />
|
|
||||||
<Spacer />
|
|
||||||
</div>
|
|
||||||
<CoreRouteSection />
|
|
||||||
<ExtensionRouteSection />
|
|
||||||
</div>
|
|
||||||
<div className="sticky bottom-0 flex w-full flex-col">
|
|
||||||
<SettingsSection />
|
|
||||||
<Spacer />
|
|
||||||
<UserSection />
|
|
||||||
</div>
|
|
||||||
</Dialog.Content>
|
|
||||||
</Dialog.Portal>
|
|
||||||
</Dialog.Root>
|
|
||||||
<div className="flex items-center gap-x-2">
|
|
||||||
<SearchToggle />
|
|
||||||
<Notifications />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const DesktopNav = () => {
|
|
||||||
return (
|
|
||||||
<aside className="flex h-full max-h-screen w-full max-w-[240px] flex-col justify-between overflow-y-auto max-md:hidden">
|
|
||||||
<div className="flex flex-1 flex-col">
|
|
||||||
<div className="bg-ui-bg-subtle sticky top-0">
|
|
||||||
<Header />
|
|
||||||
<Spacer />
|
|
||||||
</div>
|
|
||||||
<CoreRouteSection />
|
|
||||||
<ExtensionRouteSection />
|
|
||||||
</div>
|
|
||||||
<div className="bg-ui-bg-subtle sticky bottom-0 flex flex-col">
|
|
||||||
<SettingsSection />
|
|
||||||
<Spacer />
|
|
||||||
<UserSection />
|
|
||||||
</div>
|
|
||||||
</aside>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const Header = () => {
|
|
||||||
const { store } = useAdminStore()
|
|
||||||
const { setTheme, theme } = useTheme()
|
|
||||||
const { mutateAsync: logoutMutation } = useAdminDeleteSession()
|
|
||||||
const navigate = useNavigate()
|
|
||||||
|
|
||||||
const logout = async () => {
|
|
||||||
await logoutMutation(undefined, {
|
|
||||||
onSuccess: () => {
|
|
||||||
navigate("/login")
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!store) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="w-full p-4">
|
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownMenu.Trigger className="hover:bg-ui-bg-subtle-hover active:bg-ui-bg-subtle-pressed focus:bg-ui-bg-subtle-pressed transition-fg w-full rounded-md outline-none">
|
|
||||||
<div className="flex items-center justify-between p-1 md:pr-2">
|
|
||||||
<div className="flex items-center gap-x-3">
|
|
||||||
<div className="bg-ui-bg-base shadow-borders-base flex h-8 w-8 items-center justify-center overflow-hidden rounded-md">
|
|
||||||
<div className="bg-ui-bg-component flex h-[28px] w-[28px] items-center justify-center overflow-hidden rounded-[4px]">
|
|
||||||
{store.name[0].toUpperCase()}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Text size="small" weight="plus" leading="compact">
|
|
||||||
{store.name}
|
|
||||||
</Text>
|
|
||||||
</div>
|
|
||||||
<div className="text-ui-fg-subtle">
|
|
||||||
<EllipsisHorizontal />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</DropdownMenu.Trigger>
|
|
||||||
<DropdownMenu.Content>
|
|
||||||
<DropdownMenu.Item>
|
|
||||||
<BuildingStorefront className="text-ui-fg-subtle mr-2" />
|
|
||||||
Store Settings
|
|
||||||
</DropdownMenu.Item>
|
|
||||||
<DropdownMenu.Separator />
|
|
||||||
<Link to="https://docs.medusajs.com/user-guide" target="_blank">
|
|
||||||
<DropdownMenu.Item>
|
|
||||||
<BookOpen className="text-ui-fg-subtle mr-2" />
|
|
||||||
Documentation
|
|
||||||
</DropdownMenu.Item>
|
|
||||||
</Link>
|
|
||||||
<Link to="https://medusajs.com/changelog/" target="_blank">
|
|
||||||
<DropdownMenu.Item>
|
|
||||||
<Calendar className="text-ui-fg-subtle mr-2" />
|
|
||||||
Changelog
|
|
||||||
</DropdownMenu.Item>
|
|
||||||
</Link>
|
|
||||||
<DropdownMenu.Separator />
|
|
||||||
<DropdownMenu.SubMenu>
|
|
||||||
<DropdownMenu.SubMenuTrigger className="rounded-md">
|
|
||||||
<CircleHalfSolid className="text-ui-fg-subtle mr-2" />
|
|
||||||
Theme
|
|
||||||
</DropdownMenu.SubMenuTrigger>
|
|
||||||
<DropdownMenu.SubMenuContent>
|
|
||||||
<DropdownMenu.RadioGroup value={theme}>
|
|
||||||
<DropdownMenu.RadioItem
|
|
||||||
value="light"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault()
|
|
||||||
setTheme("light")
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Light
|
|
||||||
</DropdownMenu.RadioItem>
|
|
||||||
<DropdownMenu.RadioItem
|
|
||||||
value="dark"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault()
|
|
||||||
setTheme("dark")
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Dark
|
|
||||||
</DropdownMenu.RadioItem>
|
|
||||||
</DropdownMenu.RadioGroup>
|
|
||||||
</DropdownMenu.SubMenuContent>
|
|
||||||
</DropdownMenu.SubMenu>
|
|
||||||
<DropdownMenu.Separator />
|
|
||||||
<DropdownMenu.Item onClick={logout}>
|
|
||||||
<ArrowRightOnRectangle className="text-ui-fg-subtle mr-2" />
|
|
||||||
Logout
|
|
||||||
<DropdownMenu.Shortcut>⌥⇧Q</DropdownMenu.Shortcut>
|
|
||||||
</DropdownMenu.Item>
|
|
||||||
</DropdownMenu.Content>
|
|
||||||
</DropdownMenu>
|
|
||||||
</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",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: t("inventory.domain"),
|
|
||||||
to: "/inventory",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: <Users />,
|
|
||||||
label: t("customers.domain"),
|
|
||||||
to: "/customers",
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
label: t("customerGroups.domain"),
|
|
||||||
to: "/customer-groups",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: <ReceiptPercent />,
|
|
||||||
label: t("discounts.domain"),
|
|
||||||
to: "/discounts",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: <CurrencyDollar />,
|
|
||||||
label: t("pricing.domain"),
|
|
||||||
to: "/pricing",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
const CoreRouteSection = () => {
|
|
||||||
const coreRoutes = useCoreRoutes()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<nav className="flex flex-col gap-y-1 py-4">
|
|
||||||
{coreRoutes.map((route) => {
|
|
||||||
return <NavItem key={route.to} {...route} />
|
|
||||||
})}
|
|
||||||
</nav>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const ExtensionRouteSection = () => {
|
|
||||||
if (!extensions.links || extensions.links.length === 0) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<Spacer />
|
|
||||||
<div className="flex flex-col gap-y-4 py-4">
|
|
||||||
<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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const SettingsSection = () => {
|
|
||||||
return (
|
|
||||||
<div className="py-4">
|
|
||||||
<NavItem icon={<CogSixTooth />} label="Settings" to="/settings" />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const UserSection = () => {
|
|
||||||
const { user } = useAuth()
|
|
||||||
|
|
||||||
if (!user) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
const fallback =
|
|
||||||
user.first_name && user.last_name
|
|
||||||
? `${user.first_name[0]}${user.last_name[0]}`
|
|
||||||
: user.first_name
|
|
||||||
? user.first_name[0]
|
|
||||||
: user.email[0]
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="p-4">
|
|
||||||
<Link
|
|
||||||
to="/settings/profile"
|
|
||||||
className="hover:bg-ui-bg-subtle-hover transition-fg active:bg-ui-bg-subtle-pressed focus:bg-ui-bg-subtle-pressed flex items-center gap-x-3 rounded-md p-1 outline-none"
|
|
||||||
>
|
|
||||||
<Avatar fallback={fallback.toUpperCase()} />
|
|
||||||
<div className="flex flex-1 flex-col">
|
|
||||||
{(user.first_name || user.last_name) && (
|
|
||||||
<Text
|
|
||||||
size="xsmall"
|
|
||||||
weight="plus"
|
|
||||||
leading="compact"
|
|
||||||
className="max-w-[90%] truncate"
|
|
||||||
>{`${user.first_name && `${user.first_name} `}${
|
|
||||||
user.last_name
|
|
||||||
}`}</Text>
|
|
||||||
)}
|
|
||||||
<Text
|
|
||||||
size="xsmall"
|
|
||||||
leading="compact"
|
|
||||||
className="text-ui-fg-subtle max-w-[90%] truncate"
|
|
||||||
>
|
|
||||||
{user.email}
|
|
||||||
</Text>
|
|
||||||
</div>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,148 +0,0 @@
|
|||||||
import { Text, clx } from "@medusajs/ui";
|
|
||||||
import * as Collapsible from "@radix-ui/react-collapsible";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { Link, useLocation } from "react-router-dom";
|
|
||||||
|
|
||||||
type ItemType = "core" | "extension";
|
|
||||||
|
|
||||||
type NestedItemProps = {
|
|
||||||
label: string;
|
|
||||||
to: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type NavItemProps = {
|
|
||||||
icon?: React.ReactNode;
|
|
||||||
label: string;
|
|
||||||
to: string;
|
|
||||||
items?: NestedItemProps[];
|
|
||||||
type?: ItemType;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const NavItem = ({
|
|
||||||
icon,
|
|
||||||
label,
|
|
||||||
to,
|
|
||||||
items,
|
|
||||||
type = "core",
|
|
||||||
}: NavItemProps) => {
|
|
||||||
const location = useLocation();
|
|
||||||
|
|
||||||
const [open, setOpen] = useState(
|
|
||||||
[to, ...(items?.map((i) => i.to) ?? [])].some((p) =>
|
|
||||||
location.pathname.startsWith(p)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setOpen(
|
|
||||||
[to, ...(items?.map((i) => i.to) ?? [])].some((p) =>
|
|
||||||
location.pathname.startsWith(p)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}, [location.pathname, to, items]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="px-4">
|
|
||||||
<Link
|
|
||||||
to={to}
|
|
||||||
className={clx(
|
|
||||||
"text-ui-fg-subtle hover:text-ui-fg-base px-2 py-2.5 md:py-1.5 outline-none flex items-center gap-x-2 transition-fg rounded-md hover:bg-ui-bg-subtle-hover",
|
|
||||||
{
|
|
||||||
"bg-ui-bg-base shadow-elevation-card-rest":
|
|
||||||
location.pathname.startsWith(to),
|
|
||||||
"max-md:hidden": items && items.length > 0,
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<Icon icon={icon} type={type} />
|
|
||||||
<Text size="small" weight="plus" leading="compact">
|
|
||||||
{label}
|
|
||||||
</Text>
|
|
||||||
</Link>
|
|
||||||
{items && items.length > 0 && (
|
|
||||||
<Collapsible.Root open={open} onOpenChange={setOpen}>
|
|
||||||
<Collapsible.Trigger
|
|
||||||
className={clx(
|
|
||||||
"w-full md:hidden text-ui-fg-subtle hover:text-ui-fg-base px-2 py-2.5 md:py-1.5 outline-none flex items-center gap-x-2 transition-fg rounded-md hover:bg-ui-bg-subtle-hover"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<Icon icon={icon} type={type} />
|
|
||||||
<Text size="small" weight="plus" leading="compact">
|
|
||||||
{label}
|
|
||||||
</Text>
|
|
||||||
</Collapsible.Trigger>
|
|
||||||
<Collapsible.Content className="flex flex-col gap-y-1 pt-1">
|
|
||||||
<Link
|
|
||||||
to={to}
|
|
||||||
className={clx(
|
|
||||||
"md:hidden text-ui-fg-subtle hover:text-ui-fg-base px-2 py-2.5 md:py-1.5 outline-none flex items-center gap-x-2 transition-fg rounded-md hover:bg-ui-bg-subtle-hover",
|
|
||||||
{
|
|
||||||
"bg-ui-bg-base shadow-elevation-card-rest":
|
|
||||||
location.pathname.startsWith(to),
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className="w-5 h-5 flex items-center justify-center">
|
|
||||||
<div
|
|
||||||
className={clx(
|
|
||||||
"w-1.5 h-1.5 border-[1.5px] border-ui-fg-muted transition-fg rounded-full",
|
|
||||||
{
|
|
||||||
"border-ui-fg-base border-2": location.pathname === to,
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<Text size="small" weight="plus" leading="compact">
|
|
||||||
{label}
|
|
||||||
</Text>
|
|
||||||
</Link>
|
|
||||||
{items.map((item) => {
|
|
||||||
return (
|
|
||||||
<Link
|
|
||||||
to={item.to}
|
|
||||||
key={item.to}
|
|
||||||
className={clx(
|
|
||||||
"first-of-type:mt-1 last-of-type:mb-2 text-ui-fg-subtle hover:text-ui-fg-base px-2 py-2.5 md:py-1.5 outline-none flex items-center gap-x-2 transition-fg rounded-md hover:bg-ui-bg-subtle-hover",
|
|
||||||
{
|
|
||||||
"bg-ui-bg-base shadow-elevation-card-rest":
|
|
||||||
location.pathname === item.to,
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className="w-5 h-5 flex items-center justify-center">
|
|
||||||
<div
|
|
||||||
className={clx(
|
|
||||||
"w-1.5 h-1.5 border-[1.5px] border-ui-fg-muted transition-fg rounded-full",
|
|
||||||
{
|
|
||||||
"border-ui-fg-base border-2":
|
|
||||||
location.pathname === item.to,
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<Text size="small" weight="plus" leading="compact">
|
|
||||||
{item.label}
|
|
||||||
</Text>
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Collapsible.Content>
|
|
||||||
</Collapsible.Root>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const Icon = ({ icon, type }: { icon?: React.ReactNode; type: ItemType }) => {
|
|
||||||
if (!icon) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return type === "extension" ? (
|
|
||||||
<div className="rounded-[4px] w-5 h-5 flex items-center justify-center shadow-borders-base bg-ui-bg-base">
|
|
||||||
<div className="w-4 h-4 rounded-sm overflow-hidden">{icon}</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
icon
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
import { MagnifyingGlass } from "@medusajs/icons"
|
|
||||||
import { IconButton } from "@medusajs/ui"
|
|
||||||
import { useSearch } from "../../../providers/search-provider"
|
|
||||||
|
|
||||||
export const SearchToggle = () => {
|
|
||||||
const { toggleSearch } = useSearch()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<IconButton
|
|
||||||
variant="transparent"
|
|
||||||
onClick={toggleSearch}
|
|
||||||
className="text-ui-fg-muted hover:text-ui-fg-subtle"
|
|
||||||
>
|
|
||||||
<MagnifyingGlass />
|
|
||||||
</IconButton>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,119 +0,0 @@
|
|||||||
import { ChevronDownMini, CogSixTooth, MinusMini } from "@medusajs/icons"
|
|
||||||
import { Text } from "@medusajs/ui"
|
|
||||||
import * as Collapsible from "@radix-ui/react-collapsible"
|
|
||||||
|
|
||||||
import { useMemo } from "react"
|
|
||||||
import { useTranslation } from "react-i18next"
|
|
||||||
import { NavItem, NavItemProps } from "./nav-item"
|
|
||||||
import { Spacer } from "./spacer"
|
|
||||||
|
|
||||||
const useSettingRoutes = (): NavItemProps[] => {
|
|
||||||
const { t } = useTranslation()
|
|
||||||
|
|
||||||
return useMemo(
|
|
||||||
() => [
|
|
||||||
{
|
|
||||||
label: t("profile.domain"),
|
|
||||||
to: "/settings/profile",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: t("store.domain"),
|
|
||||||
to: "/settings/store",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: t("users.domain"),
|
|
||||||
to: "/settings/users",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: t("regions.domain"),
|
|
||||||
to: "/settings/regions",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: t("currencies.domain"),
|
|
||||||
to: "/settings/currencies",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Taxes",
|
|
||||||
to: "/settings/taxes",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Locations",
|
|
||||||
to: "/settings/locations",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: t("salesChannels.domain"),
|
|
||||||
to: "/settings/sales-channels",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: t("apiKeyManagement.domain"),
|
|
||||||
to: "/settings/api-key-management",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
[t]
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const SettingsNav = () => {
|
|
||||||
const routes = useSettingRoutes()
|
|
||||||
const { t } = useTranslation()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="border-ui-border-base box-content flex h-full max-h-screen w-full max-w-[240px] flex-col overflow-hidden border-x max-md:hidden">
|
|
||||||
<div className="p-4">
|
|
||||||
<div className="flex h-10 items-center gap-x-3 p-1">
|
|
||||||
<CogSixTooth className="text-ui-fg-subtle" />
|
|
||||||
<Text leading="compact" weight="plus" size="small">
|
|
||||||
{t("general.settings")}
|
|
||||||
</Text>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Spacer />
|
|
||||||
<div className="flex flex-1 flex-col gap-y-4 overflow-y-auto py-4">
|
|
||||||
<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">
|
|
||||||
{t("general.general")}
|
|
||||||
</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 asChild>
|
|
||||||
<nav className="flex flex-col gap-y-1 py-1">
|
|
||||||
{routes.map((setting) => (
|
|
||||||
<NavItem key={setting.to} {...setting} />
|
|
||||||
))}
|
|
||||||
</nav>
|
|
||||||
</Collapsible.Content>
|
|
||||||
</Collapsible.Root>
|
|
||||||
<Collapsible.Root>
|
|
||||||
<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">
|
|
||||||
{t("general.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 asChild>
|
|
||||||
<nav className="flex flex-col gap-y-1 py-1">
|
|
||||||
{routes.map((setting) => (
|
|
||||||
<NavItem key={setting.to} {...setting} />
|
|
||||||
))}
|
|
||||||
</nav>
|
|
||||||
</Collapsible.Content>
|
|
||||||
</Collapsible.Root>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
export const Spacer = () => {
|
|
||||||
return (
|
|
||||||
<div className="px-4">
|
|
||||||
<div className="w-full h-px border-b border-dashed border-ui-border-strong" />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
import { Sidebar } from "@medusajs/icons"
|
|
||||||
import { Breadcrumbs } from "./breadcrumbs"
|
|
||||||
import { Notifications } from "./notifications"
|
|
||||||
import { SearchToggle } from "./search-toggle"
|
|
||||||
|
|
||||||
export const Topbar = () => {
|
|
||||||
return (
|
|
||||||
<div className="hidden items-center justify-between px-4 py-1 md:flex">
|
|
||||||
<div className="flex items-center gap-x-1.5">
|
|
||||||
<Sidebar className="text-ui-fg-muted" />
|
|
||||||
<Breadcrumbs />
|
|
||||||
</div>
|
|
||||||
<div className="text-ui-fg-muted flex items-center gap-x-1">
|
|
||||||
<SearchToggle />
|
|
||||||
<Notifications />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from "./main-layout"
|
||||||
@@ -0,0 +1,197 @@
|
|||||||
|
import {
|
||||||
|
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 { useAdminStore } from "medusa-react"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
|
||||||
|
import { Skeleton } from "../../common/skeleton"
|
||||||
|
import { NavItem, NavItemProps } from "../nav-item"
|
||||||
|
import { Shell } from "../shell"
|
||||||
|
|
||||||
|
import extensions from "medusa-admin:routes/links"
|
||||||
|
|
||||||
|
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 } = useAdminStore()
|
||||||
|
|
||||||
|
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="w-8 h-8 rounded-md" />
|
||||||
|
)}
|
||||||
|
{name ? (
|
||||||
|
<Text size="small" weight="plus" leading="compact">
|
||||||
|
{store.name}
|
||||||
|
</Text>
|
||||||
|
) : (
|
||||||
|
<Skeleton className="w-[120px] h-[9px]" />
|
||||||
|
)}
|
||||||
|
</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",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t("inventory.domain"),
|
||||||
|
to: "/inventory",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <Users />,
|
||||||
|
label: t("customers.domain"),
|
||||||
|
to: "/customers",
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
label: t("customerGroups.domain"),
|
||||||
|
to: "/customer-groups",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <ReceiptPercent />,
|
||||||
|
label: t("discounts.domain"),
|
||||||
|
to: "/discounts",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from "./nav-item"
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
import { Text, clx } from "@medusajs/ui"
|
||||||
|
import * as Collapsible from "@radix-ui/react-collapsible"
|
||||||
|
import { useEffect, useState } from "react"
|
||||||
|
import { Link, useLocation } from "react-router-dom"
|
||||||
|
|
||||||
|
type ItemType = "core" | "extension"
|
||||||
|
|
||||||
|
type NestedItemProps = {
|
||||||
|
label: string
|
||||||
|
to: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NavItemProps = {
|
||||||
|
icon?: React.ReactNode
|
||||||
|
label: string
|
||||||
|
to: string
|
||||||
|
items?: NestedItemProps[]
|
||||||
|
type?: ItemType
|
||||||
|
from?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const NavItem = ({
|
||||||
|
icon,
|
||||||
|
label,
|
||||||
|
to,
|
||||||
|
items,
|
||||||
|
type = "core",
|
||||||
|
from,
|
||||||
|
}: NavItemProps) => {
|
||||||
|
const location = useLocation()
|
||||||
|
|
||||||
|
const [open, setOpen] = useState(
|
||||||
|
[to, ...(items?.map((i) => i.to) ?? [])].some((p) =>
|
||||||
|
location.pathname.startsWith(p)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setOpen(
|
||||||
|
[to, ...(items?.map((i) => i.to) ?? [])].some((p) =>
|
||||||
|
location.pathname.startsWith(p)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}, [location.pathname, to, items])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="px-3">
|
||||||
|
<Link
|
||||||
|
to={to}
|
||||||
|
state={
|
||||||
|
from
|
||||||
|
? {
|
||||||
|
from,
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
className={clx(
|
||||||
|
"text-ui-fg-subtle hover:text-ui-fg-base transition-fg hover:bg-ui-bg-subtle-hover flex items-center gap-x-2 rounded-md px-2 py-2.5 outline-none md:py-1.5",
|
||||||
|
{
|
||||||
|
"bg-ui-bg-base hover:bg-ui-bg-base-hover shadow-elevation-card-rest":
|
||||||
|
location.pathname.startsWith(to),
|
||||||
|
"max-md:hidden": items && items.length > 0,
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Icon icon={icon} type={type} />
|
||||||
|
<Text size="small" weight="plus" leading="compact">
|
||||||
|
{label}
|
||||||
|
</Text>
|
||||||
|
</Link>
|
||||||
|
{items && items.length > 0 && (
|
||||||
|
<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 px-2 py-2.5 outline-none md:hidden md:py-1.5"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Icon icon={icon} type={type} />
|
||||||
|
<Text size="small" weight="plus" leading="compact">
|
||||||
|
{label}
|
||||||
|
</Text>
|
||||||
|
</Collapsible.Trigger>
|
||||||
|
<Collapsible.Content className="flex flex-col pt-1">
|
||||||
|
<div className="flex h-[36px] w-full items-center gap-x-1 pl-2 md:hidden">
|
||||||
|
<div
|
||||||
|
role="presentation"
|
||||||
|
className="flex h-full w-5 items-center justify-center"
|
||||||
|
>
|
||||||
|
<div className="bg-ui-border-strong h-full w-px" />
|
||||||
|
</div>
|
||||||
|
<Link
|
||||||
|
to={to}
|
||||||
|
className={clx(
|
||||||
|
"text-ui-fg-subtle hover:text-ui-fg-base transition-fg hover:bg-ui-bg-subtle-hover mb-2 mt-1 flex h-8 flex-1 items-center gap-x-2 rounded-md px-2 py-2.5 outline-none md:py-1.5",
|
||||||
|
{
|
||||||
|
"bg-ui-bg-base hover:bg-ui-bg-base text-ui-fg-base shadow-elevation-card-rest":
|
||||||
|
location.pathname.startsWith(to),
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Text size="small" weight="plus" leading="compact">
|
||||||
|
{label}
|
||||||
|
</Text>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
<ul>
|
||||||
|
{items.map((item) => {
|
||||||
|
return (
|
||||||
|
<li
|
||||||
|
key={item.to}
|
||||||
|
className="flex h-[36px] items-center gap-x-1 pl-2"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
role="presentation"
|
||||||
|
className="flex h-full w-5 items-center justify-center"
|
||||||
|
>
|
||||||
|
<div className="bg-ui-border-strong h-full w-px" />
|
||||||
|
</div>
|
||||||
|
<Link
|
||||||
|
to={item.to}
|
||||||
|
className={clx(
|
||||||
|
"text-ui-fg-subtle hover:text-ui-fg-base transition-fg hover:bg-ui-bg-subtle-hover flex h-8 flex-1 items-center gap-x-2 rounded-md px-2 py-2.5 outline-none first-of-type:mt-1 last-of-type:mb-2 md:py-1.5",
|
||||||
|
{
|
||||||
|
"bg-ui-bg-base text-ui-fg-base hover:bg-ui-bg-base shadow-elevation-card-rest":
|
||||||
|
location.pathname === item.to,
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Text size="small" weight="plus" leading="compact">
|
||||||
|
{item.label}
|
||||||
|
</Text>
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
</Collapsible.Content>
|
||||||
|
</Collapsible.Root>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const Icon = ({ icon, type }: { icon?: React.ReactNode; type: ItemType }) => {
|
||||||
|
if (!icon) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return type === "extension" ? (
|
||||||
|
<div className="shadow-borders-base bg-ui-bg-base flex h-5 w-5 items-center justify-center rounded-[4px]">
|
||||||
|
<div className="h-4 w-4 overflow-hidden rounded-sm">{icon}</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
icon
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from "./notifications"
|
||||||
+16
-13
@@ -1,28 +1,31 @@
|
|||||||
import { BellAlert } from "@medusajs/icons";
|
import { BellAlert } from "@medusajs/icons"
|
||||||
import { Drawer, Heading, IconButton } from "@medusajs/ui";
|
import { Drawer, Heading, IconButton } from "@medusajs/ui"
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react"
|
||||||
|
|
||||||
export const Notifications = () => {
|
export const Notifications = () => {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const onKeyDown = (e: KeyboardEvent) => {
|
const onKeyDown = (e: KeyboardEvent) => {
|
||||||
if (e.key === "n" && (e.metaKey || e.ctrlKey)) {
|
if (e.key === "n" && (e.metaKey || e.ctrlKey)) {
|
||||||
setOpen((prev) => !prev);
|
setOpen((prev) => !prev)
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
document.addEventListener("keydown", onKeyDown);
|
document.addEventListener("keydown", onKeyDown)
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
document.removeEventListener("keydown", onKeyDown);
|
document.removeEventListener("keydown", onKeyDown)
|
||||||
};
|
}
|
||||||
}, []);
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Drawer open={open} onOpenChange={setOpen}>
|
<Drawer open={open} onOpenChange={setOpen}>
|
||||||
<Drawer.Trigger asChild>
|
<Drawer.Trigger asChild>
|
||||||
<IconButton variant="transparent" className="text-ui-fg-muted">
|
<IconButton
|
||||||
|
variant="transparent"
|
||||||
|
className="text-ui-fg-muted hover:text-ui-fg-subtle"
|
||||||
|
>
|
||||||
<BellAlert />
|
<BellAlert />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Drawer.Trigger>
|
</Drawer.Trigger>
|
||||||
@@ -33,5 +36,5 @@ export const Notifications = () => {
|
|||||||
<Drawer.Body>Notifications will go here</Drawer.Body>
|
<Drawer.Body>Notifications will go here</Drawer.Body>
|
||||||
</Drawer.Content>
|
</Drawer.Content>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from "./settings-layout"
|
||||||
+99
@@ -0,0 +1,99 @@
|
|||||||
|
import { ArrowUturnLeft } from "@medusajs/icons"
|
||||||
|
import { IconButton, Text } from "@medusajs/ui"
|
||||||
|
import { useEffect, useMemo, useState } from "react"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
import { Link, useLocation } from "react-router-dom"
|
||||||
|
|
||||||
|
import { NavItem, NavItemProps } from "../nav-item"
|
||||||
|
import { Shell } from "../shell"
|
||||||
|
|
||||||
|
export const SettingsLayout = () => {
|
||||||
|
return (
|
||||||
|
<Shell>
|
||||||
|
<SettingsSidebar />
|
||||||
|
</Shell>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const useSettingRoutes = (): NavItemProps[] => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
return useMemo(
|
||||||
|
() => [
|
||||||
|
{
|
||||||
|
label: t("profile.domain"),
|
||||||
|
to: "/settings/profile",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t("store.domain"),
|
||||||
|
to: "/settings/store",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t("users.domain"),
|
||||||
|
to: "/settings/users",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t("regions.domain"),
|
||||||
|
to: "/settings/regions",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Taxes",
|
||||||
|
to: "/settings/taxes",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Locations",
|
||||||
|
to: "/settings/locations",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t("salesChannels.domain"),
|
||||||
|
to: "/settings/sales-channels",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t("apiKeyManagement.domain"),
|
||||||
|
to: "/settings/api-key-management",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[t]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const SettingsSidebar = () => {
|
||||||
|
const routes = useSettingRoutes()
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
const location = useLocation()
|
||||||
|
const [from, setFrom] = useState("/orders")
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (location.state?.from) {
|
||||||
|
setFrom(location.state.from)
|
||||||
|
}
|
||||||
|
}, [location])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<aside className="flex flex-1 flex-col justify-between overflow-y-auto">
|
||||||
|
<div className="px-3 py-2">
|
||||||
|
<div className="flex items-center gap-x-3 p-1">
|
||||||
|
<Link to={from} replace className="flex items-center justify-center">
|
||||||
|
<IconButton size="small" variant="transparent">
|
||||||
|
<ArrowUturnLeft />
|
||||||
|
</IconButton>
|
||||||
|
</Link>
|
||||||
|
<Text leading="compact" weight="plus" size="small">
|
||||||
|
{t("general.settings")}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="px-3">
|
||||||
|
<div className="border-ui-border-strong h-px w-full border-b border-dashed" />
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-1 flex-col gap-y-4 overflow-y-auto py-2">
|
||||||
|
<nav className="flex flex-col gap-y-1">
|
||||||
|
{routes.map((setting) => (
|
||||||
|
<NavItem key={setting.to} {...setting} />
|
||||||
|
))}
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from "./shell"
|
||||||
@@ -0,0 +1,378 @@
|
|||||||
|
import {
|
||||||
|
ArrowRightOnRectangle,
|
||||||
|
BellAlert,
|
||||||
|
BookOpen,
|
||||||
|
Calendar,
|
||||||
|
CircleHalfSolid,
|
||||||
|
CogSixTooth,
|
||||||
|
MagnifyingGlass,
|
||||||
|
Sidebar,
|
||||||
|
User as UserIcon,
|
||||||
|
} from "@medusajs/icons"
|
||||||
|
import { Avatar, DropdownMenu, IconButton, Kbd, Text, clx } from "@medusajs/ui"
|
||||||
|
import * as Dialog from "@radix-ui/react-dialog"
|
||||||
|
import { useAdminDeleteSession, useAdminGetSession } from "medusa-react"
|
||||||
|
import { PropsWithChildren } from "react"
|
||||||
|
import {
|
||||||
|
Link,
|
||||||
|
Outlet,
|
||||||
|
UIMatch,
|
||||||
|
useLocation,
|
||||||
|
useMatches,
|
||||||
|
useNavigate,
|
||||||
|
} from "react-router-dom"
|
||||||
|
|
||||||
|
import { Skeleton } from "../../common/skeleton"
|
||||||
|
|
||||||
|
import { useSearch } from "../../../providers/search-provider"
|
||||||
|
import { useSidebar } from "../../../providers/sidebar-provider"
|
||||||
|
import { useTheme } from "../../../providers/theme-provider"
|
||||||
|
|
||||||
|
export const Shell = ({ children }: PropsWithChildren) => {
|
||||||
|
return (
|
||||||
|
<div className="flex h-screen flex-col items-start overflow-hidden lg:flex-row">
|
||||||
|
<div>
|
||||||
|
<MobileSidebarContainer>{children}</MobileSidebarContainer>
|
||||||
|
<DesktopSidebarContainer>{children}</DesktopSidebarContainer>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col h-screen w-full">
|
||||||
|
<Topbar />
|
||||||
|
<div className="flex h-full w-full flex-col items-center overflow-y-auto">
|
||||||
|
<Gutter>
|
||||||
|
<Outlet />
|
||||||
|
</Gutter>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const Gutter = ({ children }: PropsWithChildren) => {
|
||||||
|
return (
|
||||||
|
<div className="flex w-full max-w-[1600px] flex-col gap-y-2 p-3">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const Breadcrumbs = () => {
|
||||||
|
const matches = useMatches() as unknown as UIMatch<
|
||||||
|
unknown,
|
||||||
|
{ crumb?: (data?: unknown) => string }
|
||||||
|
>[]
|
||||||
|
|
||||||
|
const crumbs = matches
|
||||||
|
.filter((match) => Boolean(match.handle?.crumb))
|
||||||
|
.map((match) => {
|
||||||
|
const handle = match.handle
|
||||||
|
|
||||||
|
return {
|
||||||
|
label: handle.crumb!(match.data),
|
||||||
|
path: match.pathname,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ol className={clx("text-ui-fg-muted flex items-center select-none")}>
|
||||||
|
{crumbs.map((crumb, index) => {
|
||||||
|
const isLast = index === crumbs.length - 1
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li
|
||||||
|
key={index}
|
||||||
|
className={clx("txt-compact-small-plus flex items-center", {
|
||||||
|
"text-ui-fg-subtle": isLast,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{!isLast ? (
|
||||||
|
<Link
|
||||||
|
className="transition-fg hover:text-ui-fg-subtle"
|
||||||
|
to={crumb.path}
|
||||||
|
>
|
||||||
|
{crumb.label}
|
||||||
|
</Link>
|
||||||
|
) : (
|
||||||
|
<div>
|
||||||
|
<span className="block md:hidden">...</span>
|
||||||
|
<span key={index} className="hidden md:block">
|
||||||
|
{crumb.label}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{/* {!isLast && <TriangleRightMini className="-mt-0.5 mx-2" />} */}
|
||||||
|
{!isLast && <span className="-mt-0.5 mx-2">›</span>}
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</ol>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const UserBadge = () => {
|
||||||
|
const { user, isError, error } = useAdminGetSession()
|
||||||
|
|
||||||
|
const displayName = user
|
||||||
|
? user.first_name && user.last_name
|
||||||
|
? `${user.first_name} ${user.last_name}`
|
||||||
|
: user.first_name
|
||||||
|
? user.first_name
|
||||||
|
: user.email
|
||||||
|
: null
|
||||||
|
|
||||||
|
const fallback = displayName ? displayName[0].toUpperCase() : null
|
||||||
|
|
||||||
|
if (isError) {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DropdownMenu.Trigger asChild>
|
||||||
|
<button
|
||||||
|
disabled={!user}
|
||||||
|
className={clx(
|
||||||
|
"shadow-borders-base flex max-w-[192px] items-center gap-x-2 overflow-hidden text-ellipsis whitespace-nowrap rounded-full py-1 pl-1 pr-2.5 select-none"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{fallback ? (
|
||||||
|
<Avatar size="xsmall" fallback={fallback} />
|
||||||
|
) : (
|
||||||
|
<Skeleton className="w-5 h-5 rounded-full" />
|
||||||
|
)}
|
||||||
|
{displayName ? (
|
||||||
|
<Text
|
||||||
|
size="xsmall"
|
||||||
|
weight="plus"
|
||||||
|
leading="compact"
|
||||||
|
className="truncate"
|
||||||
|
>
|
||||||
|
{displayName}
|
||||||
|
</Text>
|
||||||
|
) : (
|
||||||
|
<Skeleton className="w-[70px] h-[9px]" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</DropdownMenu.Trigger>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const ThemeToggle = () => {
|
||||||
|
const { theme, setTheme } = useTheme()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DropdownMenu.SubMenu>
|
||||||
|
<DropdownMenu.SubMenuTrigger className="rounded-md">
|
||||||
|
<CircleHalfSolid className="text-ui-fg-subtle mr-2" />
|
||||||
|
Theme
|
||||||
|
</DropdownMenu.SubMenuTrigger>
|
||||||
|
<DropdownMenu.SubMenuContent>
|
||||||
|
<DropdownMenu.RadioGroup value={theme}>
|
||||||
|
<DropdownMenu.RadioItem
|
||||||
|
value="light"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
setTheme("light")
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Light
|
||||||
|
</DropdownMenu.RadioItem>
|
||||||
|
<DropdownMenu.RadioItem
|
||||||
|
value="dark"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
setTheme("dark")
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Dark
|
||||||
|
</DropdownMenu.RadioItem>
|
||||||
|
</DropdownMenu.RadioGroup>
|
||||||
|
</DropdownMenu.SubMenuContent>
|
||||||
|
</DropdownMenu.SubMenu>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const Logout = () => {
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const { mutateAsync: logoutMutation } = useAdminDeleteSession()
|
||||||
|
|
||||||
|
const handleLayout = async () => {
|
||||||
|
await logoutMutation(undefined, {
|
||||||
|
onSuccess: () => {
|
||||||
|
navigate("/login")
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DropdownMenu.Item onClick={handleLayout}>
|
||||||
|
<div className="flex items-center gap-x-2">
|
||||||
|
<ArrowRightOnRectangle className="text-ui-fg-subtle" />
|
||||||
|
<span>Logout</span>
|
||||||
|
</div>
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const Profile = () => {
|
||||||
|
const location = useLocation()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link to="/settings/profile" state={{ from: location.pathname }}>
|
||||||
|
<DropdownMenu.Item>
|
||||||
|
<UserIcon className="text-ui-fg-subtle mr-2" />
|
||||||
|
Profile
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const LoggedInUser = () => {
|
||||||
|
return (
|
||||||
|
<DropdownMenu>
|
||||||
|
<UserBadge />
|
||||||
|
<DropdownMenu.Content align="center">
|
||||||
|
<Profile />
|
||||||
|
<DropdownMenu.Separator />
|
||||||
|
<Link to="https://docs.medusajs.com/user-guide" target="_blank">
|
||||||
|
<DropdownMenu.Item>
|
||||||
|
<BookOpen className="text-ui-fg-subtle mr-2" />
|
||||||
|
Documentation
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
</Link>
|
||||||
|
<Link to="https://medusajs.com/changelog/" target="_blank">
|
||||||
|
<DropdownMenu.Item>
|
||||||
|
<Calendar className="text-ui-fg-subtle mr-2" />
|
||||||
|
Changelog
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
</Link>
|
||||||
|
<DropdownMenu.Separator />
|
||||||
|
<ThemeToggle />
|
||||||
|
<DropdownMenu.Separator />
|
||||||
|
<Logout />
|
||||||
|
</DropdownMenu.Content>
|
||||||
|
</DropdownMenu>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const SettingsLink = () => {
|
||||||
|
const location = useLocation()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
to="/settings"
|
||||||
|
className="flex items-center justify-center"
|
||||||
|
state={{ from: location.pathname }}
|
||||||
|
>
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
variant="transparent"
|
||||||
|
className="text-ui-fg-muted transition-fg hover:text-ui-fg-subtle"
|
||||||
|
>
|
||||||
|
<CogSixTooth />
|
||||||
|
</IconButton>
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const ToggleNotifications = () => {
|
||||||
|
return (
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
variant="transparent"
|
||||||
|
className="text-ui-fg-muted transition-fg hover:text-ui-fg-subtle"
|
||||||
|
>
|
||||||
|
<BellAlert />
|
||||||
|
</IconButton>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const Searchbar = () => {
|
||||||
|
const { toggleSearch } = useSearch()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClick={toggleSearch}
|
||||||
|
className="shadow-borders-base bg-ui-bg-subtle hover:bg-ui-bg-subtle-hover transition-fg focus-visible:shadow-borders-focus text-ui-fg-muted flex w-full max-w-[280px] items-center gap-x-2 rounded-full py-1.5 pl-2 pr-1.5 outline-none select-none"
|
||||||
|
>
|
||||||
|
<MagnifyingGlass />
|
||||||
|
<div className="flex-1 text-left">
|
||||||
|
<Text size="small" leading="compact">
|
||||||
|
Jump to or search
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<Kbd className="rounded-full">⌘K</Kbd>
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const ToggleSidebar = () => {
|
||||||
|
const { toggle } = useSidebar()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<IconButton
|
||||||
|
className="hidden lg:flex"
|
||||||
|
variant="transparent"
|
||||||
|
onClick={() => toggle("desktop")}
|
||||||
|
>
|
||||||
|
<Sidebar className="text-ui-fg-muted" />
|
||||||
|
</IconButton>
|
||||||
|
<IconButton
|
||||||
|
className="hidden max-lg:flex"
|
||||||
|
variant="transparent"
|
||||||
|
onClick={() => toggle("mobile")}
|
||||||
|
>
|
||||||
|
<Sidebar className="text-ui-fg-muted" />
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const Topbar = () => {
|
||||||
|
return (
|
||||||
|
<div className="w-full grid-cols-3 border-b p-3 grid">
|
||||||
|
<div className="flex items-center gap-x-1.5">
|
||||||
|
<ToggleSidebar />
|
||||||
|
<Breadcrumbs />
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-center">
|
||||||
|
<Searchbar />
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-end gap-x-3">
|
||||||
|
<div className="text-ui-fg-muted flex items-center gap-x-1">
|
||||||
|
<ToggleNotifications />
|
||||||
|
<SettingsLink />
|
||||||
|
</div>
|
||||||
|
<LoggedInUser />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const DesktopSidebarContainer = ({ children }: PropsWithChildren) => {
|
||||||
|
const { desktop } = useSidebar()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={clx("hidden h-screen w-[220px] border-r", {
|
||||||
|
"lg:flex": desktop,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const MobileSidebarContainer = ({ children }: PropsWithChildren) => {
|
||||||
|
const { mobile, toggle } = useSidebar()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog.Root open={mobile} onOpenChange={() => toggle("mobile")}>
|
||||||
|
<Dialog.Portal>
|
||||||
|
<Dialog.Overlay className="fixed inset-0 bg-ui-bg-overlay" />
|
||||||
|
<Dialog.Content className="h-screen fixed left-0 inset-y-0 w-[220px] border-r bg-ui-bg-subtle">
|
||||||
|
{children}
|
||||||
|
</Dialog.Content>
|
||||||
|
</Dialog.Portal>
|
||||||
|
</Dialog.Root>
|
||||||
|
)
|
||||||
|
}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export * from "./localized-date-picker"
|
||||||
+36
@@ -0,0 +1,36 @@
|
|||||||
|
import { DatePicker } from "@medusajs/ui"
|
||||||
|
import { ComponentPropsWithoutRef } from "react"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
import { languages } from "../../../i18n/config"
|
||||||
|
|
||||||
|
type LocalizedDatePickerProps = Omit<
|
||||||
|
ComponentPropsWithoutRef<typeof DatePicker>,
|
||||||
|
"translations" | "locale"
|
||||||
|
>
|
||||||
|
|
||||||
|
export const LocalizedDatePicker = ({
|
||||||
|
mode = "single",
|
||||||
|
...props
|
||||||
|
}: LocalizedDatePickerProps) => {
|
||||||
|
const { i18n, t } = useTranslation()
|
||||||
|
|
||||||
|
const locale = languages.find((lang) => lang.code === i18n.language)
|
||||||
|
?.date_locale
|
||||||
|
|
||||||
|
const translations = {
|
||||||
|
cancel: t("general.cancel"),
|
||||||
|
apply: t("general.apply"),
|
||||||
|
end: t("general.end"),
|
||||||
|
start: t("general.start"),
|
||||||
|
range: t("general.range"),
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DatePicker
|
||||||
|
mode={mode}
|
||||||
|
translations={translations}
|
||||||
|
locale={locale}
|
||||||
|
{...(props as any)}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export * from "./localized-table-pagination"
|
||||||
+26
@@ -0,0 +1,26 @@
|
|||||||
|
import { Table } from "@medusajs/ui"
|
||||||
|
import { ComponentPropsWithoutRef, ElementRef, forwardRef } from "react"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
|
||||||
|
type LocalizedTablePaginationProps = Omit<
|
||||||
|
ComponentPropsWithoutRef<typeof Table.Pagination>,
|
||||||
|
"translations"
|
||||||
|
>
|
||||||
|
|
||||||
|
export const LocalizedTablePagination = forwardRef<
|
||||||
|
ElementRef<typeof Table.Pagination>,
|
||||||
|
LocalizedTablePaginationProps
|
||||||
|
>((props, ref) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
const translations = {
|
||||||
|
of: t("general.of"),
|
||||||
|
results: t("general.results"),
|
||||||
|
pages: t("general.pages"),
|
||||||
|
prev: t("general.prev"),
|
||||||
|
next: t("general.next"),
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Table.Pagination {...props} translations={translations} ref={ref} />
|
||||||
|
})
|
||||||
|
LocalizedTablePagination.displayName = "LocalizedTablePagination"
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { MagnifyingGlass } from "@medusajs/icons"
|
import { MagnifyingGlass } from "@medusajs/icons"
|
||||||
import { clx } from "@medusajs/ui"
|
import { Kbd, Text, clx } from "@medusajs/ui"
|
||||||
import * as Dialog from "@radix-ui/react-dialog"
|
import * as Dialog from "@radix-ui/react-dialog"
|
||||||
import { Command } from "cmdk"
|
import { Command } from "cmdk"
|
||||||
import {
|
import {
|
||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
useMemo,
|
useMemo,
|
||||||
} from "react"
|
} from "react"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
|
|
||||||
import { useSearch } from "../../providers/search-provider"
|
import { useSearch } from "../../providers/search-provider"
|
||||||
|
|
||||||
export const Search = () => {
|
export const Search = () => {
|
||||||
@@ -100,6 +101,18 @@ const useLinks = (): CommandGroupProps[] => {
|
|||||||
{
|
{
|
||||||
label: t("users.domain"),
|
label: t("users.domain"),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: t("regions.domain"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t("locations.domain"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t("salesChannels.domain"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t("apiKeyManagement.domain"),
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -129,11 +142,30 @@ const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
|
|||||||
<Dialog.Root {...props}>
|
<Dialog.Root {...props}>
|
||||||
<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 shadow-elevation-modal fixed left-[50%] top-[50%] w-full max-w-2xl translate-x-[-50%] translate-y-[-50%] overflow-hidden rounded-xl p-0">
|
<Dialog.Content className="bg-ui-bg-base shadow-elevation-modal fixed left-[50%] top-[50%] w-full max-w-2xl translate-x-[-50%] translate-y-[-50%] overflow-hidden rounded-xl p-0">
|
||||||
<CommandPalette className="[&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
|
<CommandPalette className="[&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
|
||||||
{children}
|
{children}
|
||||||
</CommandPalette>
|
</CommandPalette>
|
||||||
<div className="border-t px-4 pb-4 pt-3"></div>
|
<div className="flex items-center justify-between border-t px-4 pb-4 pt-3">
|
||||||
|
<div></div>
|
||||||
|
<div className="flex items-center gap-x-3">
|
||||||
|
<div className="flex items-center gap-x-2">
|
||||||
|
<Text size="xsmall" leading="compact">
|
||||||
|
Navigation
|
||||||
|
</Text>
|
||||||
|
<div className="flex items-center gap-x-1">
|
||||||
|
<Kbd>↓</Kbd>
|
||||||
|
<Kbd>↑</Kbd>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-x-2">
|
||||||
|
<Text size="xsmall" leading="compact">
|
||||||
|
Open Result
|
||||||
|
</Text>
|
||||||
|
<Kbd>↵</Kbd>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</Dialog.Content>
|
</Dialog.Content>
|
||||||
</Dialog.Portal>
|
</Dialog.Portal>
|
||||||
</Dialog.Root>
|
</Dialog.Root>
|
||||||
@@ -149,7 +181,7 @@ const CommandInput = forwardRef<
|
|||||||
<Command.Input
|
<Command.Input
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={clx(
|
className={clx(
|
||||||
"placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-none disabled:cursor-not-allowed disabled:opacity-50",
|
"placeholder:text-ui-fg-muted flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-none disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
@@ -166,7 +198,7 @@ const CommandList = forwardRef<
|
|||||||
<Command.List
|
<Command.List
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={clx(
|
className={clx(
|
||||||
"max-h-[300px] overflow-y-auto overflow-x-hidden",
|
"max-h-[300px] overflow-y-auto overflow-x-hidden pb-4",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
@@ -219,7 +251,7 @@ const CommandItem = forwardRef<
|
|||||||
<Command.Item
|
<Command.Item
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={clx(
|
className={clx(
|
||||||
"aria-selected:bg-accent aria-selected:text-accent-foreground txt-compact-small relative flex cursor-default select-none items-center rounded-sm p-2 outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
"aria-selected:bg-ui-bg-base-hover hover:bg-ui-bg-base-hover focus-visible:bg-ui-bg-base-hover txt-compact-small relative flex cursor-default select-none items-center rounded-md p-2 outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { usePrompt } from "@medusajs/ui"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
|
||||||
|
export const useFormPrompt = () => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const fn = usePrompt()
|
||||||
|
|
||||||
|
const promptValues = {
|
||||||
|
title: t("general.unsavedChangesTitle"),
|
||||||
|
description: t("general.unsavedChangesDescription"),
|
||||||
|
cancelText: t("general.cancel"),
|
||||||
|
confirmText: t("general.continue"),
|
||||||
|
}
|
||||||
|
|
||||||
|
const prompt = async () => {
|
||||||
|
return await fn(promptValues)
|
||||||
|
}
|
||||||
|
|
||||||
|
return prompt
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { useSearchParams } from "react-router-dom"
|
||||||
|
|
||||||
|
export function useQueryParams<T extends string>(
|
||||||
|
keys: T[]
|
||||||
|
): Record<T, string | undefined> {
|
||||||
|
const [params] = useSearchParams()
|
||||||
|
|
||||||
|
// Use a type assertion to initialize the result
|
||||||
|
const result = {} as Record<T, string | undefined>
|
||||||
|
|
||||||
|
keys.forEach((key) => {
|
||||||
|
result[key] = params.get(key) || undefined
|
||||||
|
})
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import { usePrompt } from "@medusajs/ui"
|
||||||
|
import { useEffect, useState } from "react"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
import { useNavigate } from "react-router-dom"
|
||||||
|
|
||||||
|
type Prompt = {
|
||||||
|
title: string
|
||||||
|
description: string
|
||||||
|
cancelText: string
|
||||||
|
confirmText: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook for managing the state of route modals.
|
||||||
|
*/
|
||||||
|
export const useRouteModalState = (): [
|
||||||
|
open: boolean,
|
||||||
|
onOpenChange: (open: boolean, ignore?: boolean) => void,
|
||||||
|
/**
|
||||||
|
* Subscribe to the dirty state of the form.
|
||||||
|
* If the form is dirty, the modal will prompt
|
||||||
|
* the user before closing.
|
||||||
|
*/
|
||||||
|
subscribe: (value: boolean) => void,
|
||||||
|
] => {
|
||||||
|
const [open, setOpen] = useState(false)
|
||||||
|
const [shouldPrompt, subscribe] = useState(false)
|
||||||
|
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const prompt = usePrompt()
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
let promptValues: Prompt = {
|
||||||
|
title: t("general.unsavedChangesTitle"),
|
||||||
|
description: t("general.unsavedChangesDescription"),
|
||||||
|
cancelText: t("general.cancel"),
|
||||||
|
confirmText: t("general.continue"),
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setOpen(true)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const onOpenChange = async (open: boolean, ignore = false) => {
|
||||||
|
if (!open) {
|
||||||
|
if (shouldPrompt && !ignore) {
|
||||||
|
const confirmed = await prompt(promptValues)
|
||||||
|
|
||||||
|
if (!confirmed) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
navigate("..", { replace: true })
|
||||||
|
}, 200)
|
||||||
|
}
|
||||||
|
|
||||||
|
setOpen(open)
|
||||||
|
}
|
||||||
|
|
||||||
|
return [open, onOpenChange, subscribe]
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { enUS } from "date-fns/locale"
|
||||||
import i18n from "i18next"
|
import i18n from "i18next"
|
||||||
import LanguageDetector from "i18next-browser-languagedetector"
|
import LanguageDetector from "i18next-browser-languagedetector"
|
||||||
import Backend, { type HttpBackendOptions } from "i18next-http-backend"
|
import Backend, { type HttpBackendOptions } from "i18next-http-backend"
|
||||||
@@ -22,6 +23,7 @@ export const languages: Language[] = [
|
|||||||
code: "en",
|
code: "en",
|
||||||
display_name: "English",
|
display_name: "English",
|
||||||
ltr: true,
|
ltr: true,
|
||||||
|
date_locale: enUS,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import type { Locale } from "date-fns"
|
||||||
import en from "../../public/locales/en/translation.json"
|
import en from "../../public/locales/en/translation.json"
|
||||||
|
|
||||||
const resources = {
|
const resources = {
|
||||||
@@ -10,4 +11,5 @@ export type Language = {
|
|||||||
code: string
|
code: string
|
||||||
display_name: string
|
display_name: string
|
||||||
ltr: boolean
|
ltr: boolean
|
||||||
|
date_locale: Locale
|
||||||
}
|
}
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,20 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import type { AxiosError } from "axios"
|
||||||
|
|
||||||
|
export const isAxiosError = (error: any): error is AxiosError => {
|
||||||
|
return error.isAxiosError
|
||||||
|
}
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
import Medusa from "@medusajs/medusa-js"
|
import Medusa from "@medusajs/medusa-js"
|
||||||
import { QueryClient } from "@tanstack/react-query"
|
import { QueryClient } from "@tanstack/react-query"
|
||||||
|
|
||||||
|
export const MEDUSA_BACKEND_URL =
|
||||||
|
import.meta.env.VITE_MEDUSA_ADMIN_BACKEND_URL || "http://localhost:9000"
|
||||||
|
|
||||||
export const queryClient = new QueryClient({
|
export const queryClient = new QueryClient({
|
||||||
defaultOptions: {
|
defaultOptions: {
|
||||||
queries: {
|
queries: {
|
||||||
@@ -12,6 +15,6 @@ export const queryClient = new QueryClient({
|
|||||||
})
|
})
|
||||||
|
|
||||||
export const medusa = new Medusa({
|
export const medusa = new Medusa({
|
||||||
baseUrl: "http://localhost:9000",
|
baseUrl: MEDUSA_BACKEND_URL,
|
||||||
maxRetries: 3,
|
maxRetries: 1,
|
||||||
})
|
})
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
import { AdminAuthRes, User } from "@medusajs/medusa"
|
|
||||||
import { createContext } from "react"
|
|
||||||
|
|
||||||
type AuthContextValue = {
|
|
||||||
login: (email: string, password: string) => Promise<AdminAuthRes>
|
|
||||||
user: Omit<User, "password_hash"> | null
|
|
||||||
isLoading: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export const AuthContext = createContext<AuthContextValue | null>(null)
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
import { useAdminGetSession, useAdminLogin } from "medusa-react"
|
|
||||||
import { PropsWithChildren } from "react"
|
|
||||||
import { AuthContext } from "./auth-context"
|
|
||||||
|
|
||||||
export const AuthProvider = ({ children }: PropsWithChildren) => {
|
|
||||||
const { mutateAsync: loginMutation } = useAdminLogin()
|
|
||||||
const { user, isLoading } = useAdminGetSession()
|
|
||||||
|
|
||||||
const login = async (email: string, password: string) => {
|
|
||||||
return await loginMutation({ email, password })
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AuthContext.Provider
|
|
||||||
value={{
|
|
||||||
login,
|
|
||||||
user: user ?? null,
|
|
||||||
isLoading,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</AuthContext.Provider>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
export * from "./auth-provider";
|
|
||||||
export * from "./use-auth";
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
import { useContext } from "react";
|
|
||||||
import { AuthContext } from "./auth-context";
|
|
||||||
|
|
||||||
export const useAuth = () => {
|
|
||||||
const context = useContext(AuthContext);
|
|
||||||
if (!context) {
|
|
||||||
throw new Error("useAuth must be used within an AuthProvider");
|
|
||||||
}
|
|
||||||
return context;
|
|
||||||
};
|
|
||||||
@@ -1,38 +1,35 @@
|
|||||||
|
import type { AdminProductsRes, AdminRegionsRes } from "@medusajs/medusa"
|
||||||
import {
|
import {
|
||||||
|
Outlet,
|
||||||
RouterProvider as Provider,
|
RouterProvider as Provider,
|
||||||
RouteObject,
|
|
||||||
createBrowserRouter,
|
createBrowserRouter,
|
||||||
} from "react-router-dom"
|
} from "react-router-dom"
|
||||||
|
|
||||||
import { RequireAuth } from "../../components/authentication/require-auth"
|
import { ProtectedRoute } from "../../components/authentication/require-auth"
|
||||||
import { AppLayout } from "../../components/layout/app-layout"
|
|
||||||
import { PublicLayout } from "../../components/layout/public-layout"
|
|
||||||
|
|
||||||
import { AdminProductsRes } from "@medusajs/medusa"
|
|
||||||
import routes from "medusa-admin:routes/pages"
|
|
||||||
import settings from "medusa-admin:settings/pages"
|
|
||||||
import { ErrorBoundary } from "../../components/error/error-boundary"
|
import { ErrorBoundary } from "../../components/error/error-boundary"
|
||||||
import { SearchProvider } from "../search-provider"
|
import { MainLayout } from "../../components/layout/main-layout"
|
||||||
|
import { PublicLayout } from "../../components/layout/public-layout"
|
||||||
|
import { SettingsLayout } from "../../components/layout/settings-layout"
|
||||||
|
|
||||||
const routeExtensions: RouteObject[] = routes.pages.map((ext) => {
|
// const routeExtensions: RouteObject[] = routes.pages.map((ext) => {
|
||||||
return {
|
// return {
|
||||||
path: ext.path,
|
// path: ext.path,
|
||||||
async lazy() {
|
// async lazy() {
|
||||||
const { default: Component } = await import(/* @vite-ignore */ ext.file)
|
// const { default: Component } = await import(/* @vite-ignore */ ext.file)
|
||||||
return { Component }
|
// return { Component }
|
||||||
},
|
// },
|
||||||
}
|
// }
|
||||||
})
|
// })
|
||||||
|
|
||||||
const settingsExtensions: RouteObject[] = settings.pages.map((ext) => {
|
// const settingsExtensions: RouteObject[] = settings.pages.map((ext) => {
|
||||||
return {
|
// return {
|
||||||
path: `/settings${ext.path}`,
|
// path: `/settings${ext.path}`,
|
||||||
async lazy() {
|
// async lazy() {
|
||||||
const { default: Component } = await import(/* @vite-ignore */ ext.file)
|
// const { default: Component } = await import(/* @vite-ignore */ ext.file)
|
||||||
return { Component }
|
// return { Component }
|
||||||
},
|
// },
|
||||||
}
|
// }
|
||||||
})
|
// })
|
||||||
|
|
||||||
const router = createBrowserRouter([
|
const router = createBrowserRouter([
|
||||||
{
|
{
|
||||||
@@ -45,161 +42,192 @@ const router = createBrowserRouter([
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
element: (
|
element: <ProtectedRoute />,
|
||||||
<RequireAuth>
|
|
||||||
<SearchProvider>
|
|
||||||
<AppLayout />
|
|
||||||
</SearchProvider>
|
|
||||||
</RequireAuth>
|
|
||||||
),
|
|
||||||
errorElement: <ErrorBoundary />,
|
errorElement: <ErrorBoundary />,
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: "/",
|
path: "/",
|
||||||
lazy: () => import("../../routes/home"),
|
element: <MainLayout />,
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "/orders",
|
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
index: true,
|
index: true,
|
||||||
lazy: () => import("../../routes/orders/list"),
|
lazy: () => import("../../routes/home"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: ":id",
|
path: "/orders",
|
||||||
lazy: () => import("../../routes/orders/details"),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "/draft-orders",
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
index: true,
|
|
||||||
lazy: () => import("../../routes/draft-orders/list"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: ":id",
|
|
||||||
lazy: () => import("../../routes/draft-orders/details"),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "/products",
|
|
||||||
handle: {
|
|
||||||
crumb: () => "Products",
|
|
||||||
},
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
index: true,
|
|
||||||
lazy: () => import("../../routes/products/views/product-list"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: ":id",
|
|
||||||
lazy: () => import("../../routes/products/views/product-details"),
|
|
||||||
handle: {
|
handle: {
|
||||||
crumb: (data: AdminProductsRes) => data.product.title,
|
crumb: () => "Orders",
|
||||||
},
|
},
|
||||||
},
|
children: [
|
||||||
],
|
{
|
||||||
},
|
index: true,
|
||||||
{
|
lazy: () => import("../../routes/orders/list"),
|
||||||
path: "/categories",
|
},
|
||||||
children: [
|
{
|
||||||
{
|
path: ":id",
|
||||||
index: true,
|
lazy: () => import("../../routes/orders/details"),
|
||||||
lazy: () => import("../../routes/categories/list"),
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: ":id",
|
path: "/draft-orders",
|
||||||
lazy: () => import("../../routes/categories/details"),
|
handle: {
|
||||||
},
|
crumb: () => "Draft Orders",
|
||||||
],
|
},
|
||||||
},
|
children: [
|
||||||
{
|
{
|
||||||
path: "/collections",
|
index: true,
|
||||||
children: [
|
lazy: () => import("../../routes/draft-orders/list"),
|
||||||
{
|
},
|
||||||
index: true,
|
{
|
||||||
lazy: () => import("../../routes/collections/list"),
|
path: ":id",
|
||||||
|
lazy: () => import("../../routes/draft-orders/details"),
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: ":id",
|
path: "/products",
|
||||||
lazy: () => import("../../routes/collections/details"),
|
handle: {
|
||||||
},
|
crumb: () => "Products",
|
||||||
],
|
},
|
||||||
},
|
children: [
|
||||||
{
|
{
|
||||||
path: "/customers",
|
index: true,
|
||||||
children: [
|
lazy: () => import("../../routes/products/product-list"),
|
||||||
{
|
},
|
||||||
index: true,
|
{
|
||||||
lazy: () => import("../../routes/customers/list"),
|
path: ":id",
|
||||||
|
lazy: () => import("../../routes/products/product-detail"),
|
||||||
|
handle: {
|
||||||
|
crumb: (data: AdminProductsRes) => data.product.title,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: ":id",
|
path: "/categories",
|
||||||
lazy: () => import("../../routes/customers/details"),
|
handle: {
|
||||||
},
|
crumb: () => "Categories",
|
||||||
],
|
},
|
||||||
},
|
children: [
|
||||||
{
|
{
|
||||||
path: "/customer-groups",
|
index: true,
|
||||||
children: [
|
lazy: () => import("../../routes/categories/list"),
|
||||||
{
|
},
|
||||||
index: true,
|
{
|
||||||
lazy: () => import("../../routes/customer-groups/list"),
|
path: ":id",
|
||||||
|
lazy: () => import("../../routes/categories/details"),
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: ":id",
|
path: "/collections",
|
||||||
lazy: () => import("../../routes/customer-groups/details"),
|
handle: {
|
||||||
},
|
crumb: () => "Collections",
|
||||||
],
|
},
|
||||||
},
|
children: [
|
||||||
{
|
{
|
||||||
path: "/gift-cards",
|
index: true,
|
||||||
children: [
|
lazy: () => import("../../routes/collections/list"),
|
||||||
{
|
},
|
||||||
index: true,
|
{
|
||||||
lazy: () => import("../../routes/gift-cards/list"),
|
path: ":id",
|
||||||
|
lazy: () => import("../../routes/collections/details"),
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: ":id",
|
path: "/customers",
|
||||||
lazy: () => import("../../routes/gift-cards/details"),
|
handle: {
|
||||||
},
|
crumb: () => "Customers",
|
||||||
],
|
},
|
||||||
},
|
children: [
|
||||||
{
|
{
|
||||||
path: "/inventory",
|
index: true,
|
||||||
lazy: () => import("../../routes/inventory/list"),
|
lazy: () => import("../../routes/customers/list"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/discounts",
|
path: ":id",
|
||||||
children: [
|
lazy: () => import("../../routes/customers/details"),
|
||||||
{
|
},
|
||||||
index: true,
|
],
|
||||||
lazy: () => import("../../routes/discounts/list"),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: ":id",
|
path: "/customer-groups",
|
||||||
lazy: () => import("../../routes/discounts/details"),
|
handle: {
|
||||||
},
|
crumb: () => "Customer Groups",
|
||||||
],
|
},
|
||||||
},
|
children: [
|
||||||
{
|
{
|
||||||
path: "/pricing",
|
index: true,
|
||||||
children: [
|
lazy: () => import("../../routes/customer-groups/list"),
|
||||||
{
|
},
|
||||||
index: true,
|
{
|
||||||
lazy: () => import("../../routes/pricing/list"),
|
path: ":id",
|
||||||
|
lazy: () => import("../../routes/customer-groups/details"),
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: ":id",
|
path: "/gift-cards",
|
||||||
lazy: () => import("../../routes/pricing/details"),
|
handle: {
|
||||||
|
crumb: () => "Gift Cards",
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
index: true,
|
||||||
|
lazy: () => import("../../routes/gift-cards/list"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: ":id",
|
||||||
|
lazy: () => import("../../routes/gift-cards/details"),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/inventory",
|
||||||
|
handle: {
|
||||||
|
crumb: () => "Inventory",
|
||||||
|
},
|
||||||
|
lazy: () => import("../../routes/inventory/list"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/discounts",
|
||||||
|
handle: {
|
||||||
|
crumb: () => "Discounts",
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
index: true,
|
||||||
|
lazy: () => import("../../routes/discounts/list"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: ":id",
|
||||||
|
lazy: () => import("../../routes/discounts/details"),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/pricing",
|
||||||
|
handle: {
|
||||||
|
crumb: () => "Pricing",
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
index: true,
|
||||||
|
lazy: () => import("../../routes/pricing/list"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: ":id",
|
||||||
|
lazy: () => import("../../routes/pricing/details"),
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/settings",
|
path: "/settings",
|
||||||
|
element: <SettingsLayout />,
|
||||||
handle: {
|
handle: {
|
||||||
crumb: () => "Settings",
|
crumb: () => "Settings",
|
||||||
},
|
},
|
||||||
@@ -210,52 +238,131 @@ const router = createBrowserRouter([
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "profile",
|
path: "profile",
|
||||||
lazy: () => import("../../routes/profile/views/profile-details"),
|
lazy: () => import("../../routes/profile/profile-detail"),
|
||||||
handle: {
|
handle: {
|
||||||
crumb: () => "Profile",
|
crumb: () => "Profile",
|
||||||
},
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: "edit",
|
||||||
|
lazy: () => import("../../routes/profile/profile-edit"),
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "store",
|
path: "store",
|
||||||
lazy: () => import("../../routes/store/views/store-details"),
|
lazy: () => import("../../routes/store/store-detail"),
|
||||||
handle: {
|
handle: {
|
||||||
crumb: () => "Store",
|
crumb: () => "Store",
|
||||||
},
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: "edit",
|
||||||
|
lazy: () => import("../../routes/store/store-edit"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "add-currencies",
|
||||||
|
lazy: () => import("../../routes/store/store-add-currencies"),
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "locations",
|
path: "locations",
|
||||||
lazy: () => import("../../routes/locations/list"),
|
element: <Outlet />,
|
||||||
|
handle: {
|
||||||
|
crumb: () => "Locations",
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: "",
|
||||||
|
lazy: () => import("../../routes/locations/location-list"),
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: "create",
|
||||||
|
lazy: () =>
|
||||||
|
import("../../routes/locations/location-create"),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: ":id",
|
||||||
|
lazy: () => import("../../routes/locations/location-detail"),
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: "edit",
|
||||||
|
lazy: () => import("../../routes/locations/location-edit"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "add-sales-channels",
|
||||||
|
lazy: () =>
|
||||||
|
import(
|
||||||
|
"../../routes/locations/location-add-sales-channels"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "regions",
|
path: "regions",
|
||||||
|
element: <Outlet />,
|
||||||
handle: {
|
handle: {
|
||||||
crumb: () => "Regions",
|
crumb: () => "Regions",
|
||||||
},
|
},
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
index: true,
|
path: "",
|
||||||
lazy: () => import("../../routes/regions/views/region-list"),
|
lazy: () => import("../../routes/regions/region-list"),
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: "create",
|
||||||
|
lazy: () => import("../../routes/regions/region-create"),
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: ":id",
|
path: ":id",
|
||||||
lazy: () => import("../../routes/regions/views/region-details"),
|
lazy: () => import("../../routes/regions/region-detail"),
|
||||||
|
handle: {
|
||||||
|
crumb: (data: AdminRegionsRes) => data.region.name,
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: "edit",
|
||||||
|
lazy: () => import("../../routes/regions/region-edit"),
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "users",
|
path: "users",
|
||||||
lazy: () => import("../../routes/users"),
|
element: <Outlet />,
|
||||||
handle: {
|
handle: {
|
||||||
crumb: () => "Users",
|
crumb: () => "Users",
|
||||||
},
|
},
|
||||||
},
|
children: [
|
||||||
{
|
{
|
||||||
path: "currencies",
|
path: "",
|
||||||
lazy: () =>
|
lazy: () => import("../../routes/users/user-list"),
|
||||||
import("../../routes/currencies/views/currencies-details"),
|
children: [
|
||||||
handle: {
|
{
|
||||||
crumb: () => "Currencies",
|
path: "invite",
|
||||||
},
|
lazy: () => import("../../routes/users/user-invite"),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: ":id",
|
||||||
|
lazy: () => import("../../routes/users/user-detail"),
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: "edit",
|
||||||
|
lazy: () => import("../../routes/users/user-edit"),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "taxes",
|
path: "taxes",
|
||||||
@@ -275,37 +382,91 @@ const router = createBrowserRouter([
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "sales-channels",
|
path: "sales-channels",
|
||||||
|
element: <Outlet />,
|
||||||
handle: {
|
handle: {
|
||||||
crumb: () => "Sales Channels",
|
crumb: () => "Sales Channels",
|
||||||
},
|
},
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
index: true,
|
path: "",
|
||||||
lazy: () =>
|
lazy: () =>
|
||||||
import(
|
import("../../routes/sales-channels/sales-channel-list"),
|
||||||
"../../routes/sales-channels/views/sales-channel-list"
|
children: [
|
||||||
),
|
{
|
||||||
|
path: "create",
|
||||||
|
lazy: () =>
|
||||||
|
import(
|
||||||
|
"../../routes/sales-channels/sales-channel-create"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: ":id",
|
path: ":id",
|
||||||
lazy: () =>
|
lazy: () =>
|
||||||
import(
|
import("../../routes/sales-channels/sales-channel-detail"),
|
||||||
"../../routes/sales-channels/views/sales-channel-details"
|
children: [
|
||||||
),
|
{
|
||||||
|
path: "edit",
|
||||||
|
lazy: () =>
|
||||||
|
import("../../routes/sales-channels/sales-channel-edit"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "add-products",
|
||||||
|
lazy: () =>
|
||||||
|
import(
|
||||||
|
"../../routes/sales-channels/sales-channel-add-products"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "api-key-management",
|
path: "api-key-management",
|
||||||
lazy: () => import("../../routes/api-key-management"),
|
element: <Outlet />,
|
||||||
handle: {
|
handle: {
|
||||||
crumb: () => "API Key Management",
|
crumb: () => "API Key Management",
|
||||||
},
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: "",
|
||||||
|
lazy: () =>
|
||||||
|
import(
|
||||||
|
"../../routes/api-key-management/api-key-management-list"
|
||||||
|
),
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: "create",
|
||||||
|
lazy: () =>
|
||||||
|
import(
|
||||||
|
"../../routes/api-key-management/api-key-management-create"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: ":id",
|
||||||
|
lazy: () =>
|
||||||
|
import(
|
||||||
|
"../../routes/api-key-management/api-key-management-detail"
|
||||||
|
),
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: "edit",
|
||||||
|
lazy: () =>
|
||||||
|
import(
|
||||||
|
"../../routes/api-key-management/api-key-management-edit"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
...settingsExtensions,
|
// ...settingsExtensions,
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
...routeExtensions,
|
// ...routeExtensions,
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export * from "./sidebar-provider"
|
||||||
|
export * from "./use-sidebar"
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { createContext } from "react"
|
||||||
|
|
||||||
|
type SidebarContextValue = {
|
||||||
|
desktop: boolean
|
||||||
|
mobile: boolean
|
||||||
|
toggle: (view: "desktop" | "mobile") => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SidebarContext = createContext<SidebarContextValue | null>(null)
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { PropsWithChildren, useState } from "react"
|
||||||
|
import { SidebarContext } from "./sidebar-context"
|
||||||
|
|
||||||
|
export const SidebarProvider = ({ children }: PropsWithChildren) => {
|
||||||
|
const [desktop, setDesktop] = useState(true)
|
||||||
|
const [mobile, setMobile] = useState(false)
|
||||||
|
|
||||||
|
const toggle = (view: "desktop" | "mobile") => {
|
||||||
|
if (view === "desktop") {
|
||||||
|
setDesktop(!desktop)
|
||||||
|
} else {
|
||||||
|
setMobile(!mobile)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SidebarContext.Provider value={{ desktop, mobile, toggle }}>
|
||||||
|
{children}
|
||||||
|
</SidebarContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { useContext } from "react"
|
||||||
|
import { SidebarContext } from "./sidebar-context"
|
||||||
|
|
||||||
|
export const useSidebar = () => {
|
||||||
|
const context = useContext(SidebarContext)
|
||||||
|
|
||||||
|
if (!context) {
|
||||||
|
throw new Error("useSidebar must be used within a SidebarProvider")
|
||||||
|
}
|
||||||
|
|
||||||
|
return context
|
||||||
|
}
|
||||||
+15
@@ -0,0 +1,15 @@
|
|||||||
|
import { FocusModal } from "@medusajs/ui"
|
||||||
|
import { useRouteModalState } from "../../../hooks/use-route-modal-state"
|
||||||
|
import { CreatePublishableApiKeyForm } from "./components/create-publishable-api-key-form"
|
||||||
|
|
||||||
|
export const ApiKeyManagementCreate = () => {
|
||||||
|
const [open, onOpenChange, subscribe] = useRouteModalState()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FocusModal open={open} onOpenChange={onOpenChange}>
|
||||||
|
<FocusModal.Content>
|
||||||
|
<CreatePublishableApiKeyForm />
|
||||||
|
</FocusModal.Content>
|
||||||
|
</FocusModal>
|
||||||
|
)
|
||||||
|
}
|
||||||
+97
@@ -0,0 +1,97 @@
|
|||||||
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
|
import { Button, FocusModal, Heading, Input, Text } from "@medusajs/ui"
|
||||||
|
import { useAdminCreatePublishableApiKey } from "medusa-react"
|
||||||
|
import { useForm } from "react-hook-form"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
import * as zod from "zod"
|
||||||
|
|
||||||
|
import { useEffect } from "react"
|
||||||
|
import { Form } from "../../../../../components/common/form"
|
||||||
|
|
||||||
|
type CreatePublishableApiKeyFormProps = {
|
||||||
|
subscribe: (state: boolean) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const CreatePublishableApiKeySchema = zod.object({
|
||||||
|
title: zod.string().min(1),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const CreatePublishableApiKeyForm = ({
|
||||||
|
subscribe,
|
||||||
|
}: CreatePublishableApiKeyFormProps) => {
|
||||||
|
const { mutateAsync, isLoading } = useAdminCreatePublishableApiKey()
|
||||||
|
|
||||||
|
const form = useForm<zod.infer<typeof CreatePublishableApiKeySchema>>({
|
||||||
|
defaultValues: {
|
||||||
|
title: "",
|
||||||
|
},
|
||||||
|
resolver: zodResolver(CreatePublishableApiKeySchema),
|
||||||
|
})
|
||||||
|
|
||||||
|
const {
|
||||||
|
formState: { isDirty },
|
||||||
|
} = form
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
subscribe(isDirty)
|
||||||
|
}, [isDirty])
|
||||||
|
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
const handleSubmit = form.handleSubmit(async (values) => {
|
||||||
|
await mutateAsync(values)
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form {...form}>
|
||||||
|
<form
|
||||||
|
className="flex h-full flex-col overflow-hidden"
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
>
|
||||||
|
<FocusModal.Header>
|
||||||
|
<div className="flex items-center gap-x-2 justify-end">
|
||||||
|
<FocusModal.Close asChild>
|
||||||
|
<Button size="small" variant="secondary">
|
||||||
|
{t("general.cancel")}
|
||||||
|
</Button>
|
||||||
|
</FocusModal.Close>
|
||||||
|
<Button size="small" type="submit" isLoading={isLoading}>
|
||||||
|
{t("general.save")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</FocusModal.Header>
|
||||||
|
<FocusModal.Body className="flex flex-1 flex-col overflow-hidden">
|
||||||
|
<div className="flex flex-1 flex-col items-center overflow-y-auto">
|
||||||
|
<div className="flex w-full max-w-[720px] flex-col gap-y-10 px-2 pb-6 pt-[72px]">
|
||||||
|
<div>
|
||||||
|
<Heading>
|
||||||
|
{t("apiKeyManagement.createPublishableApiKey")}
|
||||||
|
</Heading>
|
||||||
|
<Text size="small" className="text-ui-fg-subtle">
|
||||||
|
{t("apiKeyManagement.publishableApiKeyHint")}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<Form.Field
|
||||||
|
control={form.control}
|
||||||
|
name="title"
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<Form.Item>
|
||||||
|
<Form.Label>{t("fields.title")}</Form.Label>
|
||||||
|
<Form.Control>
|
||||||
|
<Input size="small" {...field} />
|
||||||
|
</Form.Control>
|
||||||
|
<Form.ErrorMessage />
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</FocusModal.Body>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
)
|
||||||
|
}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export * from "./create-publishable-api-key-form"
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export { ApiKeyManagementCreate as Component } from "./api-key-management-create"
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
export const ApiKeyManagementDetail = () => {
|
||||||
|
return <div className="flex flex-col gap-y-2"></div>
|
||||||
|
}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export { ApiKeyManagementDetail as Component } from "./api-key-management-detail"
|
||||||
+12
@@ -0,0 +1,12 @@
|
|||||||
|
import { Drawer } from "@medusajs/ui"
|
||||||
|
import { useRouteModalState } from "../../../hooks/use-route-modal-state"
|
||||||
|
|
||||||
|
export const ApiKeyManagementEdit = () => {
|
||||||
|
const [open, onOpenChange] = useRouteModalState()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Drawer open={open} onOpenChange={onOpenChange}>
|
||||||
|
<Drawer.Content></Drawer.Content>
|
||||||
|
</Drawer>
|
||||||
|
)
|
||||||
|
}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export { ApiKeyManagementEdit as Component } from "./api-key-management-edit"
|
||||||
+15
-124
@@ -1,4 +1,3 @@
|
|||||||
import { InformationCircle } from "@medusajs/icons"
|
|
||||||
import { PublishableApiKey, SalesChannel } from "@medusajs/medusa"
|
import { PublishableApiKey, SalesChannel } from "@medusajs/medusa"
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
@@ -21,136 +20,22 @@ import {
|
|||||||
} from "@tanstack/react-table"
|
} from "@tanstack/react-table"
|
||||||
import {
|
import {
|
||||||
useAdminCreatePublishableApiKey,
|
useAdminCreatePublishableApiKey,
|
||||||
useAdminPublishableApiKeys,
|
|
||||||
useAdminSalesChannels,
|
useAdminSalesChannels,
|
||||||
} from "medusa-react"
|
} from "medusa-react"
|
||||||
import { useMemo, useState } from "react"
|
import { useMemo } from "react"
|
||||||
import { useForm } from "react-hook-form"
|
import { useForm } from "react-hook-form"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
import * as zod from "zod"
|
import * as zod from "zod"
|
||||||
|
|
||||||
import { Form } from "../../components/common/form"
|
import { Outlet } from "react-router-dom"
|
||||||
|
import { Form } from "../../../components/common/form"
|
||||||
export const ApiKeyManagement = () => {
|
import { ApiKeyManagementListTable } from "./components/api-key-management-list-table"
|
||||||
const [showCreateModal, setShowCreateModal] = useState(false)
|
|
||||||
|
|
||||||
const { publishable_api_keys, isLoading, isError, error } =
|
|
||||||
useAdminPublishableApiKeys()
|
|
||||||
|
|
||||||
const columns = useColumns()
|
|
||||||
|
|
||||||
const table = useReactTable({
|
|
||||||
data: publishable_api_keys || [],
|
|
||||||
columns,
|
|
||||||
getCoreRowModel: getCoreRowModel(),
|
|
||||||
getRowId: (row) => row.id,
|
|
||||||
})
|
|
||||||
|
|
||||||
const { t } = useTranslation()
|
|
||||||
|
|
||||||
// TODO: Move to loading.tsx and set as Suspense fallback for the route
|
|
||||||
if (isLoading) {
|
|
||||||
return <div>Loading</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Move to error.tsx and set as ErrorBoundary for the route
|
|
||||||
if (isError || !publishable_api_keys) {
|
|
||||||
const err = error ? JSON.parse(JSON.stringify(error)) : null
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
{(err as Error & { status: number })?.status === 404 ? (
|
|
||||||
<div>Not found</div>
|
|
||||||
) : (
|
|
||||||
<div>Something went wrong!</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const hasData = publishable_api_keys.length !== 0
|
|
||||||
|
|
||||||
|
export const ApiKeyManagementList = () => {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-y-2">
|
<div className="flex flex-col gap-y-2">
|
||||||
<Container className="p-0">
|
<ApiKeyManagementListTable />
|
||||||
<div className="px-8 py-6 pb-4">
|
<Outlet />
|
||||||
<Heading>{t("apiKeyManagement.domain")}</Heading>
|
|
||||||
</div>
|
|
||||||
<div className="border-ui-border-base border-y">
|
|
||||||
{hasData ? (
|
|
||||||
<Table>
|
|
||||||
<Table.Header>
|
|
||||||
{table.getHeaderGroups().map((headerGroup) => {
|
|
||||||
return (
|
|
||||||
<Table.Row
|
|
||||||
key={headerGroup.id}
|
|
||||||
className="[&_th:first-of-type]:w-[1%] [&_th:first-of-type]:whitespace-nowrap [&_th]:w-1/3"
|
|
||||||
>
|
|
||||||
{headerGroup.headers.map((header) => {
|
|
||||||
return (
|
|
||||||
<Table.HeaderCell key={header.id}>
|
|
||||||
{flexRender(
|
|
||||||
header.column.columnDef.header,
|
|
||||||
header.getContext()
|
|
||||||
)}
|
|
||||||
</Table.HeaderCell>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</Table.Row>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</Table.Header>
|
|
||||||
<Table.Body className="border-b-0">
|
|
||||||
{table.getRowModel().rows.map((row) => (
|
|
||||||
<Table.Row
|
|
||||||
key={row.id}
|
|
||||||
className={clx(
|
|
||||||
"transition-fg cursor-pointer [&_td:last-of-type]:w-[1%] [&_td:last-of-type]:whitespace-nowrap",
|
|
||||||
{
|
|
||||||
"bg-ui-bg-highlight hover:bg-ui-bg-highlight-hover":
|
|
||||||
row.getIsSelected(),
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{row.getVisibleCells().map((cell) => (
|
|
||||||
<Table.Cell key={cell.id}>
|
|
||||||
{flexRender(
|
|
||||||
cell.column.columnDef.cell,
|
|
||||||
cell.getContext()
|
|
||||||
)}
|
|
||||||
</Table.Cell>
|
|
||||||
))}
|
|
||||||
</Table.Row>
|
|
||||||
))}
|
|
||||||
</Table.Body>
|
|
||||||
</Table>
|
|
||||||
) : (
|
|
||||||
<div className="flex flex-col items-center py-24">
|
|
||||||
<div className="flex flex-col items-center gap-y-6">
|
|
||||||
<div className="flex flex-col items-center gap-y-2">
|
|
||||||
<InformationCircle />
|
|
||||||
<Text weight="plus" size="small" leading="compact">
|
|
||||||
{t("general.noRecordsFound")}
|
|
||||||
</Text>
|
|
||||||
<Text size="small" className="text-ui-fg-muted">
|
|
||||||
{t("apiKeyManagement.createAPublishableApiKey")}
|
|
||||||
</Text>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
onClick={() => setShowCreateModal(!showCreateModal)}
|
|
||||||
>
|
|
||||||
{t("apiKeyManagement.createKey")}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="h-[72px]"></div>
|
|
||||||
</Container>
|
|
||||||
<CreatePublishableApiKey
|
|
||||||
open={showCreateModal}
|
|
||||||
onOpenChange={setShowCreateModal}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -274,8 +159,14 @@ const CreatePublishableApiKey = (props: CreatePublishableApiKeyProps) => {
|
|||||||
<FocusModal.Content>
|
<FocusModal.Content>
|
||||||
<FocusModal.Header>
|
<FocusModal.Header>
|
||||||
<div className="flex items-center justify-end gap-x-2">
|
<div className="flex items-center justify-end gap-x-2">
|
||||||
<Button variant="secondary">{t("general.cancel")}</Button>
|
<FocusModal.Close asChild>
|
||||||
<Button type="submit">Publish API Key</Button>
|
<Button size="small" variant="secondary">
|
||||||
|
{t("general.cancel")}
|
||||||
|
</Button>
|
||||||
|
</FocusModal.Close>
|
||||||
|
<Button size="small" type="submit">
|
||||||
|
Publish API Key
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</FocusModal.Header>
|
</FocusModal.Header>
|
||||||
<FocusModal.Body className="flex flex-col items-center py-16">
|
<FocusModal.Body className="flex flex-col items-center py-16">
|
||||||
+265
@@ -0,0 +1,265 @@
|
|||||||
|
import { EllipsisHorizontal, Trash, XCircle } from "@medusajs/icons"
|
||||||
|
import { PublishableApiKey } from "@medusajs/medusa"
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Container,
|
||||||
|
DropdownMenu,
|
||||||
|
Heading,
|
||||||
|
IconButton,
|
||||||
|
Table,
|
||||||
|
clx,
|
||||||
|
usePrompt,
|
||||||
|
} from "@medusajs/ui"
|
||||||
|
import {
|
||||||
|
PaginationState,
|
||||||
|
RowSelectionState,
|
||||||
|
createColumnHelper,
|
||||||
|
flexRender,
|
||||||
|
getCoreRowModel,
|
||||||
|
useReactTable,
|
||||||
|
} from "@tanstack/react-table"
|
||||||
|
import {
|
||||||
|
useAdminDeletePublishableApiKey,
|
||||||
|
useAdminPublishableApiKeys,
|
||||||
|
useAdminRevokePublishableApiKey,
|
||||||
|
} from "medusa-react"
|
||||||
|
import { useMemo, useState } from "react"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
import { Link, useNavigate } from "react-router-dom"
|
||||||
|
import { NoRecords } from "../../../../../components/common/empty-table-content"
|
||||||
|
import { LocalizedTablePagination } from "../../../../../components/localization/localized-table-pagination"
|
||||||
|
|
||||||
|
const PAGE_SIZE = 50
|
||||||
|
|
||||||
|
export const ApiKeyManagementListTable = () => {
|
||||||
|
const [{ pageIndex, pageSize }, setPagination] = useState<PaginationState>({
|
||||||
|
pageIndex: 0,
|
||||||
|
pageSize: PAGE_SIZE,
|
||||||
|
})
|
||||||
|
|
||||||
|
const pagination = useMemo(
|
||||||
|
() => ({
|
||||||
|
pageIndex,
|
||||||
|
pageSize,
|
||||||
|
}),
|
||||||
|
[pageIndex, pageSize]
|
||||||
|
)
|
||||||
|
|
||||||
|
const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
|
||||||
|
|
||||||
|
const { publishable_api_keys, count, isLoading, isError, error } =
|
||||||
|
useAdminPublishableApiKeys({})
|
||||||
|
|
||||||
|
const columns = useColumns()
|
||||||
|
|
||||||
|
const table = useReactTable({
|
||||||
|
data: publishable_api_keys || [],
|
||||||
|
columns,
|
||||||
|
pageCount: Math.ceil((count ?? 0) / PAGE_SIZE),
|
||||||
|
state: {
|
||||||
|
pagination,
|
||||||
|
rowSelection,
|
||||||
|
},
|
||||||
|
getRowId: (row) => row.id,
|
||||||
|
onPaginationChange: setPagination,
|
||||||
|
onRowSelectionChange: setRowSelection,
|
||||||
|
getCoreRowModel: getCoreRowModel(),
|
||||||
|
manualPagination: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const navigate = useNavigate()
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return <div>Loading...</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isError) {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container className="p-0">
|
||||||
|
<div className="px-6 py-4 flex items-center justify-between">
|
||||||
|
<Heading level="h2">{t("apiKeyManagement.domain")}</Heading>
|
||||||
|
<Link to="create">
|
||||||
|
<Button variant="secondary" size="small">
|
||||||
|
{t("general.create")}
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{(publishable_api_keys?.length ?? 0) > 0 ? (
|
||||||
|
<div>
|
||||||
|
<Table>
|
||||||
|
<Table.Header className="border-t-0">
|
||||||
|
{table.getHeaderGroups().map((headerGroup) => {
|
||||||
|
return (
|
||||||
|
<Table.Row
|
||||||
|
key={headerGroup.id}
|
||||||
|
className="[&_th:last-of-type]:w-[1%] [&_th:last-of-type]:whitespace-nowrap [&_th]:w-1/3"
|
||||||
|
>
|
||||||
|
{headerGroup.headers.map((header) => {
|
||||||
|
return (
|
||||||
|
<Table.HeaderCell key={header.id}>
|
||||||
|
{flexRender(
|
||||||
|
header.column.columnDef.header,
|
||||||
|
header.getContext()
|
||||||
|
)}
|
||||||
|
</Table.HeaderCell>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</Table.Row>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</Table.Header>
|
||||||
|
<Table.Body className="border-b-0">
|
||||||
|
{table.getRowModel().rows.map((row) => (
|
||||||
|
<Table.Row
|
||||||
|
key={row.id}
|
||||||
|
className={clx(
|
||||||
|
"transition-fg cursor-pointer [&_td:last-of-type]:w-[1%] [&_td:last-of-type]:whitespace-nowrap",
|
||||||
|
{
|
||||||
|
"bg-ui-bg-highlight hover:bg-ui-bg-highlight-hover":
|
||||||
|
row.getIsSelected(),
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
onClick={() =>
|
||||||
|
navigate(
|
||||||
|
`/settings/api-key-management/${row.original.id}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{row.getVisibleCells().map((cell) => (
|
||||||
|
<Table.Cell key={cell.id}>
|
||||||
|
{flexRender(
|
||||||
|
cell.column.columnDef.cell,
|
||||||
|
cell.getContext()
|
||||||
|
)}
|
||||||
|
</Table.Cell>
|
||||||
|
))}
|
||||||
|
</Table.Row>
|
||||||
|
))}
|
||||||
|
</Table.Body>
|
||||||
|
</Table>
|
||||||
|
<LocalizedTablePagination
|
||||||
|
canNextPage={table.getCanNextPage()}
|
||||||
|
canPreviousPage={table.getCanPreviousPage()}
|
||||||
|
nextPage={table.nextPage}
|
||||||
|
previousPage={table.previousPage}
|
||||||
|
count={count ?? 0}
|
||||||
|
pageIndex={pageIndex}
|
||||||
|
pageCount={table.getPageCount()}
|
||||||
|
pageSize={PAGE_SIZE}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<NoRecords
|
||||||
|
action={{
|
||||||
|
label: t("apiKeyManagement.createKey"),
|
||||||
|
to: "create",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const KeyActions = ({ apiKey }: { apiKey: PublishableApiKey }) => {
|
||||||
|
const { mutateAsync: revokeAsync } = useAdminRevokePublishableApiKey(
|
||||||
|
apiKey.id
|
||||||
|
)
|
||||||
|
const { mutateAsync: deleteAsync } = useAdminDeletePublishableApiKey(
|
||||||
|
apiKey.id
|
||||||
|
)
|
||||||
|
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const prompt = usePrompt()
|
||||||
|
|
||||||
|
const handleDelete = async () => {
|
||||||
|
const res = await prompt({
|
||||||
|
title: t("general.areYouSure"),
|
||||||
|
description: t("apiKeyManagement.deleteKeyWarning", {
|
||||||
|
title: apiKey.title,
|
||||||
|
}),
|
||||||
|
confirmText: t("general.delete"),
|
||||||
|
cancelText: t("general.cancel"),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!res) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await deleteAsync()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleRevoke = async () => {
|
||||||
|
const res = await prompt({
|
||||||
|
title: t("general.areYouSure"),
|
||||||
|
description: t("apiKeyManagement.revokeKeyWarning", {
|
||||||
|
title: apiKey.title,
|
||||||
|
}),
|
||||||
|
confirmText: t("apiKeyManagement.revoke"),
|
||||||
|
cancelText: t("general.cancel"),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!res) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await revokeAsync()
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenu.Trigger asChild>
|
||||||
|
<IconButton size="small" variant="transparent">
|
||||||
|
<EllipsisHorizontal />
|
||||||
|
</IconButton>
|
||||||
|
</DropdownMenu.Trigger>
|
||||||
|
<DropdownMenu.Content>
|
||||||
|
<DropdownMenu.Item onClick={handleRevoke}>
|
||||||
|
<div className="flex items-center gap-x-2 [&_svg]:text-ui-fg-subtle">
|
||||||
|
<XCircle />
|
||||||
|
<span onClick={handleRevoke}>{t("apiKeyManagement.revoke")}</span>
|
||||||
|
</div>
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
<DropdownMenu.Item onClick={handleDelete}>
|
||||||
|
<div className="flex items-center gap-x-2 [&_svg]:text-ui-fg-subtle">
|
||||||
|
<Trash />
|
||||||
|
<span>{t("general.delete")}</span>
|
||||||
|
</div>
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
</DropdownMenu.Content>
|
||||||
|
</DropdownMenu>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const columnHelper = createColumnHelper<PublishableApiKey>()
|
||||||
|
|
||||||
|
const useColumns = () => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
const columns = useMemo(
|
||||||
|
() => [
|
||||||
|
columnHelper.accessor("title", {
|
||||||
|
header: t("fields.title"),
|
||||||
|
cell: ({ getValue }) => getValue(),
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("id", {
|
||||||
|
header: "Key",
|
||||||
|
cell: ({ getValue }) => getValue(),
|
||||||
|
}),
|
||||||
|
columnHelper.display({
|
||||||
|
id: "actions",
|
||||||
|
cell: ({ row }) => {
|
||||||
|
return <KeyActions apiKey={row.original} />
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
[t]
|
||||||
|
)
|
||||||
|
|
||||||
|
return columns
|
||||||
|
}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export * from "./api-key-management-list-table"
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export { ApiKeyManagementList as Component } from "./api-key-management-list"
|
||||||
@@ -1 +0,0 @@
|
|||||||
export { ApiKeyManagement as Component } from "./api-key-management"
|
|
||||||
-159
@@ -1,159 +0,0 @@
|
|||||||
import { Store } from "@medusajs/medusa"
|
|
||||||
import { Button, Drawer, Heading, Select } from "@medusajs/ui"
|
|
||||||
import { useAdminUpdateStore } from "medusa-react"
|
|
||||||
import { useState } from "react"
|
|
||||||
import { useForm } from "react-hook-form"
|
|
||||||
import { useTranslation } from "react-i18next"
|
|
||||||
import * as zod from "zod"
|
|
||||||
import { Form } from "../../../../components/common/form"
|
|
||||||
|
|
||||||
const EditCurrenciesDetailsSchema = zod.object({
|
|
||||||
default_currency_code: zod.string(),
|
|
||||||
})
|
|
||||||
|
|
||||||
type EditCurrenciesDetailsDrawerProps = {
|
|
||||||
store: Store
|
|
||||||
}
|
|
||||||
|
|
||||||
export const EditCurrenciesDetailsDrawer = ({
|
|
||||||
store,
|
|
||||||
}: EditCurrenciesDetailsDrawerProps) => {
|
|
||||||
const [open, setOpen] = useState(false)
|
|
||||||
const [selectOpen, setSelectOpen] = useState(false)
|
|
||||||
|
|
||||||
const { t } = useTranslation()
|
|
||||||
|
|
||||||
const { mutateAsync } = useAdminUpdateStore()
|
|
||||||
|
|
||||||
const form = useForm<zod.infer<typeof EditCurrenciesDetailsSchema>>({
|
|
||||||
defaultValues: {
|
|
||||||
default_currency_code: store.default_currency_code,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const sortedCurrencies = store.currencies.sort((a, b) => {
|
|
||||||
if (a.code === store.default_currency_code) {
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
|
|
||||||
if (b.code === store.default_currency_code) {
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
return a.code.localeCompare(b.code)
|
|
||||||
})
|
|
||||||
|
|
||||||
const onOpenChange = (open: boolean) => {
|
|
||||||
if (!open) {
|
|
||||||
form.reset()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* We need to close the select when the drawer closes.
|
|
||||||
* Otherwise it may lead to `pointer-events: none` being applied to the body.
|
|
||||||
*/
|
|
||||||
setSelectOpen(false)
|
|
||||||
}
|
|
||||||
setOpen(open)
|
|
||||||
}
|
|
||||||
|
|
||||||
const onSubmit = form.handleSubmit(async (values) => {
|
|
||||||
await mutateAsync(
|
|
||||||
{
|
|
||||||
default_currency_code: values.default_currency_code,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
onSuccess: ({ store }) => {
|
|
||||||
form.reset({
|
|
||||||
default_currency_code: store.default_currency_code,
|
|
||||||
})
|
|
||||||
|
|
||||||
onOpenChange(false)
|
|
||||||
},
|
|
||||||
onError: (err) => {
|
|
||||||
console.log(err)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Drawer open={open} onOpenChange={onOpenChange}>
|
|
||||||
<Drawer.Trigger asChild>
|
|
||||||
<Button variant="secondary">
|
|
||||||
{t("currencies.editCurrencyDetails")}
|
|
||||||
</Button>
|
|
||||||
</Drawer.Trigger>
|
|
||||||
<Drawer.Content>
|
|
||||||
<Drawer.Header>
|
|
||||||
<Heading>{t("currencies.editCurrencyDetails")}</Heading>
|
|
||||||
</Drawer.Header>
|
|
||||||
<Drawer.Body>
|
|
||||||
<Form {...form}>
|
|
||||||
<form>
|
|
||||||
<Form.Field
|
|
||||||
name="default_currency_code"
|
|
||||||
control={form.control}
|
|
||||||
render={({ field: { ref, ...field } }) => {
|
|
||||||
return (
|
|
||||||
<Form.Item className="gap-y-2">
|
|
||||||
<Form.Label>{t("currencies.defaultCurrency")}</Form.Label>
|
|
||||||
<div>
|
|
||||||
<Form.Control>
|
|
||||||
<Select
|
|
||||||
{...field}
|
|
||||||
open={selectOpen}
|
|
||||||
onOpenChange={setSelectOpen}
|
|
||||||
value={field.value}
|
|
||||||
onValueChange={field.onChange}
|
|
||||||
size="small"
|
|
||||||
>
|
|
||||||
<Select.Trigger ref={ref}>
|
|
||||||
<Select.Value>
|
|
||||||
<span>
|
|
||||||
<span className="txt-compact-small-plus uppercase">
|
|
||||||
{field.value}
|
|
||||||
</span>{" "}
|
|
||||||
{
|
|
||||||
sortedCurrencies.find(
|
|
||||||
(curr) => curr.code === field.value
|
|
||||||
)?.name
|
|
||||||
}
|
|
||||||
</span>
|
|
||||||
</Select.Value>
|
|
||||||
</Select.Trigger>
|
|
||||||
<Select.Content>
|
|
||||||
{sortedCurrencies.map((curr) => (
|
|
||||||
<Select.Item key={curr.code} value={curr.code}>
|
|
||||||
<span>
|
|
||||||
<span className="txt-compact-small-plus uppercase">
|
|
||||||
{curr.code}
|
|
||||||
</span>{" "}
|
|
||||||
{curr.name}
|
|
||||||
</span>
|
|
||||||
</Select.Item>
|
|
||||||
))}
|
|
||||||
</Select.Content>
|
|
||||||
</Select>
|
|
||||||
</Form.Control>
|
|
||||||
<Form.ErrorMessage />
|
|
||||||
</div>
|
|
||||||
<Form.Hint>
|
|
||||||
{t("currencies.defaultCurrencyHint")}
|
|
||||||
</Form.Hint>
|
|
||||||
</Form.Item>
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</form>
|
|
||||||
</Form>
|
|
||||||
</Drawer.Body>
|
|
||||||
<Drawer.Footer>
|
|
||||||
<Drawer.Close asChild>
|
|
||||||
<Button variant="secondary">{t("general.cancel")}</Button>
|
|
||||||
</Drawer.Close>
|
|
||||||
<Button onClick={onSubmit}>{t("general.save")}</Button>
|
|
||||||
</Drawer.Footer>
|
|
||||||
</Drawer.Content>
|
|
||||||
</Drawer>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
-1
@@ -1 +0,0 @@
|
|||||||
export * from "./edit-currencies-details-drawer"
|
|
||||||
-488
@@ -1,488 +0,0 @@
|
|||||||
import {
|
|
||||||
BuildingTax,
|
|
||||||
CurrencyDollar,
|
|
||||||
EllipsisHorizontal,
|
|
||||||
} from "@medusajs/icons"
|
|
||||||
import { Currency, Store } from "@medusajs/medusa"
|
|
||||||
import {
|
|
||||||
Badge,
|
|
||||||
Button,
|
|
||||||
Checkbox,
|
|
||||||
CommandBar,
|
|
||||||
Container,
|
|
||||||
DropdownMenu,
|
|
||||||
FocusModal,
|
|
||||||
Heading,
|
|
||||||
IconButton,
|
|
||||||
Table,
|
|
||||||
Text,
|
|
||||||
Tooltip,
|
|
||||||
clx,
|
|
||||||
usePrompt,
|
|
||||||
} from "@medusajs/ui"
|
|
||||||
import {
|
|
||||||
PaginationState,
|
|
||||||
RowSelectionState,
|
|
||||||
createColumnHelper,
|
|
||||||
flexRender,
|
|
||||||
getCoreRowModel,
|
|
||||||
getPaginationRowModel,
|
|
||||||
useReactTable,
|
|
||||||
} from "@tanstack/react-table"
|
|
||||||
import {
|
|
||||||
useAdminCurrencies,
|
|
||||||
useAdminStore,
|
|
||||||
useAdminUpdateStore,
|
|
||||||
} from "medusa-react"
|
|
||||||
import { useMemo, useState } from "react"
|
|
||||||
import { useTranslation } from "react-i18next"
|
|
||||||
import { EditCurrenciesDetailsDrawer } from "../../components/edit-currencies-details-drawer"
|
|
||||||
|
|
||||||
export const CurrenciesDetails = () => {
|
|
||||||
const { t } = useTranslation()
|
|
||||||
|
|
||||||
const { store, isLoading } = useAdminStore()
|
|
||||||
|
|
||||||
if (isLoading || !store) {
|
|
||||||
return <div>Loading...</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col gap-y-2">
|
|
||||||
<Container className="p-0">
|
|
||||||
<div className="flex items-center justify-between px-8 py-6">
|
|
||||||
<div>
|
|
||||||
<Heading>{t("currencies.domain")}</Heading>
|
|
||||||
<Text size="small" className="text-ui-fg-subtle">
|
|
||||||
{t("currencies.manageTheCurrencies")}
|
|
||||||
</Text>
|
|
||||||
</div>
|
|
||||||
<EditCurrenciesDetailsDrawer store={store} />
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-2 border-t px-8 py-6">
|
|
||||||
<Text size="small" leading="compact" weight="plus">
|
|
||||||
{t("currencies.defaultCurrency")}
|
|
||||||
</Text>
|
|
||||||
<div className="flex items-center gap-x-2">
|
|
||||||
<Badge rounded="full" className="uppercase">
|
|
||||||
{store.default_currency_code}
|
|
||||||
</Badge>
|
|
||||||
<Text size="small" leading="compact">
|
|
||||||
{store.default_currency.name}
|
|
||||||
</Text>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Container>
|
|
||||||
<StoreCurrencySection store={store} />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
type StoreCurrenciesSectionProps = {
|
|
||||||
store: Store
|
|
||||||
}
|
|
||||||
|
|
||||||
const PAGE_SIZE = 20
|
|
||||||
|
|
||||||
const StoreCurrencySection = ({ store }: StoreCurrenciesSectionProps) => {
|
|
||||||
const [addModalOpen, setAddModalOpen] = useState(false)
|
|
||||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
|
|
||||||
const { mutateAsync } = useAdminUpdateStore()
|
|
||||||
const prompt = usePrompt()
|
|
||||||
const { t } = useTranslation()
|
|
||||||
const pageCount = Math.ceil(store.currencies.length / PAGE_SIZE)
|
|
||||||
const columns = useStoreCurrencyColumns()
|
|
||||||
|
|
||||||
const table = useReactTable({
|
|
||||||
data: store.currencies,
|
|
||||||
columns,
|
|
||||||
getCoreRowModel: getCoreRowModel(),
|
|
||||||
getPaginationRowModel: getPaginationRowModel(),
|
|
||||||
onRowSelectionChange: setRowSelection,
|
|
||||||
pageCount: pageCount,
|
|
||||||
state: {
|
|
||||||
rowSelection,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const onDeleteCurrencies = async () => {
|
|
||||||
const ids = Object.keys(rowSelection)
|
|
||||||
|
|
||||||
const result = await prompt({
|
|
||||||
title: t("general.areYouSure"),
|
|
||||||
description: t("currencies.removeCurrenciesWarning", {
|
|
||||||
count: ids.length,
|
|
||||||
}),
|
|
||||||
confirmText: t("general.remove"),
|
|
||||||
cancelText: t("general.cancel"),
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!result) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
await mutateAsync({
|
|
||||||
currencies: store.currencies
|
|
||||||
.filter((c) => !ids.includes(c.code))
|
|
||||||
.map((c) => c.code),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Container className="p-0">
|
|
||||||
<div className="flex items-center justify-between px-8 pb-4 pt-6">
|
|
||||||
<Heading level="h2">Store Currencies</Heading>
|
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownMenu.Trigger asChild>
|
|
||||||
<IconButton variant="transparent">
|
|
||||||
<EllipsisHorizontal />
|
|
||||||
</IconButton>
|
|
||||||
</DropdownMenu.Trigger>
|
|
||||||
<DropdownMenu.Content>
|
|
||||||
<DropdownMenu.Item
|
|
||||||
className="gap-x-2"
|
|
||||||
onClick={() => setAddModalOpen(!addModalOpen)}
|
|
||||||
>
|
|
||||||
<CurrencyDollar className="text-ui-fg-subtle" />
|
|
||||||
<span>Add Currencies</span>
|
|
||||||
</DropdownMenu.Item>
|
|
||||||
<DropdownMenu.Item className="gap-x-2">
|
|
||||||
<BuildingTax className="text-ui-fg-subtle" />
|
|
||||||
<span>Tax Preferences</span>
|
|
||||||
</DropdownMenu.Item>
|
|
||||||
</DropdownMenu.Content>
|
|
||||||
</DropdownMenu>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Table>
|
|
||||||
<Table.Header>
|
|
||||||
{table.getHeaderGroups().map((headerGroup) => {
|
|
||||||
return (
|
|
||||||
<Table.Row
|
|
||||||
key={headerGroup.id}
|
|
||||||
className="[&_th:first-of-type]:w-[1%] [&_th:first-of-type]:whitespace-nowrap [&_th]:w-1/3"
|
|
||||||
>
|
|
||||||
{headerGroup.headers.map((header) => {
|
|
||||||
return (
|
|
||||||
<Table.HeaderCell key={header.id}>
|
|
||||||
{flexRender(
|
|
||||||
header.column.columnDef.header,
|
|
||||||
header.getContext()
|
|
||||||
)}
|
|
||||||
</Table.HeaderCell>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</Table.Row>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</Table.Header>
|
|
||||||
<Table.Body className="border-b-0">
|
|
||||||
{table.getRowModel().rows.map((row) => (
|
|
||||||
<Table.Row
|
|
||||||
key={row.id}
|
|
||||||
className={clx("transition-fg", {
|
|
||||||
"bg-ui-bg-highlight hover:bg-ui-bg-highlight-hover":
|
|
||||||
row.getIsSelected(),
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{row.getVisibleCells().map((cell) => (
|
|
||||||
<Table.Cell key={cell.id}>
|
|
||||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
|
||||||
</Table.Cell>
|
|
||||||
))}
|
|
||||||
</Table.Row>
|
|
||||||
))}
|
|
||||||
</Table.Body>
|
|
||||||
</Table>
|
|
||||||
<Table.Pagination
|
|
||||||
canNextPage={table.getCanNextPage()}
|
|
||||||
canPreviousPage={table.getCanPreviousPage()}
|
|
||||||
nextPage={table.nextPage}
|
|
||||||
previousPage={table.previousPage}
|
|
||||||
count={store.currencies.length}
|
|
||||||
pageIndex={table.getState().pagination.pageIndex}
|
|
||||||
pageCount={table.getPageCount()}
|
|
||||||
pageSize={PAGE_SIZE}
|
|
||||||
/>
|
|
||||||
<CommandBar open={!!Object.keys(rowSelection).length}>
|
|
||||||
<CommandBar.Bar>
|
|
||||||
<CommandBar.Value>
|
|
||||||
{t("general.countSelected", {
|
|
||||||
count: Object.keys(rowSelection).length,
|
|
||||||
})}
|
|
||||||
</CommandBar.Value>
|
|
||||||
<CommandBar.Seperator />
|
|
||||||
<CommandBar.Command
|
|
||||||
action={onDeleteCurrencies}
|
|
||||||
shortcut="r"
|
|
||||||
label={t("general.remove")}
|
|
||||||
/>
|
|
||||||
</CommandBar.Bar>
|
|
||||||
</CommandBar>
|
|
||||||
</div>
|
|
||||||
<AddCurrenciesModal
|
|
||||||
store={store}
|
|
||||||
onOpenChange={setAddModalOpen}
|
|
||||||
open={addModalOpen}
|
|
||||||
/>
|
|
||||||
</Container>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const storeCurrencyColumnHelper = createColumnHelper<Currency>()
|
|
||||||
|
|
||||||
const useStoreCurrencyColumns = () => {
|
|
||||||
const { t } = useTranslation()
|
|
||||||
|
|
||||||
return useMemo(
|
|
||||||
() => [
|
|
||||||
storeCurrencyColumnHelper.display({
|
|
||||||
id: "select",
|
|
||||||
header: ({ table }) => {
|
|
||||||
return (
|
|
||||||
<Checkbox
|
|
||||||
checked={
|
|
||||||
table.getIsSomePageRowsSelected()
|
|
||||||
? "indeterminate"
|
|
||||||
: table.getIsAllPageRowsSelected()
|
|
||||||
}
|
|
||||||
onCheckedChange={(value) =>
|
|
||||||
table.toggleAllPageRowsSelected(!!value)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
cell: ({ row }) => {
|
|
||||||
return (
|
|
||||||
<Checkbox
|
|
||||||
checked={row.getIsSelected()}
|
|
||||||
onCheckedChange={(value) => row.toggleSelected(!!value)}
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation()
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
storeCurrencyColumnHelper.accessor("code", {
|
|
||||||
header: t("fields.code"),
|
|
||||||
cell: ({ getValue }) => getValue().toUpperCase(),
|
|
||||||
}),
|
|
||||||
storeCurrencyColumnHelper.accessor("name", {
|
|
||||||
header: t("fields.name"),
|
|
||||||
cell: ({ getValue }) => getValue(),
|
|
||||||
}),
|
|
||||||
storeCurrencyColumnHelper.accessor("includes_tax", {
|
|
||||||
header: "Tax Inclusive Prices",
|
|
||||||
cell: ({ getValue }) => {
|
|
||||||
return getValue() ? t("general.enabled") : t("general.disabled")
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
[t]
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const CURRENCIES_PAGE_SIZE = 50
|
|
||||||
|
|
||||||
const AddCurrenciesModal = ({
|
|
||||||
store,
|
|
||||||
open,
|
|
||||||
onOpenChange,
|
|
||||||
}: {
|
|
||||||
store: Store
|
|
||||||
open: boolean
|
|
||||||
onOpenChange: (open: boolean) => void
|
|
||||||
}) => {
|
|
||||||
const [{ pageIndex, pageSize }, setPagination] = useState<PaginationState>({
|
|
||||||
pageIndex: 0,
|
|
||||||
pageSize: CURRENCIES_PAGE_SIZE,
|
|
||||||
})
|
|
||||||
|
|
||||||
const pagination = useMemo(
|
|
||||||
() => ({
|
|
||||||
pageIndex,
|
|
||||||
pageSize,
|
|
||||||
}),
|
|
||||||
[pageIndex, pageSize]
|
|
||||||
)
|
|
||||||
|
|
||||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
|
|
||||||
|
|
||||||
const { currencies, count, isLoading } = useAdminCurrencies({
|
|
||||||
limit: CURRENCIES_PAGE_SIZE,
|
|
||||||
offset: pageIndex * CURRENCIES_PAGE_SIZE,
|
|
||||||
})
|
|
||||||
|
|
||||||
const columns = useCurrencyColumns()
|
|
||||||
|
|
||||||
const table = useReactTable({
|
|
||||||
data: currencies ?? [],
|
|
||||||
columns,
|
|
||||||
pageCount: Math.ceil((count ?? 0) / CURRENCIES_PAGE_SIZE),
|
|
||||||
state: {
|
|
||||||
pagination,
|
|
||||||
rowSelection,
|
|
||||||
},
|
|
||||||
onPaginationChange: setPagination,
|
|
||||||
onRowSelectionChange: setRowSelection,
|
|
||||||
getCoreRowModel: getCoreRowModel(),
|
|
||||||
manualPagination: true,
|
|
||||||
meta: {
|
|
||||||
currencyCodes: store.currencies?.map((c) => c.code) ?? [],
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const { t } = useTranslation()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FocusModal open={open} onOpenChange={onOpenChange}>
|
|
||||||
<FocusModal.Content>
|
|
||||||
<FocusModal.Header>
|
|
||||||
<div className="flex items-center justify-end gap-x-2">
|
|
||||||
<Button variant="secondary">{t("general.cancel")}</Button>
|
|
||||||
<Button>{t("general.save")}</Button>
|
|
||||||
</div>
|
|
||||||
</FocusModal.Header>
|
|
||||||
<FocusModal.Body className="flex h-full w-full flex-col items-center overflow-y-auto">
|
|
||||||
<div className="w-full flex-1 overflow-y-auto">
|
|
||||||
<Table>
|
|
||||||
<Table.Header className="border-t-0">
|
|
||||||
{table.getHeaderGroups().map((headerGroup) => {
|
|
||||||
return (
|
|
||||||
<Table.Row
|
|
||||||
key={headerGroup.id}
|
|
||||||
className="[&_th:first-of-type]:w-[1%] [&_th:first-of-type]:whitespace-nowrap [&_th]:w-1/3"
|
|
||||||
>
|
|
||||||
{headerGroup.headers.map((header) => {
|
|
||||||
return (
|
|
||||||
<Table.HeaderCell key={header.id}>
|
|
||||||
{flexRender(
|
|
||||||
header.column.columnDef.header,
|
|
||||||
header.getContext()
|
|
||||||
)}
|
|
||||||
</Table.HeaderCell>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</Table.Row>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</Table.Header>
|
|
||||||
<Table.Body className="border-b-0">
|
|
||||||
{table.getRowModel().rows.map((row) => (
|
|
||||||
<Table.Row
|
|
||||||
key={row.id}
|
|
||||||
className={clx(
|
|
||||||
"transition-fg last-of-type:border-b-0",
|
|
||||||
{
|
|
||||||
"bg-ui-bg-disabled hover:bg-ui-bg-disabled":
|
|
||||||
store.currencies
|
|
||||||
.map((c) => c.code)
|
|
||||||
?.includes(row.original.code),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"bg-ui-bg-highlight hover:bg-ui-bg-highlight-hover":
|
|
||||||
row.getIsSelected(),
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{row.getVisibleCells().map((cell) => (
|
|
||||||
<Table.Cell key={cell.id}>
|
|
||||||
{flexRender(
|
|
||||||
cell.column.columnDef.cell,
|
|
||||||
cell.getContext()
|
|
||||||
)}
|
|
||||||
</Table.Cell>
|
|
||||||
))}
|
|
||||||
</Table.Row>
|
|
||||||
))}
|
|
||||||
</Table.Body>
|
|
||||||
</Table>
|
|
||||||
</div>
|
|
||||||
<div className="w-full border-t">
|
|
||||||
<Table.Pagination
|
|
||||||
canNextPage={table.getCanNextPage()}
|
|
||||||
canPreviousPage={table.getCanPreviousPage()}
|
|
||||||
nextPage={table.nextPage}
|
|
||||||
previousPage={table.previousPage}
|
|
||||||
count={count ?? 0}
|
|
||||||
pageIndex={pageIndex}
|
|
||||||
pageCount={table.getPageCount()}
|
|
||||||
pageSize={CURRENCIES_PAGE_SIZE}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</FocusModal.Body>
|
|
||||||
</FocusModal.Content>
|
|
||||||
</FocusModal>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const currencyColumnHelper = createColumnHelper<Currency>()
|
|
||||||
|
|
||||||
const useCurrencyColumns = () => {
|
|
||||||
const { t } = useTranslation()
|
|
||||||
|
|
||||||
return useMemo(
|
|
||||||
() => [
|
|
||||||
currencyColumnHelper.display({
|
|
||||||
id: "select",
|
|
||||||
header: ({ table }) => {
|
|
||||||
return (
|
|
||||||
<Checkbox
|
|
||||||
checked={
|
|
||||||
table.getIsSomePageRowsSelected()
|
|
||||||
? "indeterminate"
|
|
||||||
: table.getIsAllPageRowsSelected()
|
|
||||||
}
|
|
||||||
onCheckedChange={(value) =>
|
|
||||||
table.toggleAllPageRowsSelected(!!value)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
cell: ({ row, table }) => {
|
|
||||||
const { currencyCodes } = table.options.meta as {
|
|
||||||
currencyCodes: string[]
|
|
||||||
}
|
|
||||||
|
|
||||||
const isAdded = currencyCodes.includes(row.original.code)
|
|
||||||
|
|
||||||
const isSelected = row.getIsSelected() || isAdded
|
|
||||||
|
|
||||||
const Component = (
|
|
||||||
<Checkbox
|
|
||||||
checked={isSelected}
|
|
||||||
disabled={isAdded}
|
|
||||||
onCheckedChange={(value) => row.toggleSelected(!!value)}
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation()
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
|
|
||||||
if (isAdded) {
|
|
||||||
return (
|
|
||||||
<Tooltip
|
|
||||||
content={t("currencies.currencyAlreadyAdded")}
|
|
||||||
side="right"
|
|
||||||
>
|
|
||||||
{Component}
|
|
||||||
</Tooltip>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return Component
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
currencyColumnHelper.accessor("code", {
|
|
||||||
header: t("fields.code"),
|
|
||||||
cell: ({ getValue }) => getValue().toUpperCase(),
|
|
||||||
}),
|
|
||||||
currencyColumnHelper.accessor("name", {
|
|
||||||
header: t("fields.name"),
|
|
||||||
cell: ({ getValue }) => getValue(),
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
[t]
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export { CurrenciesDetails as Component } from "./currencies-details"
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export { LocationsList as Component } from "./list";
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import { Container, Heading } from "@medusajs/ui";
|
|
||||||
|
|
||||||
export const LocationsList = () => {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<Container>
|
|
||||||
<Heading>Locations</Heading>
|
|
||||||
</Container>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export { LocationAddSalesChannels as Component } from "./location-add-sales-channels"
|
||||||
+15
@@ -0,0 +1,15 @@
|
|||||||
|
import { FocusModal } from "@medusajs/ui"
|
||||||
|
import { useAdminAddLocationToSalesChannel } from "medusa-react"
|
||||||
|
import { useRouteModalState } from "../../../hooks/use-route-modal-state"
|
||||||
|
|
||||||
|
export const LocationAddSalesChannels = () => {
|
||||||
|
const [open, onOpenChange] = useRouteModalState()
|
||||||
|
|
||||||
|
const { mutateAsync } = useAdminAddLocationToSalesChannel() // TODO: We need a batch mutation instead of this to avoid multiple requests
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FocusModal open={open} onOpenChange={onOpenChange}>
|
||||||
|
<FocusModal.Content></FocusModal.Content>
|
||||||
|
</FocusModal>
|
||||||
|
)
|
||||||
|
}
|
||||||
+234
@@ -0,0 +1,234 @@
|
|||||||
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
|
import { Button, FocusModal, Heading, Input, Text } from "@medusajs/ui"
|
||||||
|
import { useAdminCreateStockLocation } from "medusa-react"
|
||||||
|
import { useForm } from "react-hook-form"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
import * as zod from "zod"
|
||||||
|
import { CountrySelect } from "../../../../../components/common/country-select"
|
||||||
|
import { Form } from "../../../../../components/common/form"
|
||||||
|
|
||||||
|
const CreateLocationSchema = zod.object({
|
||||||
|
name: zod.string().min(1),
|
||||||
|
address: zod.object({
|
||||||
|
address_1: zod.string().min(1),
|
||||||
|
address_2: zod.string().optional(),
|
||||||
|
country_code: zod.string().min(2).max(2),
|
||||||
|
city: zod.string().optional(),
|
||||||
|
postal_code: zod.string().optional(),
|
||||||
|
province: zod.string().optional(),
|
||||||
|
company: zod.string().optional(),
|
||||||
|
phone: zod.string().optional(), // TODO: Add validation
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const CreateLocationForm = () => {
|
||||||
|
const { mutateAsync, isLoading } = useAdminCreateStockLocation()
|
||||||
|
|
||||||
|
const form = useForm<zod.infer<typeof CreateLocationSchema>>({
|
||||||
|
defaultValues: {
|
||||||
|
name: "",
|
||||||
|
address: {
|
||||||
|
address_1: "",
|
||||||
|
address_2: "",
|
||||||
|
city: "",
|
||||||
|
company: "",
|
||||||
|
country_code: "",
|
||||||
|
phone: "",
|
||||||
|
postal_code: "",
|
||||||
|
province: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
resolver: zodResolver(CreateLocationSchema),
|
||||||
|
})
|
||||||
|
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
const handleSubmit = form.handleSubmit(async (values) => {
|
||||||
|
mutateAsync(
|
||||||
|
{
|
||||||
|
name: values.name,
|
||||||
|
address: values.address,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSuccess: () => {},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form {...form}>
|
||||||
|
<form
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
className="flex h-full flex-col overflow-hidden"
|
||||||
|
>
|
||||||
|
<FocusModal.Header>
|
||||||
|
<div className="flex items-center justify-end gap-x-2">
|
||||||
|
<FocusModal.Close asChild>
|
||||||
|
<Button size="small" variant="secondary">
|
||||||
|
{t("general.cancel")}
|
||||||
|
</Button>
|
||||||
|
</FocusModal.Close>
|
||||||
|
<Button type="submit" size="small" isLoading={isLoading}>
|
||||||
|
{t("general.save")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</FocusModal.Header>
|
||||||
|
<FocusModal.Body className="flex flex-1 flex-col overflow-hidden">
|
||||||
|
<div className="flex flex-1 flex-col items-center overflow-y-auto">
|
||||||
|
<div className="flex w-full max-w-[720px] flex-col gap-y-10 px-2 pb-6 pt-[72px]">
|
||||||
|
<div>
|
||||||
|
<Heading className="capitalize">
|
||||||
|
{t("locations.createLocation")}
|
||||||
|
</Heading>
|
||||||
|
<Text size="small" className="text-ui-fg-subtle">
|
||||||
|
{t("locations.detailsHint")}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<Form.Field
|
||||||
|
control={form.control}
|
||||||
|
name="name"
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<Form.Item>
|
||||||
|
<Form.Label>{t("fields.name")}</Form.Label>
|
||||||
|
<Form.Control>
|
||||||
|
<Input size="small" {...field} />
|
||||||
|
</Form.Control>
|
||||||
|
<Form.ErrorMessage />
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<Form.Field
|
||||||
|
control={form.control}
|
||||||
|
name="address.address_1"
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<Form.Item>
|
||||||
|
<Form.Label>{t("fields.address")}</Form.Label>
|
||||||
|
<Form.Control>
|
||||||
|
<Input size="small" {...field} />
|
||||||
|
</Form.Control>
|
||||||
|
<Form.ErrorMessage />
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Form.Field
|
||||||
|
control={form.control}
|
||||||
|
name="address.address_2"
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<Form.Item>
|
||||||
|
<Form.Label optional>{t("fields.address2")}</Form.Label>
|
||||||
|
<Form.Control>
|
||||||
|
<Input size="small" {...field} />
|
||||||
|
</Form.Control>
|
||||||
|
<Form.ErrorMessage />
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Form.Field
|
||||||
|
control={form.control}
|
||||||
|
name="address.postal_code"
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<Form.Item>
|
||||||
|
<Form.Label optional>
|
||||||
|
{t("fields.postalCode")}
|
||||||
|
</Form.Label>
|
||||||
|
<Form.Control>
|
||||||
|
<Input size="small" {...field} />
|
||||||
|
</Form.Control>
|
||||||
|
<Form.ErrorMessage />
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Form.Field
|
||||||
|
control={form.control}
|
||||||
|
name="address.city"
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<Form.Item>
|
||||||
|
<Form.Label optional>{t("fields.city")}</Form.Label>
|
||||||
|
<Form.Control>
|
||||||
|
<Input size="small" {...field} />
|
||||||
|
</Form.Control>
|
||||||
|
<Form.ErrorMessage />
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Form.Field
|
||||||
|
control={form.control}
|
||||||
|
name="address.country_code"
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<Form.Item>
|
||||||
|
<Form.Label>{t("fields.country")}</Form.Label>
|
||||||
|
<Form.Control>
|
||||||
|
<CountrySelect {...field} />
|
||||||
|
</Form.Control>
|
||||||
|
<Form.ErrorMessage />
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Form.Field
|
||||||
|
control={form.control}
|
||||||
|
name="address.province"
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<Form.Item>
|
||||||
|
<Form.Label optional>{t("fields.state")}</Form.Label>
|
||||||
|
<Form.Control>
|
||||||
|
<Input size="small" {...field} />
|
||||||
|
</Form.Control>
|
||||||
|
<Form.ErrorMessage />
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Form.Field
|
||||||
|
control={form.control}
|
||||||
|
name="address.company"
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<Form.Item>
|
||||||
|
<Form.Label optional>{t("fields.company")}</Form.Label>
|
||||||
|
<Form.Control>
|
||||||
|
<Input size="small" {...field} />
|
||||||
|
</Form.Control>
|
||||||
|
<Form.ErrorMessage />
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Form.Field
|
||||||
|
control={form.control}
|
||||||
|
name="address.phone"
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<Form.Item>
|
||||||
|
<Form.Label optional>{t("fields.phone")}</Form.Label>
|
||||||
|
<Form.Control>
|
||||||
|
<Input size="small" {...field} />
|
||||||
|
</Form.Control>
|
||||||
|
<Form.ErrorMessage />
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</FocusModal.Body>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
)
|
||||||
|
}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export * from "./create-location-form"
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { LocationCreate as Component } from "./location-create"
|
||||||
+15
@@ -0,0 +1,15 @@
|
|||||||
|
import { FocusModal } from "@medusajs/ui"
|
||||||
|
import { useRouteModalState } from "../../../hooks/use-route-modal-state"
|
||||||
|
import { CreateLocationForm } from "./components/create-location-form"
|
||||||
|
|
||||||
|
export const LocationCreate = () => {
|
||||||
|
const [open, onOpenChange] = useRouteModalState()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FocusModal open={open} onOpenChange={onOpenChange}>
|
||||||
|
<FocusModal.Content>
|
||||||
|
<CreateLocationForm />
|
||||||
|
</FocusModal.Content>
|
||||||
|
</FocusModal>
|
||||||
|
)
|
||||||
|
}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export * from "./location-general-section"
|
||||||
+101
@@ -0,0 +1,101 @@
|
|||||||
|
import type {
|
||||||
|
StockLocationAddressDTO,
|
||||||
|
StockLocationExpandedDTO,
|
||||||
|
} from "@medusajs/types"
|
||||||
|
import { Button, Container, Heading, Text, clx } from "@medusajs/ui"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
import { Link } from "react-router-dom"
|
||||||
|
|
||||||
|
type LocationGeneralSectionProps = {
|
||||||
|
location: StockLocationExpandedDTO
|
||||||
|
}
|
||||||
|
|
||||||
|
export const LocationGeneralSection = ({
|
||||||
|
location,
|
||||||
|
}: LocationGeneralSectionProps) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container className="divide-y p-0">
|
||||||
|
<div className="flex items-center justify-between px-6 py-4">
|
||||||
|
<Heading>{location.name}</Heading>
|
||||||
|
<Link to={"edit"}>
|
||||||
|
<Button size="small" variant="secondary">
|
||||||
|
{t("locations.editLocation")}
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 items-center px-6 py-4">
|
||||||
|
<Text size="small" weight="plus" leading="compact">
|
||||||
|
{t("fields.address")}
|
||||||
|
</Text>
|
||||||
|
<AddressDisplay address={location.address} />
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 items-center px-6 py-4">
|
||||||
|
<Text
|
||||||
|
size="small"
|
||||||
|
weight="plus"
|
||||||
|
leading="compact"
|
||||||
|
className={clx({
|
||||||
|
"text-ui-fg-subtle": !location.address?.company,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{t("fields.company")}
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
size="small"
|
||||||
|
leading="compact"
|
||||||
|
className={clx({
|
||||||
|
"text-ui-fg-subtle": !location.address?.company,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{location.address?.company || "-"}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 items-center px-6 py-4">
|
||||||
|
<Text size="small" weight="plus" leading="compact">
|
||||||
|
{t("fields.phone")}
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
size="small"
|
||||||
|
leading="compact"
|
||||||
|
className={clx({
|
||||||
|
"text-ui-fg-subtle": !location.address?.phone,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{location.address?.phone || "-"}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const AddressDisplay = ({
|
||||||
|
address,
|
||||||
|
}: {
|
||||||
|
address: StockLocationAddressDTO | undefined
|
||||||
|
}) => {
|
||||||
|
if (!address) {
|
||||||
|
return (
|
||||||
|
<Text size="small" className="text-ui-fg-subtle">
|
||||||
|
-
|
||||||
|
</Text>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const { address_1, address_2, city, province, postal_code, country_code } =
|
||||||
|
address
|
||||||
|
|
||||||
|
const addressParts = [
|
||||||
|
address_1,
|
||||||
|
address_2,
|
||||||
|
`${city ? city + " " : ""}${province ? province + " " : ""}${postal_code}`,
|
||||||
|
country_code.toUpperCase(),
|
||||||
|
]
|
||||||
|
|
||||||
|
const addressString = addressParts
|
||||||
|
.filter((part) => part !== null && part !== undefined && part.trim() !== "")
|
||||||
|
.join(", ")
|
||||||
|
|
||||||
|
return <Text size="small">{addressString}</Text>
|
||||||
|
}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export * from "./location-sales-channel-section"
|
||||||
+38
-32
@@ -1,6 +1,8 @@
|
|||||||
import { EllipsisHorizontal, PencilSquare, Trash } from "@medusajs/icons"
|
import { EllipsisHorizontal, PencilSquare, Trash } from "@medusajs/icons"
|
||||||
import { SalesChannel } from "@medusajs/medusa"
|
import { SalesChannel } from "@medusajs/medusa"
|
||||||
|
import { StockLocationExpandedDTO } from "@medusajs/types"
|
||||||
import {
|
import {
|
||||||
|
Button,
|
||||||
Container,
|
Container,
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
Heading,
|
Heading,
|
||||||
@@ -17,16 +19,22 @@ import {
|
|||||||
getCoreRowModel,
|
getCoreRowModel,
|
||||||
useReactTable,
|
useReactTable,
|
||||||
} from "@tanstack/react-table"
|
} from "@tanstack/react-table"
|
||||||
import { useAdminDeleteSalesChannel, useAdminSalesChannels } from "medusa-react"
|
|
||||||
import { useMemo, useState } from "react"
|
import { useMemo, useState } from "react"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
import { useNavigate } from "react-router-dom"
|
import { Link, useNavigate } from "react-router-dom"
|
||||||
|
import { NoRecords } from "../../../../../components/common/empty-table-content/empty-table-content"
|
||||||
|
|
||||||
const PAGE_SIZE = 50
|
type LocationSalesChannelSectionProps = {
|
||||||
|
location: StockLocationExpandedDTO
|
||||||
|
}
|
||||||
|
|
||||||
export const SalesChannelList = () => {
|
const PAGE_SIZE = 20
|
||||||
const navigate = useNavigate()
|
|
||||||
|
export const LocationSalesChannelSection = ({
|
||||||
|
location,
|
||||||
|
}: LocationSalesChannelSectionProps) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
const navigate = useNavigate()
|
||||||
|
|
||||||
const [{ pageIndex, pageSize }, setPagination] = useState<PaginationState>({
|
const [{ pageIndex, pageSize }, setPagination] = useState<PaginationState>({
|
||||||
pageIndex: 0,
|
pageIndex: 0,
|
||||||
@@ -43,15 +51,12 @@ export const SalesChannelList = () => {
|
|||||||
|
|
||||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
|
const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
|
||||||
|
|
||||||
const { sales_channels, count, isLoading } = useAdminSalesChannels({
|
const salesChannels = location.sales_channels
|
||||||
limit: PAGE_SIZE,
|
const count = location.sales_channels?.length || 0
|
||||||
offset: pageIndex * PAGE_SIZE,
|
|
||||||
})
|
|
||||||
|
|
||||||
const columns = useColumns()
|
const columns = useColumns()
|
||||||
|
|
||||||
const table = useReactTable({
|
const table = useReactTable({
|
||||||
data: sales_channels ?? [],
|
data: salesChannels ?? [],
|
||||||
columns,
|
columns,
|
||||||
pageCount: Math.ceil((count ?? 0) / PAGE_SIZE),
|
pageCount: Math.ceil((count ?? 0) / PAGE_SIZE),
|
||||||
state: {
|
state: {
|
||||||
@@ -65,12 +70,17 @@ export const SalesChannelList = () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-y-2">
|
<Container className="divide-y p-0">
|
||||||
<Container className="p-0">
|
<div className="flex items-center justify-between px-6 py-4">
|
||||||
<div className="px-8 pb-4 pt-6">
|
<Heading level="h2">Sales Channels</Heading>
|
||||||
<Heading>{t("salesChannels.domain")}</Heading>
|
<Link to={"add-sales-channels"}>
|
||||||
</div>
|
<Button size="small" variant="secondary">
|
||||||
<div>
|
{t("locations.addSalesChannels")}
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{count ? (
|
||||||
<Table>
|
<Table>
|
||||||
<Table.Header>
|
<Table.Header>
|
||||||
{table.getHeaderGroups().map((headerGroup) => {
|
{table.getHeaderGroups().map((headerGroup) => {
|
||||||
@@ -120,24 +130,20 @@ export const SalesChannelList = () => {
|
|||||||
))}
|
))}
|
||||||
</Table.Body>
|
</Table.Body>
|
||||||
</Table>
|
</Table>
|
||||||
<Table.Pagination
|
) : (
|
||||||
canNextPage={table.getCanNextPage()}
|
<NoRecords
|
||||||
canPreviousPage={table.getCanPreviousPage()}
|
action={{
|
||||||
nextPage={table.nextPage}
|
label: t("locations.addSalesChannels"),
|
||||||
previousPage={table.previousPage}
|
to: "add-sales-channels",
|
||||||
count={count ?? 0}
|
}}
|
||||||
pageIndex={pageIndex}
|
|
||||||
pageCount={table.getPageCount()}
|
|
||||||
pageSize={PAGE_SIZE}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
)}
|
||||||
</Container>
|
</div>
|
||||||
</div>
|
</Container>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const SalesChannelActions = ({ id }: { id: string }) => {
|
const SalesChannelActions = ({ id }: { id: string }) => {
|
||||||
const { mutateAsync } = useAdminDeleteSalesChannel(id)
|
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -178,11 +184,11 @@ const useColumns = () => {
|
|||||||
cell: ({ getValue }) => getValue(),
|
cell: ({ getValue }) => getValue(),
|
||||||
}),
|
}),
|
||||||
columnHelper.accessor("is_disabled", {
|
columnHelper.accessor("is_disabled", {
|
||||||
header: () => <div className="text-right">{t("fields.status")}</div>,
|
header: t("fields.status"),
|
||||||
cell: ({ getValue }) => {
|
cell: ({ getValue }) => {
|
||||||
const value = getValue()
|
const value = getValue()
|
||||||
return (
|
return (
|
||||||
<div className="text-right">
|
<div>
|
||||||
<StatusBadge color={value ? "grey" : "green"}>
|
<StatusBadge color={value ? "grey" : "green"}>
|
||||||
{value ? t("general.disabled") : t("general.enabled")}
|
{value ? t("general.disabled") : t("general.enabled")}
|
||||||
</StatusBadge>
|
</StatusBadge>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { LocationDetail as Component } from "./location-detail"
|
||||||
Loaded 100 of 266 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user