diff --git a/.changeset/cool-rockets-wash.md b/.changeset/cool-rockets-wash.md new file mode 100644 index 0000000000..e67340d512 --- /dev/null +++ b/.changeset/cool-rockets-wash.md @@ -0,0 +1,6 @@ +--- +"@medusajs/types": patch +"@medusajs/utils": patch +--- + +feat(utils,types): add registerUsages for promotion's computed actions diff --git a/.changeset/forty-shrimps-watch.md b/.changeset/forty-shrimps-watch.md new file mode 100644 index 0000000000..d60f718b33 --- /dev/null +++ b/.changeset/forty-shrimps-watch.md @@ -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`. diff --git a/.changeset/khaki-deers-talk.md b/.changeset/khaki-deers-talk.md new file mode 100644 index 0000000000..f8c86c93f3 --- /dev/null +++ b/.changeset/khaki-deers-talk.md @@ -0,0 +1,5 @@ +--- +"@medusajs/types": patch +--- + +Add line items in cart creation diff --git a/.changeset/lazy-shoes-kiss.md b/.changeset/lazy-shoes-kiss.md new file mode 100644 index 0000000000..e97e5b1f7e --- /dev/null +++ b/.changeset/lazy-shoes-kiss.md @@ -0,0 +1,5 @@ +--- +"@medusajs/types": patch +--- + +feat(types): add campaign + promotion operations diff --git a/.changeset/neat-seals-help.md b/.changeset/neat-seals-help.md new file mode 100644 index 0000000000..f3b0c6d196 --- /dev/null +++ b/.changeset/neat-seals-help.md @@ -0,0 +1,5 @@ +--- +"@medusajs/modules-sdk": patch +--- + +feat(modules-sdk): run hooks on application start diff --git a/.changeset/olive-ads-brake.md b/.changeset/olive-ads-brake.md new file mode 100644 index 0000000000..a5fc56c03b --- /dev/null +++ b/.changeset/olive-ads-brake.md @@ -0,0 +1,5 @@ +--- +"@medusajs/types": patch +--- + +feat(cart): Shipping methods diff --git a/.changeset/three-files-yawn.md b/.changeset/three-files-yawn.md new file mode 100644 index 0000000000..d894fc6595 --- /dev/null +++ b/.changeset/three-files-yawn.md @@ -0,0 +1,5 @@ +--- +"@medusajs/workflows-sdk": patch +--- + +create workflow can return destructured properties of a step diff --git a/.changeset/twenty-emus-roll.md b/.changeset/twenty-emus-roll.md new file mode 100644 index 0000000000..561ea8d3aa --- /dev/null +++ b/.changeset/twenty-emus-roll.md @@ -0,0 +1,8 @@ +--- +"@medusajs/pricing": patch +"@medusajs/product": patch +"@medusajs/utils": patch +"@medusajs/types": patch +--- + +chore: Abstract module services diff --git a/.eslintrc.js b/.eslintrc.js index be653df480..22a44eb628 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -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 }, extends: [ "eslint:recommended", @@ -236,7 +236,7 @@ module.exports = { ], parser: "@typescript-eslint/parser", parserOptions: { - project: "./packages/admin-next/dashboard/tsconfig.json", + project: "tsconfig.json", }, plugins: ["react-refresh"], rules: { diff --git a/CODEOWNERS b/CODEOWNERS index 423e085510..38f46e00f2 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1,5 +1,5 @@ # All files not owned by other teams must be reviewed by the core team -* @medusajs/core +* @medusajs/core @medusajs/engineering /docs-util/ @medusajs/docs /www/ @medusajs/docs /packages/admin @medusajs/ui diff --git a/integration-tests/plugins/__tests__/workflows/utils/composer/compose.ts b/integration-tests/plugins/__tests__/workflows/utils/composer/compose.ts index 27fb772003..9fdedc22c2 100644 --- a/integration-tests/plugins/__tests__/workflows/utils/composer/compose.ts +++ b/integration-tests/plugins/__tests__/workflows/utils/composer/compose.ts @@ -1980,4 +1980,110 @@ describe("Workflow composer", function () { expect(mockStep1Fn.mock.calls[0][0]).toEqual(workflowInput) expect(mockCompensateSte1.mock.calls[0][0]).toEqual(undefined) }) + + it("should compose a workflow that returns destructured properties", async () => { + const step = function () { + return new StepResponse({ + propertyNotReturned: 1234, + property: { + complex: { + nested: 123, + }, + a: "bc", + }, + obj: "return from 2", + }) + } + + const step1 = createStep("step1", step) + + const workflow = createWorkflow("workflow1", function () { + const { property, obj } = step1() + + return { someOtherName: property, obj } + }) + + const { result } = await workflow().run({ + throwOnError: false, + }) + + expect(result).toEqual({ + someOtherName: { + complex: { + nested: 123, + }, + a: "bc", + }, + obj: "return from 2", + }) + }) + + it("should compose a workflow that returns an array of steps", async () => { + const step1 = createStep("step1", () => { + return new StepResponse({ + obj: "return from 1", + }) + }) + const step2 = createStep("step2", () => { + return new StepResponse({ + obj: "returned from 2**", + }) + }) + + const workflow = createWorkflow("workflow1", function () { + const s1 = step1() + const s2 = step2() + + return [s1, s2] + }) + + const { result } = await workflow().run({ + throwOnError: false, + }) + + expect(result).toEqual([ + { + obj: "return from 1", + }, + { + obj: "returned from 2**", + }, + ]) + }) + + it("should compose a workflow that returns an object mixed of steps and properties", async () => { + const step1 = createStep("step1", () => { + return new StepResponse({ + obj: { + nested: "nested", + }, + }) + }) + + const step2 = createStep("step2", () => { + return new StepResponse({ + obj: "returned from 2**", + }) + }) + + const workflow = createWorkflow("workflow1", function () { + const { obj } = step1() + const s2 = step2() + + return [{ step1_nested_obj: obj.nested }, s2] + }) + + const { result } = await workflow().run({ + throwOnError: false, + }) + + expect(result).toEqual([ + { + step1_nested_obj: "nested", + }, + { + obj: "returned from 2**", + }, + ]) + }) }) diff --git a/packages/admin-next/dashboard/package.json b/packages/admin-next/dashboard/package.json index 39e57d182a..559d2a4a41 100644 --- a/packages/admin-next/dashboard/package.json +++ b/packages/admin-next/dashboard/package.json @@ -3,6 +3,7 @@ "private": true, "version": "0.0.0", "scripts": { + "generate:static": "node ./scripts/generate-countries.js && prettier --write ./src/lib/countries.ts && node ./scripts/generate-currencies.js && prettier --write ./src/lib/currencies.ts", "dev": "vite", "build": "vite build", "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0" @@ -23,6 +24,7 @@ "@tanstack/react-table": "8.10.7", "@uiw/react-json-view": "2.0.0-alpha.10", "cmdk": "^0.2.0", + "date-fns": "^3.2.0", "i18next": "23.7.11", "i18next-browser-languagedetector": "7.2.0", "i18next-http-backend": "2.4.2", @@ -36,15 +38,15 @@ }, "devDependencies": { "@medusajs/medusa": "workspace:^", + "@medusajs/types": "workspace:^", "@medusajs/ui-preset": "workspace:^", "@medusajs/vite-plugin-extension": "workspace:^", "@types/react": "18.2.43", "@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", "autoprefixer": "10.4.16", "postcss": "8.4.32", + "prettier": "^3.1.1", "tailwindcss": "3.3.6", "typescript": "5.2.2", "vite": "5.0.10" diff --git a/packages/admin-next/dashboard/public/locales/en/translation.json b/packages/admin-next/dashboard/public/locales/en/translation.json index 6a3b624d8b..ed7e017c13 100644 --- a/packages/admin-next/dashboard/public/locales/en/translation.json +++ b/packages/admin-next/dashboard/public/locales/en/translation.json @@ -1,12 +1,26 @@ { "$schema": "../$schema.json", "general": { + "ascending": "Ascending", + "descending": "Descending", "cancel": "Cancel", "save": "Save", "create": "Create", "delete": "Delete", "edit": "Edit", + "confirm": "Confirm", + "add": "Add", + "continue": "Continue", + "start": "Start", + "end": "End", + "apply": "Apply", + "range": "Range", "search": "Search", + "of": "of", + "results": "results", + "pages": "pages", + "next": "Next", + "prev": "Prev", "extensions": "Extensions", "settings": "Settings", "general": "General", @@ -14,17 +28,26 @@ "enabled": "Enabled", "disabled": "Disabled", "remove": "Remove", + "admin": "Admin", + "store": "Store", "countSelected": "{{count}} selected", "plusCountMore": "+ {{count}} more", "areYouSure": "Are you sure?", - "noRecordsFound": "No records found" + "noRecordsFound": "No records found", + "typeToConfirm": "Please type {val} to confirm:", + "noResultsTitle": "No results", + "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": { "domain": "Products", "variants": "Variants", "availableInSalesChannels": "Available in <0>{{x}} of <1>{{y}} sales channels", - "inStockVariants_one": "{{inventory}} in stock for {{count}} variant", - "inStockVariants_other": "{{inventory}} in stock for {{count}} variants", + "variantCount_one": "{{count}} variant", + "variantCount_other": "{{count}} variants", "productStatus": { "draft": "Draft", "published": "Published", @@ -45,10 +68,28 @@ "domain": "Gift Cards" }, "customers": { - "domain": "Customers" + "domain": "Customers", + "editCustomer": "Edit Customer", + "createCustomer": "Create Customer", + "createCustomerHint": "Create a new customer to manage their details.", + "passwordHint": "Create a password for the customer to use when logging in to the storefront. Make sure that you communicate the password to the customer.", + "changePassword": "Change password", + "changePasswordPromptTitle": "Change password", + "changePasswordPromptDescription": "You are about to change the password for {{email}}. Make sure that you have communicated the new password to the customer before proceeding.", + "guest": "Guest", + "registered": "Registered", + "firstSeen": "First seen" }, "customerGroups": { - "domain": "Customer Groups" + "domain": "Customer Groups", + "createGroup": "Create group", + "createCustomerGroup": "Create Customer Group", + "createCustomerGroupHint": "Create a new customer group to segment your customers.", + "customerAlreadyAdded": "The customer has already been added to the group.", + "editCustomerGroup": "Edit Customer Group", + "removeCustomersWarning_one": "You are about to remove {{count}} customer from the customer group. This action cannot be undone.", + "removeCustomersWarning_other": "You are about to remove {{count}} customers from the customer group. This action cannot be undone.", + "deleteCustomerGroupWarning": "You are about to delete the customer group {{name}}. This action cannot be undone." }, "orders": { "domain": "Orders" @@ -65,9 +106,11 @@ "profile": { "domain": "Profile", "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.", - "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." + "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.", + "language": "Language", + "usageInsights": "Usage insights" }, "users": { "domain": "Users", @@ -81,38 +124,54 @@ "store": { "domain": "Store", "manageYourStoresDetails": "Manage your store's details", - "editStoreDetails": "Edit Store Details", - "storeName": "Store name", + "editStore": "Edit store", + "defaultCurrency": "Default currency", "swapLinkTemplate": "Swap 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": { - "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": { "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_other": "You are about to remove {{count}} products from {{sales_channel}}.", "addProducts": "Add Products", - "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." - }, - "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." + "editSalesChannel": "Edit 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." }, "apiKeyManagement": { "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": { "name": "Name", @@ -122,6 +181,7 @@ "description": "Description", "email": "Email", "password": "Password", + "confirmPassword": "Confirm Password", "categories": "Categories", "category": "Category", "collection": "Collection", @@ -133,8 +193,31 @@ "sales_channels": "Sales Channels", "status": "Status", "code": "Code", + "countries": "Countries", + "paymentProviders": "Payment Providers", + "fulfillmentProviders": "Fulfillment Providers", + "providers": "Providers", "availability": "Availability", "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", + "orders": "Orders", + "account": "Account", + "total": "Total" } } diff --git a/packages/admin-next/dashboard/scripts/generate-countries.js b/packages/admin-next/dashboard/scripts/generate-countries.js new file mode 100644 index 0000000000..491310e677 --- /dev/null +++ b/packages/admin-next/dashboard/scripts/generate-countries.js @@ -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[] = ${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) + } +})() diff --git a/packages/admin-next/dashboard/scripts/generate-currencies.js b/packages/admin-next/dashboard/scripts/generate-currencies.js new file mode 100644 index 0000000000..bf6608f5eb --- /dev/null +++ b/packages/admin-next/dashboard/scripts/generate-currencies.js @@ -0,0 +1,46 @@ +async function generateCurrencies() { + const { currencies } = await import( + "@medusajs/medusa/dist/utils/currencies.js" + ) + const fs = await import("fs") + const path = await import("path") + + const record = Object.entries(currencies).reduce((acc, [key, values]) => { + const code = values.code + const symbol_native = values.symbol_native + const name = values.name + const decimal_digits = values.decimal_digits + + acc[key] = { + code, + name, + symbol_native, + decimal_digits, + } + + return acc + }, {}) + + const json = JSON.stringify(record, null, 2) + + const dest = path.join(__dirname, "../src/lib/currencies.ts") + const destDir = path.dirname(dest) + + const fileContent = `/** This file is auto-generated. Do not modify it manually. */\ntype CurrencyInfo = { code: string; name: string; symbol_native: string; decimal_digits: number }\n\nexport const currencies: Record = ${json}` + + if (!fs.existsSync(destDir)) { + fs.mkdirSync(destDir, { recursive: true }) + } + + fs.writeFileSync(dest, fileContent) +} + +;(async () => { + console.log("Generating currency info") + try { + await generateCurrencies() + console.log("Currency info generated") + } catch (e) { + console.error(e) + } +})() diff --git a/packages/admin-next/dashboard/src/app.tsx b/packages/admin-next/dashboard/src/app.tsx index 9170f0ff08..602ac18fba 100644 --- a/packages/admin-next/dashboard/src/app.tsx +++ b/packages/admin-next/dashboard/src/app.tsx @@ -1,28 +1,22 @@ import { Toaster } from "@medusajs/ui" import { MedusaProvider } from "medusa-react" -import { AuthProvider } from "./providers/auth-provider" import { RouterProvider } from "./providers/router-provider" import { ThemeProvider } from "./providers/theme-provider" -import { queryClient } from "./lib/medusa" - -const BASE_URL = - import.meta.env.VITE_MEDUSA_ADMIN_BACKEND_URL || "http://localhost:9000" +import { MEDUSA_BACKEND_URL, queryClient } from "./lib/medusa" function App() { return ( - - - - + + ) diff --git a/packages/admin-next/dashboard/src/components/authentication/require-auth/require-auth.tsx b/packages/admin-next/dashboard/src/components/authentication/require-auth/require-auth.tsx index d64ad20b87..e3a29ae7ea 100644 --- a/packages/admin-next/dashboard/src/components/authentication/require-auth/require-auth.tsx +++ b/packages/admin-next/dashboard/src/components/authentication/require-auth/require-auth.tsx @@ -1,25 +1,31 @@ -import { Spinner } from "@medusajs/icons"; -import { PropsWithChildren } from "react"; -import { Navigate, useLocation } from "react-router-dom"; +import { Spinner } from "@medusajs/icons" +import { Navigate, Outlet, 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) => { - const auth = useAuth(); - const location = useLocation(); +export const ProtectedRoute = () => { + const { user, isLoading } = useAdminGetSession() + const location = useLocation() - if (auth.isLoading) { + if (isLoading) { return ( -
- +
+
- ); + ) } - if (!auth.user) { - console.log("redirecting"); - return ; + if (!user) { + return } - return children; -}; + return ( + + + + + + ) +} diff --git a/packages/admin-next/dashboard/src/components/common/combobox/combobox.tsx b/packages/admin-next/dashboard/src/components/common/combobox/combobox.tsx new file mode 100644 index 0000000000..b499a8e943 --- /dev/null +++ b/packages/admin-next/dashboard/src/components/common/combobox/combobox.tsx @@ -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 ( + + + + + + ) +} diff --git a/packages/admin-next/dashboard/src/components/common/country-select/country-select.tsx b/packages/admin-next/dashboard/src/components/common/country-select/country-select.tsx new file mode 100644 index 0000000000..5a2dd75ef9 --- /dev/null +++ b/packages/admin-next/dashboard/src/components/common/country-select/country-select.tsx @@ -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 ( +
+ + +
+ ) +}) +CountrySelect.displayName = "CountrySelect" diff --git a/packages/admin-next/dashboard/src/components/common/country-select/index.ts b/packages/admin-next/dashboard/src/components/common/country-select/index.ts new file mode 100644 index 0000000000..4e19e8a7eb --- /dev/null +++ b/packages/admin-next/dashboard/src/components/common/country-select/index.ts @@ -0,0 +1 @@ +export * from "./country-select" diff --git a/packages/admin-next/dashboard/src/components/common/debounced-search/debounced-search.tsx b/packages/admin-next/dashboard/src/components/common/debounced-search/debounced-search.tsx index 49fe276389..d763a2ec3f 100644 --- a/packages/admin-next/dashboard/src/components/common/debounced-search/debounced-search.tsx +++ b/packages/admin-next/dashboard/src/components/common/debounced-search/debounced-search.tsx @@ -15,6 +15,7 @@ export const DebouncedSearch = ({ value: initialValue, onChange, debounce = 500, + size = "small", placeholder, ...props }: DebouncedSearchProps) => { diff --git a/packages/admin-next/dashboard/src/components/common/empty-table-content/empty-table-content.tsx b/packages/admin-next/dashboard/src/components/common/empty-table-content/empty-table-content.tsx new file mode 100644 index 0000000000..858d344311 --- /dev/null +++ b/packages/admin-next/dashboard/src/components/common/empty-table-content/empty-table-content.tsx @@ -0,0 +1,61 @@ +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 ( +
+
+ + + {title ?? t("general.noResultsTitle")} + + + {message ?? t("general.noResultsMessage")} + +
+
+ ) +} + +type NoRecordsProps = { + title?: string + message?: string + action?: { + to: string + label: string + } +} + +export const NoRecords = ({ title, message, action }: NoRecordsProps) => { + const { t } = useTranslation() + + return ( +
+
+ + + {title ?? t("general.noRecordsTitle")} + + + {message ?? t("general.noRecordsMessage")} + +
+ {action && ( + + + + )} +
+ ) +} diff --git a/packages/admin-next/dashboard/src/components/common/empty-table-content/index.ts b/packages/admin-next/dashboard/src/components/common/empty-table-content/index.ts new file mode 100644 index 0000000000..405e5ca379 --- /dev/null +++ b/packages/admin-next/dashboard/src/components/common/empty-table-content/index.ts @@ -0,0 +1 @@ +export * from "./empty-table-content" diff --git a/packages/admin-next/dashboard/src/components/common/json-view-section/index.ts b/packages/admin-next/dashboard/src/components/common/json-view-section/index.ts new file mode 100644 index 0000000000..be52949afd --- /dev/null +++ b/packages/admin-next/dashboard/src/components/common/json-view-section/index.ts @@ -0,0 +1 @@ +export * from "./json-view-section" diff --git a/packages/admin-next/dashboard/src/components/common/json-view/json-view.tsx b/packages/admin-next/dashboard/src/components/common/json-view-section/json-view-section.tsx similarity index 88% rename from packages/admin-next/dashboard/src/components/common/json-view/json-view.tsx rename to packages/admin-next/dashboard/src/components/common/json-view-section/json-view-section.tsx index ee8055b46d..7727b9f663 100644 --- a/packages/admin-next/dashboard/src/components/common/json-view/json-view.tsx +++ b/packages/admin-next/dashboard/src/components/common/json-view-section/json-view-section.tsx @@ -21,23 +21,27 @@ type JsonViewProps = { } // 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 return ( - +
JSON {numberOfKeys} keys
- + -
+
JSON {numberOfKeys} keys @@ -45,7 +49,11 @@ export const JsonView = ({ data, root }: JsonViewProps) => {
esc - + diff --git a/packages/admin-next/dashboard/src/components/common/json-view/index.ts b/packages/admin-next/dashboard/src/components/common/json-view/index.ts deleted file mode 100644 index eed209d97d..0000000000 --- a/packages/admin-next/dashboard/src/components/common/json-view/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./json-view"; diff --git a/packages/admin-next/dashboard/src/components/common/order-table-cells/index.ts b/packages/admin-next/dashboard/src/components/common/order-table-cells/index.ts new file mode 100644 index 0000000000..20b29f6c85 --- /dev/null +++ b/packages/admin-next/dashboard/src/components/common/order-table-cells/index.ts @@ -0,0 +1 @@ +export * from "./order-table-cells" diff --git a/packages/admin-next/dashboard/src/components/common/order-table-cells/order-table-cells.tsx b/packages/admin-next/dashboard/src/components/common/order-table-cells/order-table-cells.tsx new file mode 100644 index 0000000000..9026e45590 --- /dev/null +++ b/packages/admin-next/dashboard/src/components/common/order-table-cells/order-table-cells.tsx @@ -0,0 +1,97 @@ +import type { Order } from "@medusajs/medusa" +import { StatusBadge } from "@medusajs/ui" +import { format } from "date-fns" +import { getPresentationalAmount } from "../../../lib/money-amount-helpers" + +export const OrderDisplayIdCell = ({ id }: { id: Order["display_id"] }) => { + return #{id} +} + +export const OrderDateCell = ({ + date, +}: { + date: Order["created_at"] | string +}) => { + const value = new Date(date) + + return {format(value, "dd MMM, yyyy")} +} + +export const OrderFulfillmentStatusCell = ({ + status, +}: { + status: Order["fulfillment_status"] +}) => { + switch (status) { + case "not_fulfilled": + return Not fulfilled + case "partially_fulfilled": + return Partially fulfilled + case "fulfilled": + return Fulfilled + case "partially_shipped": + return Partially shipped + case "shipped": + return Shipped + case "partially_returned": + return Partially returned + case "returned": + return Returned + case "canceled": + return Canceled + case "requires_action": + return Requires action + } +} + +export const OrderPaymentStatusCell = ({ + status, +}: { + status: Order["payment_status"] +}) => { + switch (status) { + case "not_paid": + return Not paid + case "awaiting": + return Awaiting + case "captured": + return Captured + case "partially_refunded": + return Partially refunded + case "refunded": + return Refunded + case "canceled": + return Canceled + case "requires_action": + return Requires action + } +} + +// TODO: Fix formatting amount with correct division eg. EUR 1000 -> EUR 10.00 +// Source currency info from `@medusajs/medusa` definition +export const OrderTotalCell = ({ + total, + currencyCode, +}: { + total: Order["total"] + currencyCode: Order["currency_code"] +}) => { + const formatted = new Intl.NumberFormat(undefined, { + style: "currency", + currency: currencyCode, + currencyDisplay: "narrowSymbol", + }).format(0) + + const symbol = formatted.replace(/\d/g, "").replace(/[.,]/g, "").trim() + + const presentationAmount = getPresentationalAmount(total, currencyCode) + const formattedTotal = new Intl.NumberFormat(undefined, { + style: "decimal", + }).format(presentationAmount) + + return ( + + {symbol} {formattedTotal} {currencyCode.toUpperCase()} + + ) +} diff --git a/packages/admin-next/dashboard/src/components/common/product-table-cells/product-table-cells.tsx b/packages/admin-next/dashboard/src/components/common/product-table-cells/product-table-cells.tsx index 63ca516c8e..cd290cf662 100644 --- a/packages/admin-next/dashboard/src/components/common/product-table-cells/product-table-cells.tsx +++ b/packages/admin-next/dashboard/src/components/common/product-table-cells/product-table-cells.tsx @@ -8,7 +8,7 @@ import { StatusBadge, Text } from "@medusajs/ui" import { useTranslation } from "react-i18next" import { Thumbnail } from "../thumbnail" -export const ProductInventoryCell = ({ +export const ProductVariantCell = ({ variants, }: { variants: ProductVariant[] | null @@ -23,13 +23,10 @@ export const ProductInventoryCell = ({ ) } - const inventory = variants.reduce((acc, v) => acc + v.inventory_quantity, 0) - return ( - {t("products.inStockVariants", { + {t("products.variantCount", { count: variants.length, - inventory: inventory, })} ) diff --git a/packages/admin-next/dashboard/src/components/common/skeleton/index.ts b/packages/admin-next/dashboard/src/components/common/skeleton/index.ts new file mode 100644 index 0000000000..d889ad708e --- /dev/null +++ b/packages/admin-next/dashboard/src/components/common/skeleton/index.ts @@ -0,0 +1 @@ +export * from "./skeleton" diff --git a/packages/admin-next/dashboard/src/components/common/skeleton/skeleton.tsx b/packages/admin-next/dashboard/src/components/common/skeleton/skeleton.tsx new file mode 100644 index 0000000000..0ad49b6f96 --- /dev/null +++ b/packages/admin-next/dashboard/src/components/common/skeleton/skeleton.tsx @@ -0,0 +1,16 @@ +import { clx } from "@medusajs/ui" + +type SkeletonProps = { + className?: string +} + +export const Skeleton = ({ className }: SkeletonProps) => { + return ( +
+ ) +} diff --git a/packages/admin-next/dashboard/src/components/common/table-row-actions/index.ts b/packages/admin-next/dashboard/src/components/common/table-row-actions/index.ts new file mode 100644 index 0000000000..155d370c4a --- /dev/null +++ b/packages/admin-next/dashboard/src/components/common/table-row-actions/index.ts @@ -0,0 +1 @@ +export * from "./table-row-actions" diff --git a/packages/admin-next/dashboard/src/components/common/table-row-actions/table-row-actions.tsx b/packages/admin-next/dashboard/src/components/common/table-row-actions/table-row-actions.tsx new file mode 100644 index 0000000000..99e5d6adfa --- /dev/null +++ b/packages/admin-next/dashboard/src/components/common/table-row-actions/table-row-actions.tsx @@ -0,0 +1,84 @@ +import { EllipsisHorizontal } from "@medusajs/icons" +import { DropdownMenu, IconButton } from "@medusajs/ui" +import { ReactNode } from "react" +import { Link } from "react-router-dom" + +type TableRowAction = { + icon: ReactNode + label: string +} & ( + | { + to: string + onClick?: never + } + | { + onClick: () => void + to?: never + } +) + +type TableRowActionGroup = { + actions: TableRowAction[] +} + +type TableRowActionsProps = { + groups: TableRowActionGroup[] +} + +export const TableRowActions = ({ groups }: TableRowActionsProps) => { + return ( + + + + + + + + {groups.map((group, index) => { + if (!group.actions.length) { + return null + } + + const isLast = index === groups.length - 1 + + return ( +
+ {group.actions.map((action, index) => { + if (action.onClick) { + return ( + { + e.stopPropagation() + action.onClick() + }} + className="[&_svg]:text-ui-fg-subtle flex items-center gap-x-2" + > + {action.icon} + {action.label} + + ) + } + + return ( + + { + e.stopPropagation() + }} + className="[&_svg]:text-ui-fg-subtle flex items-center gap-x-2" + > + {action.icon} + {action.label} + + + ) + })} + {!isLast && } +
+ ) + })} +
+
+ ) +} diff --git a/packages/admin-next/dashboard/src/components/error/error-boundary/error-boundary.tsx b/packages/admin-next/dashboard/src/components/error/error-boundary/error-boundary.tsx index 6bc6d1719c..baf03c2873 100644 --- a/packages/admin-next/dashboard/src/components/error/error-boundary/error-boundary.tsx +++ b/packages/admin-next/dashboard/src/components/error/error-boundary/error-boundary.tsx @@ -1,5 +1,5 @@ -import type { AxiosError } from "axios" import { Navigate, useLocation, useRouteError } from "react-router-dom" +import { isAxiosError } from "../../../lib/is-axios-error" export const ErrorBoundary = () => { const error = useRouteError() @@ -20,7 +20,3 @@ export const ErrorBoundary = () => { // TODO: Actual catch-all error page return
Dang!
} - -const isAxiosError = (error: any): error is AxiosError => { - return error.isAxiosError -} diff --git a/packages/admin-next/dashboard/src/components/filtering/filter-group/filter-group.tsx b/packages/admin-next/dashboard/src/components/filtering/filter-group/filter-group.tsx new file mode 100644 index 0000000000..a9c161ba17 --- /dev/null +++ b/packages/admin-next/dashboard/src/components/filtering/filter-group/filter-group.tsx @@ -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 ( +
+ {hasMore && } + {isClearable && ( + + )} +
+ ) +} + +type AddFilterMenuProps = { + availableKeys: string[] +} + +const AddFilterMenu = ({ availableKeys }: AddFilterMenuProps) => { + return ( + + + + + + {availableKeys.map((key) => ( + {key} + ))} + + + ) +} diff --git a/packages/admin-next/dashboard/src/components/filtering/filter-group/index.ts b/packages/admin-next/dashboard/src/components/filtering/filter-group/index.ts new file mode 100644 index 0000000000..03b216813f --- /dev/null +++ b/packages/admin-next/dashboard/src/components/filtering/filter-group/index.ts @@ -0,0 +1 @@ +export * from "./filter-group" diff --git a/packages/admin-next/dashboard/src/components/filtering/order-by/index.ts b/packages/admin-next/dashboard/src/components/filtering/order-by/index.ts new file mode 100644 index 0000000000..d8200bb491 --- /dev/null +++ b/packages/admin-next/dashboard/src/components/filtering/order-by/index.ts @@ -0,0 +1 @@ +export * from "./order-by" diff --git a/packages/admin-next/dashboard/src/components/filtering/order-by/order-by.tsx b/packages/admin-next/dashboard/src/components/filtering/order-by/order-by.tsx new file mode 100644 index 0000000000..e2dcb2793c --- /dev/null +++ b/packages/admin-next/dashboard/src/components/filtering/order-by/order-by.tsx @@ -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 ( + + + + + + + + + {keys.map((key) => ( + event.preventDefault()} + > + {formatKey(key)} + + ))} + + + + event.preventDefault()} + > + {t("general.ascending")} + 1 - 30 + + event.preventDefault()} + > + {t("general.descending")} + 30 - 1 + + + + + ) +} diff --git a/packages/admin-next/dashboard/src/components/filtering/query/index.ts b/packages/admin-next/dashboard/src/components/filtering/query/index.ts new file mode 100644 index 0000000000..4ddfa841d3 --- /dev/null +++ b/packages/admin-next/dashboard/src/components/filtering/query/index.ts @@ -0,0 +1 @@ +export * from "./query" diff --git a/packages/admin-next/dashboard/src/components/filtering/query/query.tsx b/packages/admin-next/dashboard/src/components/filtering/query/query.tsx new file mode 100644 index 0000000000..eb5bef4907 --- /dev/null +++ b/packages/admin-next/dashboard/src/components/filtering/query/query.tsx @@ -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) => { + setInputValue(event.target.value) + } + + return ( + + ) +} diff --git a/packages/admin-next/dashboard/src/components/layout/app-layout/app-layout.tsx b/packages/admin-next/dashboard/src/components/layout/app-layout/app-layout.tsx deleted file mode 100644 index 0946a6a9c3..0000000000 --- a/packages/admin-next/dashboard/src/components/layout/app-layout/app-layout.tsx +++ /dev/null @@ -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 ( -
- -
- {isSettings && } -
- - - - -
-
-
- ) -} diff --git a/packages/admin-next/dashboard/src/components/layout/app-layout/breadcrumbs.tsx b/packages/admin-next/dashboard/src/components/layout/app-layout/breadcrumbs.tsx deleted file mode 100644 index 3ccb7ce18e..0000000000 --- a/packages/admin-next/dashboard/src/components/layout/app-layout/breadcrumbs.tsx +++ /dev/null @@ -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 ( -
    - {crumbs.map((crumb, index) => { - const isLast = index === crumbs.length - 1; - - return ( -
  1. - {!isLast ? ( - {crumb.label} - ) : ( - {crumb.label} - )} - {!isLast && } -
  2. - ); - })} -
- ); -}; diff --git a/packages/admin-next/dashboard/src/components/layout/app-layout/gutter.tsx b/packages/admin-next/dashboard/src/components/layout/app-layout/gutter.tsx deleted file mode 100644 index 00b75f447b..0000000000 --- a/packages/admin-next/dashboard/src/components/layout/app-layout/gutter.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { PropsWithChildren } from "react"; - -export const Gutter = ({ children }: PropsWithChildren) => { - return ( -
- {children} -
- ); -}; diff --git a/packages/admin-next/dashboard/src/components/layout/app-layout/index.ts b/packages/admin-next/dashboard/src/components/layout/app-layout/index.ts deleted file mode 100644 index 07c56e200e..0000000000 --- a/packages/admin-next/dashboard/src/components/layout/app-layout/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./app-layout"; diff --git a/packages/admin-next/dashboard/src/components/layout/app-layout/loader.tsx b/packages/admin-next/dashboard/src/components/layout/app-layout/loader.tsx deleted file mode 100644 index 917b5b80aa..0000000000 --- a/packages/admin-next/dashboard/src/components/layout/app-layout/loader.tsx +++ /dev/null @@ -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)) - ); - }; -}; diff --git a/packages/admin-next/dashboard/src/components/layout/app-layout/main-nav.tsx b/packages/admin-next/dashboard/src/components/layout/app-layout/main-nav.tsx deleted file mode 100644 index 9332308ea7..0000000000 --- a/packages/admin-next/dashboard/src/components/layout/app-layout/main-nav.tsx +++ /dev/null @@ -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 ( - - - - - ) -} - -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 ( -
- -
- - - - - - -
- - - -
-
-
- -
- - -
-
- - - -
-
-
-
-
- - -
-
- ) -} - -const DesktopNav = () => { - return ( - - ) -} - -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 ( -
- - -
-
-
-
- {store.name[0].toUpperCase()} -
-
- - {store.name} - -
-
- -
-
-
- - - - Store Settings - - - - - - Documentation - - - - - - Changelog - - - - - - - Theme - - - - { - e.preventDefault() - setTheme("light") - }} - > - Light - - { - e.preventDefault() - setTheme("dark") - }} - > - Dark - - - - - - - - Logout - ⌥⇧Q - - -
-
- ) -} - -const useCoreRoutes = (): Omit[] => { - const { t } = useTranslation() - - return [ - { - icon: , - label: t("orders.domain"), - to: "/orders", - items: [ - { - label: t("draftOrders.domain"), - to: "/draft-orders", - }, - ], - }, - { - icon: , - 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: , - label: t("customers.domain"), - to: "/customers", - items: [ - { - label: t("customerGroups.domain"), - to: "/customer-groups", - }, - ], - }, - { - icon: , - label: t("discounts.domain"), - to: "/discounts", - }, - { - icon: , - label: t("pricing.domain"), - to: "/pricing", - }, - ] -} - -const CoreRouteSection = () => { - const coreRoutes = useCoreRoutes() - - return ( - - ) -} - -const ExtensionRouteSection = () => { - if (!extensions.links || extensions.links.length === 0) { - return null - } - - return ( -
- -
- -
- - - -
- -
- {extensions.links.map((link) => { - return ( - : } - type="extension" - /> - ) - })} -
-
-
-
-
- ) -} - -const SettingsSection = () => { - return ( -
- } label="Settings" to="/settings" /> -
- ) -} - -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 ( -
- - -
- {(user.first_name || user.last_name) && ( - {`${user.first_name && `${user.first_name} `}${ - user.last_name - }`} - )} - - {user.email} - -
- -
- ) -} diff --git a/packages/admin-next/dashboard/src/components/layout/app-layout/nav-item.tsx b/packages/admin-next/dashboard/src/components/layout/app-layout/nav-item.tsx deleted file mode 100644 index 23d6f9946e..0000000000 --- a/packages/admin-next/dashboard/src/components/layout/app-layout/nav-item.tsx +++ /dev/null @@ -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 ( -
- 0, - } - )} - > - - - {label} - - - {items && items.length > 0 && ( - - - - - {label} - - - - -
-
-
- - {label} - - - {items.map((item) => { - return ( - -
-
-
- - {item.label} - - - ); - })} - - - )} -
- ); -}; - -const Icon = ({ icon, type }: { icon?: React.ReactNode; type: ItemType }) => { - if (!icon) { - return null; - } - - return type === "extension" ? ( -
-
{icon}
-
- ) : ( - icon - ); -}; diff --git a/packages/admin-next/dashboard/src/components/layout/app-layout/search-toggle.tsx b/packages/admin-next/dashboard/src/components/layout/app-layout/search-toggle.tsx deleted file mode 100644 index 949cca5ce1..0000000000 --- a/packages/admin-next/dashboard/src/components/layout/app-layout/search-toggle.tsx +++ /dev/null @@ -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 ( - - - - ) -} diff --git a/packages/admin-next/dashboard/src/components/layout/app-layout/settings-nav.tsx b/packages/admin-next/dashboard/src/components/layout/app-layout/settings-nav.tsx deleted file mode 100644 index acb25a8aa9..0000000000 --- a/packages/admin-next/dashboard/src/components/layout/app-layout/settings-nav.tsx +++ /dev/null @@ -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 ( -
-
-
- - - {t("general.settings")} - -
-
- -
- -
- - - -
- - - -
- -
- - - -
- - - -
-
-
- ) -} diff --git a/packages/admin-next/dashboard/src/components/layout/app-layout/spacer.tsx b/packages/admin-next/dashboard/src/components/layout/app-layout/spacer.tsx deleted file mode 100644 index 4e00210d2f..0000000000 --- a/packages/admin-next/dashboard/src/components/layout/app-layout/spacer.tsx +++ /dev/null @@ -1,7 +0,0 @@ -export const Spacer = () => { - return ( -
-
-
- ); -}; diff --git a/packages/admin-next/dashboard/src/components/layout/app-layout/topbar.tsx b/packages/admin-next/dashboard/src/components/layout/app-layout/topbar.tsx deleted file mode 100644 index cd2bdc40db..0000000000 --- a/packages/admin-next/dashboard/src/components/layout/app-layout/topbar.tsx +++ /dev/null @@ -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 ( -
-
- - -
-
- - -
-
- ) -} diff --git a/packages/admin-next/dashboard/src/components/layout/main-layout/index.ts b/packages/admin-next/dashboard/src/components/layout/main-layout/index.ts new file mode 100644 index 0000000000..035d09e6d6 --- /dev/null +++ b/packages/admin-next/dashboard/src/components/layout/main-layout/index.ts @@ -0,0 +1 @@ +export * from "./main-layout" diff --git a/packages/admin-next/dashboard/src/components/layout/main-layout/main-layout.tsx b/packages/admin-next/dashboard/src/components/layout/main-layout/main-layout.tsx new file mode 100644 index 0000000000..0d76bd7dd4 --- /dev/null +++ b/packages/admin-next/dashboard/src/components/layout/main-layout/main-layout.tsx @@ -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 ( + + + + ) +} + +const MainSidebar = () => { + return ( + + ) +} + +const Header = () => { + const { store, isError, error } = useAdminStore() + + const name = store?.name + const fallback = store?.name?.slice(0, 1).toUpperCase() + + if (isError) { + throw error + } + + return ( +
+
+
+ {fallback ? ( + + ) : ( + + )} + {name ? ( + + {store.name} + + ) : ( + + )} +
+
+
+ ) +} + +const useCoreRoutes = (): Omit[] => { + const { t } = useTranslation() + + return [ + { + icon: , + label: t("orders.domain"), + to: "/orders", + items: [ + { + label: t("draftOrders.domain"), + to: "/draft-orders", + }, + ], + }, + { + icon: , + 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: , + label: t("customers.domain"), + to: "/customers", + items: [ + { + label: t("customerGroups.domain"), + to: "/customer-groups", + }, + ], + }, + { + icon: , + label: t("discounts.domain"), + to: "/discounts", + }, + { + icon: , + label: t("pricing.domain"), + to: "/pricing", + }, + ] +} + +const CoreRouteSection = () => { + const coreRoutes = useCoreRoutes() + + return ( + + ) +} + +const ExtensionRouteSection = () => { + if (!extensions.links || extensions.links.length === 0) { + return null + } + + return ( +
+
+
+
+
+ +
+ + + +
+ +
+ {extensions.links.map((link) => { + return ( + : } + type="extension" + /> + ) + })} +
+
+
+
+
+ ) +} diff --git a/packages/admin-next/dashboard/src/components/layout/nav-item/index.ts b/packages/admin-next/dashboard/src/components/layout/nav-item/index.ts new file mode 100644 index 0000000000..c0155cc9bb --- /dev/null +++ b/packages/admin-next/dashboard/src/components/layout/nav-item/index.ts @@ -0,0 +1 @@ +export * from "./nav-item" diff --git a/packages/admin-next/dashboard/src/components/layout/nav-item/nav-item.tsx b/packages/admin-next/dashboard/src/components/layout/nav-item/nav-item.tsx new file mode 100644 index 0000000000..55f7cc7204 --- /dev/null +++ b/packages/admin-next/dashboard/src/components/layout/nav-item/nav-item.tsx @@ -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 ( +
+ 0, + } + )} + > + + + {label} + + + {items && items.length > 0 && ( + + + + + {label} + + + +
+
+
+
+ + + {label} + + +
+
    + {items.map((item) => { + return ( +
  • +
    +
    +
    + + + {item.label} + + +
  • + ) + })} +
+ + + )} +
+ ) +} + +const Icon = ({ icon, type }: { icon?: React.ReactNode; type: ItemType }) => { + if (!icon) { + return null + } + + return type === "extension" ? ( +
+
{icon}
+
+ ) : ( + icon + ) +} diff --git a/packages/admin-next/dashboard/src/components/layout/notifications/index.ts b/packages/admin-next/dashboard/src/components/layout/notifications/index.ts new file mode 100644 index 0000000000..3509846360 --- /dev/null +++ b/packages/admin-next/dashboard/src/components/layout/notifications/index.ts @@ -0,0 +1 @@ +export * from "./notifications" diff --git a/packages/admin-next/dashboard/src/components/layout/app-layout/notifications.tsx b/packages/admin-next/dashboard/src/components/layout/notifications/notifications.tsx similarity index 53% rename from packages/admin-next/dashboard/src/components/layout/app-layout/notifications.tsx rename to packages/admin-next/dashboard/src/components/layout/notifications/notifications.tsx index 6af0a698a3..921147ec64 100644 --- a/packages/admin-next/dashboard/src/components/layout/app-layout/notifications.tsx +++ b/packages/admin-next/dashboard/src/components/layout/notifications/notifications.tsx @@ -1,28 +1,31 @@ -import { BellAlert } from "@medusajs/icons"; -import { Drawer, Heading, IconButton } from "@medusajs/ui"; -import { useEffect, useState } from "react"; +import { BellAlert } from "@medusajs/icons" +import { Drawer, Heading, IconButton } from "@medusajs/ui" +import { useEffect, useState } from "react" export const Notifications = () => { - const [open, setOpen] = useState(false); + const [open, setOpen] = useState(false) useEffect(() => { const onKeyDown = (e: KeyboardEvent) => { if (e.key === "n" && (e.metaKey || e.ctrlKey)) { - setOpen((prev) => !prev); + setOpen((prev) => !prev) } - }; + } - document.addEventListener("keydown", onKeyDown); + document.addEventListener("keydown", onKeyDown) return () => { - document.removeEventListener("keydown", onKeyDown); - }; - }, []); + document.removeEventListener("keydown", onKeyDown) + } + }, []) return ( - + @@ -33,5 +36,5 @@ export const Notifications = () => { Notifications will go here - ); -}; + ) +} diff --git a/packages/admin-next/dashboard/src/components/layout/settings-layout/index.ts b/packages/admin-next/dashboard/src/components/layout/settings-layout/index.ts new file mode 100644 index 0000000000..6c0bebb323 --- /dev/null +++ b/packages/admin-next/dashboard/src/components/layout/settings-layout/index.ts @@ -0,0 +1 @@ +export * from "./settings-layout" diff --git a/packages/admin-next/dashboard/src/components/layout/settings-layout/settings-layout.tsx b/packages/admin-next/dashboard/src/components/layout/settings-layout/settings-layout.tsx new file mode 100644 index 0000000000..25f181756c --- /dev/null +++ b/packages/admin-next/dashboard/src/components/layout/settings-layout/settings-layout.tsx @@ -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 ( + + + + ) +} + +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 ( + + ) +} diff --git a/packages/admin-next/dashboard/src/components/layout/shell/index.ts b/packages/admin-next/dashboard/src/components/layout/shell/index.ts new file mode 100644 index 0000000000..d1e0c03c89 --- /dev/null +++ b/packages/admin-next/dashboard/src/components/layout/shell/index.ts @@ -0,0 +1 @@ +export * from "./shell" diff --git a/packages/admin-next/dashboard/src/components/layout/shell/shell.tsx b/packages/admin-next/dashboard/src/components/layout/shell/shell.tsx new file mode 100644 index 0000000000..31044999a7 --- /dev/null +++ b/packages/admin-next/dashboard/src/components/layout/shell/shell.tsx @@ -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 ( +
+
+ {children} + {children} +
+
+ +
+ + + +
+
+
+ ) +} + +const Gutter = ({ children }: PropsWithChildren) => { + return ( +
+ {children} +
+ ) +} + +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 ( +
    + {crumbs.map((crumb, index) => { + const isLast = index === crumbs.length - 1 + + return ( +
  1. + {!isLast ? ( + + {crumb.label} + + ) : ( +
    + ... + + {crumb.label} + +
    + )} + {/* {!isLast && } */} + {!isLast && } +
  2. + ) + })} +
+ ) +} + +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 ( + + + + ) +} + +const ThemeToggle = () => { + const { theme, setTheme } = useTheme() + + return ( + + + + Theme + + + + { + e.preventDefault() + setTheme("light") + }} + > + Light + + { + e.preventDefault() + setTheme("dark") + }} + > + Dark + + + + + ) +} + +const Logout = () => { + const navigate = useNavigate() + const { mutateAsync: logoutMutation } = useAdminDeleteSession() + + const handleLayout = async () => { + await logoutMutation(undefined, { + onSuccess: () => { + navigate("/login") + }, + }) + } + + return ( + +
+ + Logout +
+
+ ) +} + +const Profile = () => { + const location = useLocation() + + return ( + + + + Profile + + + ) +} + +const LoggedInUser = () => { + return ( + + + + + + + + + Documentation + + + + + + Changelog + + + + + + + + + ) +} + +const SettingsLink = () => { + const location = useLocation() + + return ( + + + + + + ) +} + +const ToggleNotifications = () => { + return ( + + + + ) +} + +const Searchbar = () => { + const { toggleSearch } = useSearch() + + return ( + + ) +} + +const ToggleSidebar = () => { + const { toggle } = useSidebar() + + return ( +
+ toggle("desktop")} + > + + + toggle("mobile")} + > + + +
+ ) +} + +const Topbar = () => { + return ( +
+
+ + +
+
+ +
+
+
+ + +
+ +
+
+ ) +} + +const DesktopSidebarContainer = ({ children }: PropsWithChildren) => { + const { desktop } = useSidebar() + + return ( +
+ {children} +
+ ) +} + +const MobileSidebarContainer = ({ children }: PropsWithChildren) => { + const { mobile, toggle } = useSidebar() + + return ( + toggle("mobile")}> + + + + {children} + + + + ) +} diff --git a/packages/admin-next/dashboard/src/components/localization/localized-date-picker/index.ts b/packages/admin-next/dashboard/src/components/localization/localized-date-picker/index.ts new file mode 100644 index 0000000000..3b64d8f60f --- /dev/null +++ b/packages/admin-next/dashboard/src/components/localization/localized-date-picker/index.ts @@ -0,0 +1 @@ +export * from "./localized-date-picker" diff --git a/packages/admin-next/dashboard/src/components/localization/localized-date-picker/localized-date-picker.tsx b/packages/admin-next/dashboard/src/components/localization/localized-date-picker/localized-date-picker.tsx new file mode 100644 index 0000000000..8517f15817 --- /dev/null +++ b/packages/admin-next/dashboard/src/components/localization/localized-date-picker/localized-date-picker.tsx @@ -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, + "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 ( + + ) +} diff --git a/packages/admin-next/dashboard/src/components/localization/localized-table-pagination/index.ts b/packages/admin-next/dashboard/src/components/localization/localized-table-pagination/index.ts new file mode 100644 index 0000000000..40d1ccdfac --- /dev/null +++ b/packages/admin-next/dashboard/src/components/localization/localized-table-pagination/index.ts @@ -0,0 +1 @@ +export * from "./localized-table-pagination" diff --git a/packages/admin-next/dashboard/src/components/localization/localized-table-pagination/localized-table-pagination.tsx b/packages/admin-next/dashboard/src/components/localization/localized-table-pagination/localized-table-pagination.tsx new file mode 100644 index 0000000000..8ec8a46955 --- /dev/null +++ b/packages/admin-next/dashboard/src/components/localization/localized-table-pagination/localized-table-pagination.tsx @@ -0,0 +1,26 @@ +import { Table } from "@medusajs/ui" +import { ComponentPropsWithoutRef, ElementRef, forwardRef } from "react" +import { useTranslation } from "react-i18next" + +type LocalizedTablePaginationProps = Omit< + ComponentPropsWithoutRef, + "translations" +> + +export const LocalizedTablePagination = forwardRef< + ElementRef, + 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 +}) +LocalizedTablePagination.displayName = "LocalizedTablePagination" diff --git a/packages/admin-next/dashboard/src/components/search/search.tsx b/packages/admin-next/dashboard/src/components/search/search.tsx index 5bee68d10a..d7fcfad992 100644 --- a/packages/admin-next/dashboard/src/components/search/search.tsx +++ b/packages/admin-next/dashboard/src/components/search/search.tsx @@ -1,5 +1,5 @@ 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 { Command } from "cmdk" import { @@ -10,6 +10,7 @@ import { useMemo, } from "react" import { useTranslation } from "react-i18next" + import { useSearch } from "../../providers/search-provider" export const Search = () => { @@ -100,6 +101,18 @@ const useLinks = (): CommandGroupProps[] => { { 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) => { - + {children} -
+
+
+
+
+ + Navigation + +
+ + +
+
+
+ + Open Result + + +
+
+
@@ -149,7 +181,7 @@ const CommandInput = forwardRef< { + 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 +} diff --git a/packages/admin-next/dashboard/src/hooks/use-query-params.tsx b/packages/admin-next/dashboard/src/hooks/use-query-params.tsx new file mode 100644 index 0000000000..afb282362e --- /dev/null +++ b/packages/admin-next/dashboard/src/hooks/use-query-params.tsx @@ -0,0 +1,16 @@ +import { useSearchParams } from "react-router-dom" + +export function useQueryParams( + keys: T[] +): Record { + const [params] = useSearchParams() + + // Use a type assertion to initialize the result + const result = {} as Record + + keys.forEach((key) => { + result[key] = params.get(key) || undefined + }) + + return result +} diff --git a/packages/admin-next/dashboard/src/hooks/use-route-modal-state.tsx b/packages/admin-next/dashboard/src/hooks/use-route-modal-state.tsx new file mode 100644 index 0000000000..ce20816951 --- /dev/null +++ b/packages/admin-next/dashboard/src/hooks/use-route-modal-state.tsx @@ -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] +} diff --git a/packages/admin-next/dashboard/src/i18n/config.ts b/packages/admin-next/dashboard/src/i18n/config.ts index c346a4b8ac..ff128d9e5e 100644 --- a/packages/admin-next/dashboard/src/i18n/config.ts +++ b/packages/admin-next/dashboard/src/i18n/config.ts @@ -1,3 +1,4 @@ +import { enUS } from "date-fns/locale" import i18n from "i18next" import LanguageDetector from "i18next-browser-languagedetector" import Backend, { type HttpBackendOptions } from "i18next-http-backend" @@ -22,6 +23,7 @@ export const languages: Language[] = [ code: "en", display_name: "English", ltr: true, + date_locale: enUS, }, ] diff --git a/packages/admin-next/dashboard/src/i18n/types.ts b/packages/admin-next/dashboard/src/i18n/types.ts index 6dd753ee66..14e59028fd 100644 --- a/packages/admin-next/dashboard/src/i18n/types.ts +++ b/packages/admin-next/dashboard/src/i18n/types.ts @@ -1,3 +1,4 @@ +import type { Locale } from "date-fns" import en from "../../public/locales/en/translation.json" const resources = { @@ -10,4 +11,5 @@ export type Language = { code: string display_name: string ltr: boolean + date_locale: Locale } diff --git a/packages/admin-next/dashboard/src/lib/countries.ts b/packages/admin-next/dashboard/src/lib/countries.ts new file mode 100644 index 0000000000..8e4efca026 --- /dev/null +++ b/packages/admin-next/dashboard/src/lib/countries.ts @@ -0,0 +1,1755 @@ +/** This file is auto-generated. Do not modify it manually. */ +import type { Country } from "@medusajs/medusa" + +export const countries: Omit[] = [ + { + iso_2: "af", + iso_3: "afg", + num_code: 4, + name: "AFGHANISTAN", + display_name: "Afghanistan", + }, + { + iso_2: "al", + iso_3: "alb", + num_code: 8, + name: "ALBANIA", + display_name: "Albania", + }, + { + iso_2: "dz", + iso_3: "dza", + num_code: 12, + name: "ALGERIA", + display_name: "Algeria", + }, + { + iso_2: "as", + iso_3: "asm", + num_code: 16, + name: "AMERICAN SAMOA", + display_name: "American Samoa", + }, + { + iso_2: "ad", + iso_3: "and", + num_code: 20, + name: "ANDORRA", + display_name: "Andorra", + }, + { + iso_2: "ao", + iso_3: "ago", + num_code: 24, + name: "ANGOLA", + display_name: "Angola", + }, + { + iso_2: "ai", + iso_3: "aia", + num_code: 660, + name: "ANGUILLA", + display_name: "Anguilla", + }, + { + iso_2: "aq", + iso_3: "ata", + num_code: 10, + name: "ANTARCTICA", + display_name: "Antarctica", + }, + { + iso_2: "ag", + iso_3: "atg", + num_code: 28, + name: "ANTIGUA AND BARBUDA", + display_name: "Antigua and Barbuda", + }, + { + iso_2: "ar", + iso_3: "arg", + num_code: 32, + name: "ARGENTINA", + display_name: "Argentina", + }, + { + iso_2: "am", + iso_3: "arm", + num_code: 51, + name: "ARMENIA", + display_name: "Armenia", + }, + { + iso_2: "aw", + iso_3: "abw", + num_code: 533, + name: "ARUBA", + display_name: "Aruba", + }, + { + iso_2: "au", + iso_3: "aus", + num_code: 36, + name: "AUSTRALIA", + display_name: "Australia", + }, + { + iso_2: "at", + iso_3: "aut", + num_code: 40, + name: "AUSTRIA", + display_name: "Austria", + }, + { + iso_2: "az", + iso_3: "aze", + num_code: 31, + name: "AZERBAIJAN", + display_name: "Azerbaijan", + }, + { + iso_2: "bs", + iso_3: "bhs", + num_code: 44, + name: "BAHAMAS", + display_name: "Bahamas", + }, + { + iso_2: "bh", + iso_3: "bhr", + num_code: 48, + name: "BAHRAIN", + display_name: "Bahrain", + }, + { + iso_2: "bd", + iso_3: "bgd", + num_code: 50, + name: "BANGLADESH", + display_name: "Bangladesh", + }, + { + iso_2: "bb", + iso_3: "brb", + num_code: 52, + name: "BARBADOS", + display_name: "Barbados", + }, + { + iso_2: "by", + iso_3: "blr", + num_code: 112, + name: "BELARUS", + display_name: "Belarus", + }, + { + iso_2: "be", + iso_3: "bel", + num_code: 56, + name: "BELGIUM", + display_name: "Belgium", + }, + { + iso_2: "bz", + iso_3: "blz", + num_code: 84, + name: "BELIZE", + display_name: "Belize", + }, + { + iso_2: "bj", + iso_3: "ben", + num_code: 204, + name: "BENIN", + display_name: "Benin", + }, + { + iso_2: "bm", + iso_3: "bmu", + num_code: 60, + name: "BERMUDA", + display_name: "Bermuda", + }, + { + iso_2: "bt", + iso_3: "btn", + num_code: 64, + name: "BHUTAN", + display_name: "Bhutan", + }, + { + iso_2: "bo", + iso_3: "bol", + num_code: 68, + name: "BOLIVIA", + display_name: "Bolivia", + }, + { + iso_2: "bq", + iso_3: "bes", + num_code: 535, + name: "BONAIRE, SINT EUSTATIUS AND SABA", + display_name: "Bonaire, Sint Eustatius and Saba", + }, + { + iso_2: "ba", + iso_3: "bih", + num_code: 70, + name: "BOSNIA AND HERZEGOVINA", + display_name: "Bosnia and Herzegovina", + }, + { + iso_2: "bw", + iso_3: "bwa", + num_code: 72, + name: "BOTSWANA", + display_name: "Botswana", + }, + { + iso_2: "bv", + iso_3: "bvd", + num_code: 74, + name: "BOUVET ISLAND", + display_name: "Bouvet Island", + }, + { + iso_2: "br", + iso_3: "bra", + num_code: 76, + name: "BRAZIL", + display_name: "Brazil", + }, + { + iso_2: "io", + iso_3: "iot", + num_code: 86, + name: "BRITISH INDIAN OCEAN TERRITORY", + display_name: "British Indian Ocean Territory", + }, + { + iso_2: "bn", + iso_3: "brn", + num_code: 96, + name: "BRUNEI DARUSSALAM", + display_name: "Brunei Darussalam", + }, + { + iso_2: "bg", + iso_3: "bgr", + num_code: 100, + name: "BULGARIA", + display_name: "Bulgaria", + }, + { + iso_2: "bf", + iso_3: "bfa", + num_code: 854, + name: "BURKINA FASO", + display_name: "Burkina Faso", + }, + { + iso_2: "bi", + iso_3: "bdi", + num_code: 108, + name: "BURUNDI", + display_name: "Burundi", + }, + { + iso_2: "kh", + iso_3: "khm", + num_code: 116, + name: "CAMBODIA", + display_name: "Cambodia", + }, + { + iso_2: "cm", + iso_3: "cmr", + num_code: 120, + name: "CAMEROON", + display_name: "Cameroon", + }, + { + iso_2: "ca", + iso_3: "can", + num_code: 124, + name: "CANADA", + display_name: "Canada", + }, + { + iso_2: "cv", + iso_3: "cpv", + num_code: 132, + name: "CAPE VERDE", + display_name: "Cape Verde", + }, + { + iso_2: "ky", + iso_3: "cym", + num_code: 136, + name: "CAYMAN ISLANDS", + display_name: "Cayman Islands", + }, + { + iso_2: "cf", + iso_3: "caf", + num_code: 140, + name: "CENTRAL AFRICAN REPUBLIC", + display_name: "Central African Republic", + }, + { + iso_2: "td", + iso_3: "tcd", + num_code: 148, + name: "CHAD", + display_name: "Chad", + }, + { + iso_2: "cl", + iso_3: "chl", + num_code: 152, + name: "CHILE", + display_name: "Chile", + }, + { + iso_2: "cn", + iso_3: "chn", + num_code: 156, + name: "CHINA", + display_name: "China", + }, + { + iso_2: "cx", + iso_3: "cxr", + num_code: 162, + name: "CHRISTMAS ISLAND", + display_name: "Christmas Island", + }, + { + iso_2: "cc", + iso_3: "cck", + num_code: 166, + name: "COCOS (KEELING) ISLANDS", + display_name: "Cocos (Keeling) Islands", + }, + { + iso_2: "co", + iso_3: "col", + num_code: 170, + name: "COLOMBIA", + display_name: "Colombia", + }, + { + iso_2: "km", + iso_3: "com", + num_code: 174, + name: "COMOROS", + display_name: "Comoros", + }, + { + iso_2: "cg", + iso_3: "cog", + num_code: 178, + name: "CONGO", + display_name: "Congo", + }, + { + iso_2: "cd", + iso_3: "cod", + num_code: 180, + name: "CONGO, THE DEMOCRATIC REPUBLIC OF THE", + display_name: "Congo, the Democratic Republic of the", + }, + { + iso_2: "ck", + iso_3: "cok", + num_code: 184, + name: "COOK ISLANDS", + display_name: "Cook Islands", + }, + { + iso_2: "cr", + iso_3: "cri", + num_code: 188, + name: "COSTA RICA", + display_name: "Costa Rica", + }, + { + iso_2: "ci", + iso_3: "civ", + num_code: 384, + name: "COTE D'IVOIRE", + display_name: "Cote D'Ivoire", + }, + { + iso_2: "hr", + iso_3: "hrv", + num_code: 191, + name: "CROATIA", + display_name: "Croatia", + }, + { + iso_2: "cu", + iso_3: "cub", + num_code: 192, + name: "CUBA", + display_name: "Cuba", + }, + { + iso_2: "cw", + iso_3: "cuw", + num_code: 531, + name: "CURAÇAO", + display_name: "Curaçao", + }, + { + iso_2: "cy", + iso_3: "cyp", + num_code: 196, + name: "CYPRUS", + display_name: "Cyprus", + }, + { + iso_2: "cz", + iso_3: "cze", + num_code: 203, + name: "CZECH REPUBLIC", + display_name: "Czech Republic", + }, + { + iso_2: "dk", + iso_3: "dnk", + num_code: 208, + name: "DENMARK", + display_name: "Denmark", + }, + { + iso_2: "dj", + iso_3: "dji", + num_code: 262, + name: "DJIBOUTI", + display_name: "Djibouti", + }, + { + iso_2: "dm", + iso_3: "dma", + num_code: 212, + name: "DOMINICA", + display_name: "Dominica", + }, + { + iso_2: "do", + iso_3: "dom", + num_code: 214, + name: "DOMINICAN REPUBLIC", + display_name: "Dominican Republic", + }, + { + iso_2: "ec", + iso_3: "ecu", + num_code: 218, + name: "ECUADOR", + display_name: "Ecuador", + }, + { + iso_2: "eg", + iso_3: "egy", + num_code: 818, + name: "EGYPT", + display_name: "Egypt", + }, + { + iso_2: "sv", + iso_3: "slv", + num_code: 222, + name: "EL SALVADOR", + display_name: "El Salvador", + }, + { + iso_2: "gq", + iso_3: "gnq", + num_code: 226, + name: "EQUATORIAL GUINEA", + display_name: "Equatorial Guinea", + }, + { + iso_2: "er", + iso_3: "eri", + num_code: 232, + name: "ERITREA", + display_name: "Eritrea", + }, + { + iso_2: "ee", + iso_3: "est", + num_code: 233, + name: "ESTONIA", + display_name: "Estonia", + }, + { + iso_2: "et", + iso_3: "eth", + num_code: 231, + name: "ETHIOPIA", + display_name: "Ethiopia", + }, + { + iso_2: "fk", + iso_3: "flk", + num_code: 238, + name: "FALKLAND ISLANDS (MALVINAS)", + display_name: "Falkland Islands (Malvinas)", + }, + { + iso_2: "fo", + iso_3: "fro", + num_code: 234, + name: "FAROE ISLANDS", + display_name: "Faroe Islands", + }, + { + iso_2: "fj", + iso_3: "fji", + num_code: 242, + name: "FIJI", + display_name: "Fiji", + }, + { + iso_2: "fi", + iso_3: "fin", + num_code: 246, + name: "FINLAND", + display_name: "Finland", + }, + { + iso_2: "fr", + iso_3: "fra", + num_code: 250, + name: "FRANCE", + display_name: "France", + }, + { + iso_2: "gf", + iso_3: "guf", + num_code: 254, + name: "FRENCH GUIANA", + display_name: "French Guiana", + }, + { + iso_2: "pf", + iso_3: "pyf", + num_code: 258, + name: "FRENCH POLYNESIA", + display_name: "French Polynesia", + }, + { + iso_2: "tf", + iso_3: "atf", + num_code: 260, + name: "FRENCH SOUTHERN TERRITORIES", + display_name: "French Southern Territories", + }, + { + iso_2: "ga", + iso_3: "gab", + num_code: 266, + name: "GABON", + display_name: "Gabon", + }, + { + iso_2: "gm", + iso_3: "gmb", + num_code: 270, + name: "GAMBIA", + display_name: "Gambia", + }, + { + iso_2: "ge", + iso_3: "geo", + num_code: 268, + name: "GEORGIA", + display_name: "Georgia", + }, + { + iso_2: "de", + iso_3: "deu", + num_code: 276, + name: "GERMANY", + display_name: "Germany", + }, + { + iso_2: "gh", + iso_3: "gha", + num_code: 288, + name: "GHANA", + display_name: "Ghana", + }, + { + iso_2: "gi", + iso_3: "gib", + num_code: 292, + name: "GIBRALTAR", + display_name: "Gibraltar", + }, + { + iso_2: "gr", + iso_3: "grc", + num_code: 300, + name: "GREECE", + display_name: "Greece", + }, + { + iso_2: "gl", + iso_3: "grl", + num_code: 304, + name: "GREENLAND", + display_name: "Greenland", + }, + { + iso_2: "gd", + iso_3: "grd", + num_code: 308, + name: "GRENADA", + display_name: "Grenada", + }, + { + iso_2: "gp", + iso_3: "glp", + num_code: 312, + name: "GUADELOUPE", + display_name: "Guadeloupe", + }, + { + iso_2: "gu", + iso_3: "gum", + num_code: 316, + name: "GUAM", + display_name: "Guam", + }, + { + iso_2: "gt", + iso_3: "gtm", + num_code: 320, + name: "GUATEMALA", + display_name: "Guatemala", + }, + { + iso_2: "gg", + iso_3: "ggy", + num_code: 831, + name: "GUERNSEY", + display_name: "Guernsey", + }, + { + iso_2: "gn", + iso_3: "gin", + num_code: 324, + name: "GUINEA", + display_name: "Guinea", + }, + { + iso_2: "gw", + iso_3: "gnb", + num_code: 624, + name: "GUINEA-BISSAU", + display_name: "Guinea-Bissau", + }, + { + iso_2: "gy", + iso_3: "guy", + num_code: 328, + name: "GUYANA", + display_name: "Guyana", + }, + { + iso_2: "ht", + iso_3: "hti", + num_code: 332, + name: "HAITI", + display_name: "Haiti", + }, + { + iso_2: "hm", + iso_3: "hmd", + num_code: 334, + name: "HEARD ISLAND AND MCDONALD ISLANDS", + display_name: "Heard Island And Mcdonald Islands", + }, + { + iso_2: "va", + iso_3: "vat", + num_code: 336, + name: "HOLY SEE (VATICAN CITY STATE)", + display_name: "Holy See (Vatican City State)", + }, + { + iso_2: "hn", + iso_3: "hnd", + num_code: 340, + name: "HONDURAS", + display_name: "Honduras", + }, + { + iso_2: "hk", + iso_3: "hkg", + num_code: 344, + name: "HONG KONG", + display_name: "Hong Kong", + }, + { + iso_2: "hu", + iso_3: "hun", + num_code: 348, + name: "HUNGARY", + display_name: "Hungary", + }, + { + iso_2: "is", + iso_3: "isl", + num_code: 352, + name: "ICELAND", + display_name: "Iceland", + }, + { + iso_2: "in", + iso_3: "ind", + num_code: 356, + name: "INDIA", + display_name: "India", + }, + { + iso_2: "id", + iso_3: "idn", + num_code: 360, + name: "INDONESIA", + display_name: "Indonesia", + }, + { + iso_2: "ir", + iso_3: "irn", + num_code: 364, + name: "IRAN, ISLAMIC REPUBLIC OF", + display_name: "Iran, Islamic Republic of", + }, + { + iso_2: "iq", + iso_3: "irq", + num_code: 368, + name: "IRAQ", + display_name: "Iraq", + }, + { + iso_2: "ie", + iso_3: "irl", + num_code: 372, + name: "IRELAND", + display_name: "Ireland", + }, + { + iso_2: "im", + iso_3: "imn", + num_code: 833, + name: "ISLE OF MAN", + display_name: "Isle Of Man", + }, + { + iso_2: "il", + iso_3: "isr", + num_code: 376, + name: "ISRAEL", + display_name: "Israel", + }, + { + iso_2: "it", + iso_3: "ita", + num_code: 380, + name: "ITALY", + display_name: "Italy", + }, + { + iso_2: "jm", + iso_3: "jam", + num_code: 388, + name: "JAMAICA", + display_name: "Jamaica", + }, + { + iso_2: "jp", + iso_3: "jpn", + num_code: 392, + name: "JAPAN", + display_name: "Japan", + }, + { + iso_2: "je", + iso_3: "jey", + num_code: 832, + name: "JERSEY", + display_name: "Jersey", + }, + { + iso_2: "jo", + iso_3: "jor", + num_code: 400, + name: "JORDAN", + display_name: "Jordan", + }, + { + iso_2: "kz", + iso_3: "kaz", + num_code: 398, + name: "KAZAKHSTAN", + display_name: "Kazakhstan", + }, + { + iso_2: "ke", + iso_3: "ken", + num_code: 404, + name: "KENYA", + display_name: "Kenya", + }, + { + iso_2: "ki", + iso_3: "kir", + num_code: 296, + name: "KIRIBATI", + display_name: "Kiribati", + }, + { + iso_2: "kp", + iso_3: "prk", + num_code: 408, + name: "KOREA, DEMOCRATIC PEOPLE'S REPUBLIC OF", + display_name: "Korea, Democratic People's Republic of", + }, + { + iso_2: "kr", + iso_3: "kor", + num_code: 410, + name: "KOREA, REPUBLIC OF", + display_name: "Korea, Republic of", + }, + { + iso_2: "xk", + iso_3: "xkx", + num_code: 900, + name: "KOSOVO", + display_name: "Kosovo", + }, + { + iso_2: "kw", + iso_3: "kwt", + num_code: 414, + name: "KUWAIT", + display_name: "Kuwait", + }, + { + iso_2: "kg", + iso_3: "kgz", + num_code: 417, + name: "KYRGYZSTAN", + display_name: "Kyrgyzstan", + }, + { + iso_2: "la", + iso_3: "lao", + num_code: 418, + name: "LAO PEOPLE'S DEMOCRATIC REPUBLIC", + display_name: "Lao People's Democratic Republic", + }, + { + iso_2: "lv", + iso_3: "lva", + num_code: 428, + name: "LATVIA", + display_name: "Latvia", + }, + { + iso_2: "lb", + iso_3: "lbn", + num_code: 422, + name: "LEBANON", + display_name: "Lebanon", + }, + { + iso_2: "ls", + iso_3: "lso", + num_code: 426, + name: "LESOTHO", + display_name: "Lesotho", + }, + { + iso_2: "lr", + iso_3: "lbr", + num_code: 430, + name: "LIBERIA", + display_name: "Liberia", + }, + { + iso_2: "ly", + iso_3: "lby", + num_code: 434, + name: "LIBYAN ARAB JAMAHIRIYA", + display_name: "Libyan Arab Jamahiriya", + }, + { + iso_2: "li", + iso_3: "lie", + num_code: 438, + name: "LIECHTENSTEIN", + display_name: "Liechtenstein", + }, + { + iso_2: "lt", + iso_3: "ltu", + num_code: 440, + name: "LITHUANIA", + display_name: "Lithuania", + }, + { + iso_2: "lu", + iso_3: "lux", + num_code: 442, + name: "LUXEMBOURG", + display_name: "Luxembourg", + }, + { + iso_2: "mo", + iso_3: "mac", + num_code: 446, + name: "MACAO", + display_name: "Macao", + }, + { + iso_2: "mk", + iso_3: "mkd", + num_code: 807, + name: "MACEDONIA, THE FORMER YUGOSLAV REPUBLIC OF", + display_name: "Macedonia, the Former Yugoslav Republic of", + }, + { + iso_2: "mg", + iso_3: "mdg", + num_code: 450, + name: "MADAGASCAR", + display_name: "Madagascar", + }, + { + iso_2: "mw", + iso_3: "mwi", + num_code: 454, + name: "MALAWI", + display_name: "Malawi", + }, + { + iso_2: "my", + iso_3: "mys", + num_code: 458, + name: "MALAYSIA", + display_name: "Malaysia", + }, + { + iso_2: "mv", + iso_3: "mdv", + num_code: 462, + name: "MALDIVES", + display_name: "Maldives", + }, + { + iso_2: "ml", + iso_3: "mli", + num_code: 466, + name: "MALI", + display_name: "Mali", + }, + { + iso_2: "mt", + iso_3: "mlt", + num_code: 470, + name: "MALTA", + display_name: "Malta", + }, + { + iso_2: "mh", + iso_3: "mhl", + num_code: 584, + name: "MARSHALL ISLANDS", + display_name: "Marshall Islands", + }, + { + iso_2: "mq", + iso_3: "mtq", + num_code: 474, + name: "MARTINIQUE", + display_name: "Martinique", + }, + { + iso_2: "mr", + iso_3: "mrt", + num_code: 478, + name: "MAURITANIA", + display_name: "Mauritania", + }, + { + iso_2: "mu", + iso_3: "mus", + num_code: 480, + name: "MAURITIUS", + display_name: "Mauritius", + }, + { + iso_2: "yt", + iso_3: "myt", + num_code: 175, + name: "MAYOTTE", + display_name: "Mayotte", + }, + { + iso_2: "mx", + iso_3: "mex", + num_code: 484, + name: "MEXICO", + display_name: "Mexico", + }, + { + iso_2: "fm", + iso_3: "fsm", + num_code: 583, + name: "MICRONESIA, FEDERATED STATES OF", + display_name: "Micronesia, Federated States of", + }, + { + iso_2: "md", + iso_3: "mda", + num_code: 498, + name: "MOLDOVA, REPUBLIC OF", + display_name: "Moldova, Republic of", + }, + { + iso_2: "mc", + iso_3: "mco", + num_code: 492, + name: "MONACO", + display_name: "Monaco", + }, + { + iso_2: "mn", + iso_3: "mng", + num_code: 496, + name: "MONGOLIA", + display_name: "Mongolia", + }, + { + iso_2: "me", + iso_3: "mne", + num_code: 499, + name: "MONTENEGRO", + display_name: "Montenegro", + }, + { + iso_2: "ms", + iso_3: "msr", + num_code: 500, + name: "MONTSERRAT", + display_name: "Montserrat", + }, + { + iso_2: "ma", + iso_3: "mar", + num_code: 504, + name: "MOROCCO", + display_name: "Morocco", + }, + { + iso_2: "mz", + iso_3: "moz", + num_code: 508, + name: "MOZAMBIQUE", + display_name: "Mozambique", + }, + { + iso_2: "mm", + iso_3: "mmr", + num_code: 104, + name: "MYANMAR", + display_name: "Myanmar", + }, + { + iso_2: "na", + iso_3: "nam", + num_code: 516, + name: "NAMIBIA", + display_name: "Namibia", + }, + { + iso_2: "nr", + iso_3: "nru", + num_code: 520, + name: "NAURU", + display_name: "Nauru", + }, + { + iso_2: "np", + iso_3: "npl", + num_code: 524, + name: "NEPAL", + display_name: "Nepal", + }, + { + iso_2: "nl", + iso_3: "nld", + num_code: 528, + name: "NETHERLANDS", + display_name: "Netherlands", + }, + { + iso_2: "nc", + iso_3: "ncl", + num_code: 540, + name: "NEW CALEDONIA", + display_name: "New Caledonia", + }, + { + iso_2: "nz", + iso_3: "nzl", + num_code: 554, + name: "NEW ZEALAND", + display_name: "New Zealand", + }, + { + iso_2: "ni", + iso_3: "nic", + num_code: 558, + name: "NICARAGUA", + display_name: "Nicaragua", + }, + { + iso_2: "ne", + iso_3: "ner", + num_code: 562, + name: "NIGER", + display_name: "Niger", + }, + { + iso_2: "ng", + iso_3: "nga", + num_code: 566, + name: "NIGERIA", + display_name: "Nigeria", + }, + { + iso_2: "nu", + iso_3: "niu", + num_code: 570, + name: "NIUE", + display_name: "Niue", + }, + { + iso_2: "nf", + iso_3: "nfk", + num_code: 574, + name: "NORFOLK ISLAND", + display_name: "Norfolk Island", + }, + { + iso_2: "mp", + iso_3: "mnp", + num_code: 580, + name: "NORTHERN MARIANA ISLANDS", + display_name: "Northern Mariana Islands", + }, + { + iso_2: "no", + iso_3: "nor", + num_code: 578, + name: "NORWAY", + display_name: "Norway", + }, + { + iso_2: "om", + iso_3: "omn", + num_code: 512, + name: "OMAN", + display_name: "Oman", + }, + { + iso_2: "pk", + iso_3: "pak", + num_code: 586, + name: "PAKISTAN", + display_name: "Pakistan", + }, + { + iso_2: "pw", + iso_3: "plw", + num_code: 585, + name: "PALAU", + display_name: "Palau", + }, + { + iso_2: "ps", + iso_3: "pse", + num_code: 275, + name: "PALESTINIAN TERRITORY, OCCUPIED", + display_name: "Palestinian Territory, Occupied", + }, + { + iso_2: "pa", + iso_3: "pan", + num_code: 591, + name: "PANAMA", + display_name: "Panama", + }, + { + iso_2: "pg", + iso_3: "png", + num_code: 598, + name: "PAPUA NEW GUINEA", + display_name: "Papua New Guinea", + }, + { + iso_2: "py", + iso_3: "pry", + num_code: 600, + name: "PARAGUAY", + display_name: "Paraguay", + }, + { + iso_2: "pe", + iso_3: "per", + num_code: 604, + name: "PERU", + display_name: "Peru", + }, + { + iso_2: "ph", + iso_3: "phl", + num_code: 608, + name: "PHILIPPINES", + display_name: "Philippines", + }, + { + iso_2: "pn", + iso_3: "pcn", + num_code: 612, + name: "PITCAIRN", + display_name: "Pitcairn", + }, + { + iso_2: "pl", + iso_3: "pol", + num_code: 616, + name: "POLAND", + display_name: "Poland", + }, + { + iso_2: "pt", + iso_3: "prt", + num_code: 620, + name: "PORTUGAL", + display_name: "Portugal", + }, + { + iso_2: "pr", + iso_3: "pri", + num_code: 630, + name: "PUERTO RICO", + display_name: "Puerto Rico", + }, + { + iso_2: "qa", + iso_3: "qat", + num_code: 634, + name: "QATAR", + display_name: "Qatar", + }, + { + iso_2: "re", + iso_3: "reu", + num_code: 638, + name: "REUNION", + display_name: "Reunion", + }, + { + iso_2: "ro", + iso_3: "rom", + num_code: 642, + name: "ROMANIA", + display_name: "Romania", + }, + { + iso_2: "ru", + iso_3: "rus", + num_code: 643, + name: "RUSSIAN FEDERATION", + display_name: "Russian Federation", + }, + { + iso_2: "rw", + iso_3: "rwa", + num_code: 646, + name: "RWANDA", + display_name: "Rwanda", + }, + { + iso_2: "bl", + iso_3: "blm", + num_code: 652, + name: "SAINT BARTHÉLEMY", + display_name: "Saint Barthélemy", + }, + { + iso_2: "sh", + iso_3: "shn", + num_code: 654, + name: "SAINT HELENA", + display_name: "Saint Helena", + }, + { + iso_2: "kn", + iso_3: "kna", + num_code: 659, + name: "SAINT KITTS AND NEVIS", + display_name: "Saint Kitts and Nevis", + }, + { + iso_2: "lc", + iso_3: "lca", + num_code: 662, + name: "SAINT LUCIA", + display_name: "Saint Lucia", + }, + { + iso_2: "mf", + iso_3: "maf", + num_code: 663, + name: "SAINT MARTIN (FRENCH PART)", + display_name: "Saint Martin (French part)", + }, + { + iso_2: "pm", + iso_3: "spm", + num_code: 666, + name: "SAINT PIERRE AND MIQUELON", + display_name: "Saint Pierre and Miquelon", + }, + { + iso_2: "vc", + iso_3: "vct", + num_code: 670, + name: "SAINT VINCENT AND THE GRENADINES", + display_name: "Saint Vincent and the Grenadines", + }, + { + iso_2: "ws", + iso_3: "wsm", + num_code: 882, + name: "SAMOA", + display_name: "Samoa", + }, + { + iso_2: "sm", + iso_3: "smr", + num_code: 674, + name: "SAN MARINO", + display_name: "San Marino", + }, + { + iso_2: "st", + iso_3: "stp", + num_code: 678, + name: "SAO TOME AND PRINCIPE", + display_name: "Sao Tome and Principe", + }, + { + iso_2: "sa", + iso_3: "sau", + num_code: 682, + name: "SAUDI ARABIA", + display_name: "Saudi Arabia", + }, + { + iso_2: "sn", + iso_3: "sen", + num_code: 686, + name: "SENEGAL", + display_name: "Senegal", + }, + { + iso_2: "rs", + iso_3: "srb", + num_code: 688, + name: "SERBIA", + display_name: "Serbia", + }, + { + iso_2: "sc", + iso_3: "syc", + num_code: 690, + name: "SEYCHELLES", + display_name: "Seychelles", + }, + { + iso_2: "sl", + iso_3: "sle", + num_code: 694, + name: "SIERRA LEONE", + display_name: "Sierra Leone", + }, + { + iso_2: "sg", + iso_3: "sgp", + num_code: 702, + name: "SINGAPORE", + display_name: "Singapore", + }, + { + iso_2: "sx", + iso_3: "sxm", + num_code: 534, + name: "SINT MAARTEN", + display_name: "Sint Maarten", + }, + { + iso_2: "sk", + iso_3: "svk", + num_code: 703, + name: "SLOVAKIA", + display_name: "Slovakia", + }, + { + iso_2: "si", + iso_3: "svn", + num_code: 705, + name: "SLOVENIA", + display_name: "Slovenia", + }, + { + iso_2: "sb", + iso_3: "slb", + num_code: 90, + name: "SOLOMON ISLANDS", + display_name: "Solomon Islands", + }, + { + iso_2: "so", + iso_3: "som", + num_code: 706, + name: "SOMALIA", + display_name: "Somalia", + }, + { + iso_2: "za", + iso_3: "zaf", + num_code: 710, + name: "SOUTH AFRICA", + display_name: "South Africa", + }, + { + iso_2: "gs", + iso_3: "sgs", + num_code: 239, + name: "SOUTH GEORGIA AND THE SOUTH SANDWICH ISLANDS", + display_name: "South Georgia and the South Sandwich Islands", + }, + { + iso_2: "ss", + iso_3: "ssd", + num_code: 728, + name: "SOUTH SUDAN", + display_name: "South Sudan", + }, + { + iso_2: "es", + iso_3: "esp", + num_code: 724, + name: "SPAIN", + display_name: "Spain", + }, + { + iso_2: "lk", + iso_3: "lka", + num_code: 144, + name: "SRI LANKA", + display_name: "Sri Lanka", + }, + { + iso_2: "sd", + iso_3: "sdn", + num_code: 729, + name: "SUDAN", + display_name: "Sudan", + }, + { + iso_2: "sr", + iso_3: "sur", + num_code: 740, + name: "SURINAME", + display_name: "Suriname", + }, + { + iso_2: "sj", + iso_3: "sjm", + num_code: 744, + name: "SVALBARD AND JAN MAYEN", + display_name: "Svalbard and Jan Mayen", + }, + { + iso_2: "sz", + iso_3: "swz", + num_code: 748, + name: "SWAZILAND", + display_name: "Swaziland", + }, + { + iso_2: "se", + iso_3: "swe", + num_code: 752, + name: "SWEDEN", + display_name: "Sweden", + }, + { + iso_2: "ch", + iso_3: "che", + num_code: 756, + name: "SWITZERLAND", + display_name: "Switzerland", + }, + { + iso_2: "sy", + iso_3: "syr", + num_code: 760, + name: "SYRIAN ARAB REPUBLIC", + display_name: "Syrian Arab Republic", + }, + { + iso_2: "tw", + iso_3: "twn", + num_code: 158, + name: "TAIWAN, PROVINCE OF CHINA", + display_name: "Taiwan, Province of China", + }, + { + iso_2: "tj", + iso_3: "tjk", + num_code: 762, + name: "TAJIKISTAN", + display_name: "Tajikistan", + }, + { + iso_2: "tz", + iso_3: "tza", + num_code: 834, + name: "TANZANIA, UNITED REPUBLIC OF", + display_name: "Tanzania, United Republic of", + }, + { + iso_2: "th", + iso_3: "tha", + num_code: 764, + name: "THAILAND", + display_name: "Thailand", + }, + { + iso_2: "tl", + iso_3: "tls", + num_code: 626, + name: "TIMOR LESTE", + display_name: "Timor Leste", + }, + { + iso_2: "tg", + iso_3: "tgo", + num_code: 768, + name: "TOGO", + display_name: "Togo", + }, + { + iso_2: "tk", + iso_3: "tkl", + num_code: 772, + name: "TOKELAU", + display_name: "Tokelau", + }, + { + iso_2: "to", + iso_3: "ton", + num_code: 776, + name: "TONGA", + display_name: "Tonga", + }, + { + iso_2: "tt", + iso_3: "tto", + num_code: 780, + name: "TRINIDAD AND TOBAGO", + display_name: "Trinidad and Tobago", + }, + { + iso_2: "tn", + iso_3: "tun", + num_code: 788, + name: "TUNISIA", + display_name: "Tunisia", + }, + { + iso_2: "tr", + iso_3: "tur", + num_code: 792, + name: "TURKEY", + display_name: "Turkey", + }, + { + iso_2: "tm", + iso_3: "tkm", + num_code: 795, + name: "TURKMENISTAN", + display_name: "Turkmenistan", + }, + { + iso_2: "tc", + iso_3: "tca", + num_code: 796, + name: "TURKS AND CAICOS ISLANDS", + display_name: "Turks and Caicos Islands", + }, + { + iso_2: "tv", + iso_3: "tuv", + num_code: 798, + name: "TUVALU", + display_name: "Tuvalu", + }, + { + iso_2: "ug", + iso_3: "uga", + num_code: 800, + name: "UGANDA", + display_name: "Uganda", + }, + { + iso_2: "ua", + iso_3: "ukr", + num_code: 804, + name: "UKRAINE", + display_name: "Ukraine", + }, + { + iso_2: "ae", + iso_3: "are", + num_code: 784, + name: "UNITED ARAB EMIRATES", + display_name: "United Arab Emirates", + }, + { + iso_2: "gb", + iso_3: "gbr", + num_code: 826, + name: "UNITED KINGDOM", + display_name: "United Kingdom", + }, + { + iso_2: "us", + iso_3: "usa", + num_code: 840, + name: "UNITED STATES", + display_name: "United States", + }, + { + iso_2: "um", + iso_3: "umi", + num_code: 581, + name: "UNITED STATES MINOR OUTLYING ISLANDS", + display_name: "United States Minor Outlying Islands", + }, + { + iso_2: "uy", + iso_3: "ury", + num_code: 858, + name: "URUGUAY", + display_name: "Uruguay", + }, + { + iso_2: "uz", + iso_3: "uzb", + num_code: 860, + name: "UZBEKISTAN", + display_name: "Uzbekistan", + }, + { + iso_2: "vu", + iso_3: "vut", + num_code: 548, + name: "VANUATU", + display_name: "Vanuatu", + }, + { + iso_2: "ve", + iso_3: "ven", + num_code: 862, + name: "VENEZUELA", + display_name: "Venezuela", + }, + { + iso_2: "vn", + iso_3: "vnm", + num_code: 704, + name: "VIET NAM", + display_name: "Viet Nam", + }, + { + iso_2: "vg", + iso_3: "vgb", + num_code: 92, + name: "VIRGIN ISLANDS, BRITISH", + display_name: "Virgin Islands, British", + }, + { + iso_2: "vi", + iso_3: "vir", + num_code: 850, + name: "VIRGIN ISLANDS, U.S.", + display_name: "Virgin Islands, U.S.", + }, + { + iso_2: "wf", + iso_3: "wlf", + num_code: 876, + name: "WALLIS AND FUTUNA", + display_name: "Wallis and Futuna", + }, + { + iso_2: "eh", + iso_3: "esh", + num_code: 732, + name: "WESTERN SAHARA", + display_name: "Western Sahara", + }, + { + iso_2: "ye", + iso_3: "yem", + num_code: 887, + name: "YEMEN", + display_name: "Yemen", + }, + { + iso_2: "zm", + iso_3: "zmb", + num_code: 894, + name: "ZAMBIA", + display_name: "Zambia", + }, + { + iso_2: "zw", + iso_3: "zwe", + num_code: 716, + name: "ZIMBABWE", + display_name: "Zimbabwe", + }, + { + iso_2: "ax", + iso_3: "ala", + num_code: 248, + name: "ÅLAND ISLANDS", + display_name: "Åland Islands", + }, +] diff --git a/packages/admin-next/dashboard/src/lib/currencies.ts b/packages/admin-next/dashboard/src/lib/currencies.ts new file mode 100644 index 0000000000..ae22a07fbb --- /dev/null +++ b/packages/admin-next/dashboard/src/lib/currencies.ts @@ -0,0 +1,730 @@ +/** This file is auto-generated. Do not modify it manually. */ +type CurrencyInfo = { + code: string + name: string + symbol_native: string + decimal_digits: number +} + +export const currencies: Record = { + USD: { + code: "USD", + name: "US Dollar", + symbol_native: "$", + decimal_digits: 2, + }, + CAD: { + code: "CAD", + name: "Canadian Dollar", + symbol_native: "$", + decimal_digits: 2, + }, + EUR: { + code: "EUR", + name: "Euro", + symbol_native: "€", + decimal_digits: 2, + }, + AED: { + code: "AED", + name: "United Arab Emirates Dirham", + symbol_native: "د.إ.‏", + decimal_digits: 2, + }, + AFN: { + code: "AFN", + name: "Afghan Afghani", + symbol_native: "؋", + decimal_digits: 0, + }, + ALL: { + code: "ALL", + name: "Albanian Lek", + symbol_native: "Lek", + decimal_digits: 0, + }, + AMD: { + code: "AMD", + name: "Armenian Dram", + symbol_native: "դր.", + decimal_digits: 0, + }, + ARS: { + code: "ARS", + name: "Argentine Peso", + symbol_native: "$", + decimal_digits: 2, + }, + AUD: { + code: "AUD", + name: "Australian Dollar", + symbol_native: "$", + decimal_digits: 2, + }, + AZN: { + code: "AZN", + name: "Azerbaijani Manat", + symbol_native: "ман.", + decimal_digits: 2, + }, + BAM: { + code: "BAM", + name: "Bosnia-Herzegovina Convertible Mark", + symbol_native: "KM", + decimal_digits: 2, + }, + BDT: { + code: "BDT", + name: "Bangladeshi Taka", + symbol_native: "৳", + decimal_digits: 2, + }, + BGN: { + code: "BGN", + name: "Bulgarian Lev", + symbol_native: "лв.", + decimal_digits: 2, + }, + BHD: { + code: "BHD", + name: "Bahraini Dinar", + symbol_native: "د.ب.‏", + decimal_digits: 3, + }, + BIF: { + code: "BIF", + name: "Burundian Franc", + symbol_native: "FBu", + decimal_digits: 0, + }, + BND: { + code: "BND", + name: "Brunei Dollar", + symbol_native: "$", + decimal_digits: 2, + }, + BOB: { + code: "BOB", + name: "Bolivian Boliviano", + symbol_native: "Bs", + decimal_digits: 2, + }, + BRL: { + code: "BRL", + name: "Brazilian Real", + symbol_native: "R$", + decimal_digits: 2, + }, + BWP: { + code: "BWP", + name: "Botswanan Pula", + symbol_native: "P", + decimal_digits: 2, + }, + BYN: { + code: "BYN", + name: "Belarusian Ruble", + symbol_native: "руб.", + decimal_digits: 2, + }, + BZD: { + code: "BZD", + name: "Belize Dollar", + symbol_native: "$", + decimal_digits: 2, + }, + CDF: { + code: "CDF", + name: "Congolese Franc", + symbol_native: "FrCD", + decimal_digits: 2, + }, + CHF: { + code: "CHF", + name: "Swiss Franc", + symbol_native: "CHF", + decimal_digits: 2, + }, + CLP: { + code: "CLP", + name: "Chilean Peso", + symbol_native: "$", + decimal_digits: 0, + }, + CNY: { + code: "CNY", + name: "Chinese Yuan", + symbol_native: "CN¥", + decimal_digits: 2, + }, + COP: { + code: "COP", + name: "Colombian Peso", + symbol_native: "$", + decimal_digits: 0, + }, + CRC: { + code: "CRC", + name: "Costa Rican Colón", + symbol_native: "₡", + decimal_digits: 0, + }, + CVE: { + code: "CVE", + name: "Cape Verdean Escudo", + symbol_native: "CV$", + decimal_digits: 2, + }, + CZK: { + code: "CZK", + name: "Czech Republic Koruna", + symbol_native: "Kč", + decimal_digits: 2, + }, + DJF: { + code: "DJF", + name: "Djiboutian Franc", + symbol_native: "Fdj", + decimal_digits: 0, + }, + DKK: { + code: "DKK", + name: "Danish Krone", + symbol_native: "kr", + decimal_digits: 2, + }, + DOP: { + code: "DOP", + name: "Dominican Peso", + symbol_native: "RD$", + decimal_digits: 2, + }, + DZD: { + code: "DZD", + name: "Algerian Dinar", + symbol_native: "د.ج.‏", + decimal_digits: 2, + }, + EEK: { + code: "EEK", + name: "Estonian Kroon", + symbol_native: "kr", + decimal_digits: 2, + }, + EGP: { + code: "EGP", + name: "Egyptian Pound", + symbol_native: "ج.م.‏", + decimal_digits: 2, + }, + ERN: { + code: "ERN", + name: "Eritrean Nakfa", + symbol_native: "Nfk", + decimal_digits: 2, + }, + ETB: { + code: "ETB", + name: "Ethiopian Birr", + symbol_native: "Br", + decimal_digits: 2, + }, + GBP: { + code: "GBP", + name: "British Pound Sterling", + symbol_native: "£", + decimal_digits: 2, + }, + GEL: { + code: "GEL", + name: "Georgian Lari", + symbol_native: "GEL", + decimal_digits: 2, + }, + GHS: { + code: "GHS", + name: "Ghanaian Cedi", + symbol_native: "GH₵", + decimal_digits: 2, + }, + GNF: { + code: "GNF", + name: "Guinean Franc", + symbol_native: "FG", + decimal_digits: 0, + }, + GTQ: { + code: "GTQ", + name: "Guatemalan Quetzal", + symbol_native: "Q", + decimal_digits: 2, + }, + HKD: { + code: "HKD", + name: "Hong Kong Dollar", + symbol_native: "$", + decimal_digits: 2, + }, + HNL: { + code: "HNL", + name: "Honduran Lempira", + symbol_native: "L", + decimal_digits: 2, + }, + HRK: { + code: "HRK", + name: "Croatian Kuna", + symbol_native: "kn", + decimal_digits: 2, + }, + HUF: { + code: "HUF", + name: "Hungarian Forint", + symbol_native: "Ft", + decimal_digits: 0, + }, + IDR: { + code: "IDR", + name: "Indonesian Rupiah", + symbol_native: "Rp", + decimal_digits: 0, + }, + ILS: { + code: "ILS", + name: "Israeli New Sheqel", + symbol_native: "₪", + decimal_digits: 2, + }, + INR: { + code: "INR", + name: "Indian Rupee", + symbol_native: "টকা", + decimal_digits: 2, + }, + IQD: { + code: "IQD", + name: "Iraqi Dinar", + symbol_native: "د.ع.‏", + decimal_digits: 0, + }, + IRR: { + code: "IRR", + name: "Iranian Rial", + symbol_native: "﷼", + decimal_digits: 0, + }, + ISK: { + code: "ISK", + name: "Icelandic Króna", + symbol_native: "kr", + decimal_digits: 0, + }, + JMD: { + code: "JMD", + name: "Jamaican Dollar", + symbol_native: "$", + decimal_digits: 2, + }, + JOD: { + code: "JOD", + name: "Jordanian Dinar", + symbol_native: "د.أ.‏", + decimal_digits: 3, + }, + JPY: { + code: "JPY", + name: "Japanese Yen", + symbol_native: "¥", + decimal_digits: 0, + }, + KES: { + code: "KES", + name: "Kenyan Shilling", + symbol_native: "Ksh", + decimal_digits: 2, + }, + KHR: { + code: "KHR", + name: "Cambodian Riel", + symbol_native: "៛", + decimal_digits: 2, + }, + KMF: { + code: "KMF", + name: "Comorian Franc", + symbol_native: "FC", + decimal_digits: 0, + }, + KRW: { + code: "KRW", + name: "South Korean Won", + symbol_native: "₩", + decimal_digits: 0, + }, + KWD: { + code: "KWD", + name: "Kuwaiti Dinar", + symbol_native: "د.ك.‏", + decimal_digits: 3, + }, + KZT: { + code: "KZT", + name: "Kazakhstani Tenge", + symbol_native: "тңг.", + decimal_digits: 2, + }, + LBP: { + code: "LBP", + name: "Lebanese Pound", + symbol_native: "ل.ل.‏", + decimal_digits: 0, + }, + LKR: { + code: "LKR", + name: "Sri Lankan Rupee", + symbol_native: "SL Re", + decimal_digits: 2, + }, + LTL: { + code: "LTL", + name: "Lithuanian Litas", + symbol_native: "Lt", + decimal_digits: 2, + }, + LVL: { + code: "LVL", + name: "Latvian Lats", + symbol_native: "Ls", + decimal_digits: 2, + }, + LYD: { + code: "LYD", + name: "Libyan Dinar", + symbol_native: "د.ل.‏", + decimal_digits: 3, + }, + MAD: { + code: "MAD", + name: "Moroccan Dirham", + symbol_native: "د.م.‏", + decimal_digits: 2, + }, + MDL: { + code: "MDL", + name: "Moldovan Leu", + symbol_native: "MDL", + decimal_digits: 2, + }, + MGA: { + code: "MGA", + name: "Malagasy Ariary", + symbol_native: "MGA", + decimal_digits: 0, + }, + MKD: { + code: "MKD", + name: "Macedonian Denar", + symbol_native: "MKD", + decimal_digits: 2, + }, + MMK: { + code: "MMK", + name: "Myanma Kyat", + symbol_native: "K", + decimal_digits: 0, + }, + MNT: { + code: "MNT", + name: "Mongolian Tugrig", + symbol_native: "₮", + decimal_digits: 0, + }, + MOP: { + code: "MOP", + name: "Macanese Pataca", + symbol_native: "MOP$", + decimal_digits: 2, + }, + MUR: { + code: "MUR", + name: "Mauritian Rupee", + symbol_native: "MURs", + decimal_digits: 0, + }, + MXN: { + code: "MXN", + name: "Mexican Peso", + symbol_native: "$", + decimal_digits: 2, + }, + MYR: { + code: "MYR", + name: "Malaysian Ringgit", + symbol_native: "RM", + decimal_digits: 2, + }, + MZN: { + code: "MZN", + name: "Mozambican Metical", + symbol_native: "MTn", + decimal_digits: 2, + }, + NAD: { + code: "NAD", + name: "Namibian Dollar", + symbol_native: "N$", + decimal_digits: 2, + }, + NGN: { + code: "NGN", + name: "Nigerian Naira", + symbol_native: "₦", + decimal_digits: 2, + }, + NIO: { + code: "NIO", + name: "Nicaraguan Córdoba", + symbol_native: "C$", + decimal_digits: 2, + }, + NOK: { + code: "NOK", + name: "Norwegian Krone", + symbol_native: "kr", + decimal_digits: 2, + }, + NPR: { + code: "NPR", + name: "Nepalese Rupee", + symbol_native: "नेरू", + decimal_digits: 2, + }, + NZD: { + code: "NZD", + name: "New Zealand Dollar", + symbol_native: "$", + decimal_digits: 2, + }, + OMR: { + code: "OMR", + name: "Omani Rial", + symbol_native: "ر.ع.‏", + decimal_digits: 3, + }, + PAB: { + code: "PAB", + name: "Panamanian Balboa", + symbol_native: "B/.", + decimal_digits: 2, + }, + PEN: { + code: "PEN", + name: "Peruvian Nuevo Sol", + symbol_native: "S/.", + decimal_digits: 2, + }, + PHP: { + code: "PHP", + name: "Philippine Peso", + symbol_native: "₱", + decimal_digits: 2, + }, + PKR: { + code: "PKR", + name: "Pakistani Rupee", + symbol_native: "₨", + decimal_digits: 0, + }, + PLN: { + code: "PLN", + name: "Polish Zloty", + symbol_native: "zł", + decimal_digits: 2, + }, + PYG: { + code: "PYG", + name: "Paraguayan Guarani", + symbol_native: "₲", + decimal_digits: 0, + }, + QAR: { + code: "QAR", + name: "Qatari Rial", + symbol_native: "ر.ق.‏", + decimal_digits: 2, + }, + RON: { + code: "RON", + name: "Romanian Leu", + symbol_native: "RON", + decimal_digits: 2, + }, + RSD: { + code: "RSD", + name: "Serbian Dinar", + symbol_native: "дин.", + decimal_digits: 0, + }, + RUB: { + code: "RUB", + name: "Russian Ruble", + symbol_native: "₽.", + decimal_digits: 2, + }, + RWF: { + code: "RWF", + name: "Rwandan Franc", + symbol_native: "FR", + decimal_digits: 0, + }, + SAR: { + code: "SAR", + name: "Saudi Riyal", + symbol_native: "ر.س.‏", + decimal_digits: 2, + }, + SDG: { + code: "SDG", + name: "Sudanese Pound", + symbol_native: "SDG", + decimal_digits: 2, + }, + SEK: { + code: "SEK", + name: "Swedish Krona", + symbol_native: "kr", + decimal_digits: 2, + }, + SGD: { + code: "SGD", + name: "Singapore Dollar", + symbol_native: "$", + decimal_digits: 2, + }, + SOS: { + code: "SOS", + name: "Somali Shilling", + symbol_native: "Ssh", + decimal_digits: 0, + }, + SYP: { + code: "SYP", + name: "Syrian Pound", + symbol_native: "ل.س.‏", + decimal_digits: 0, + }, + THB: { + code: "THB", + name: "Thai Baht", + symbol_native: "฿", + decimal_digits: 2, + }, + TND: { + code: "TND", + name: "Tunisian Dinar", + symbol_native: "د.ت.‏", + decimal_digits: 3, + }, + TOP: { + code: "TOP", + name: "Tongan Paʻanga", + symbol_native: "T$", + decimal_digits: 2, + }, + TRY: { + code: "TRY", + name: "Turkish Lira", + symbol_native: "TL", + decimal_digits: 2, + }, + TTD: { + code: "TTD", + name: "Trinidad and Tobago Dollar", + symbol_native: "$", + decimal_digits: 2, + }, + TWD: { + code: "TWD", + name: "New Taiwan Dollar", + symbol_native: "NT$", + decimal_digits: 2, + }, + TZS: { + code: "TZS", + name: "Tanzanian Shilling", + symbol_native: "TSh", + decimal_digits: 0, + }, + UAH: { + code: "UAH", + name: "Ukrainian Hryvnia", + symbol_native: "₴", + decimal_digits: 2, + }, + UGX: { + code: "UGX", + name: "Ugandan Shilling", + symbol_native: "USh", + decimal_digits: 0, + }, + UYU: { + code: "UYU", + name: "Uruguayan Peso", + symbol_native: "$", + decimal_digits: 2, + }, + UZS: { + code: "UZS", + name: "Uzbekistan Som", + symbol_native: "UZS", + decimal_digits: 0, + }, + VEF: { + code: "VEF", + name: "Venezuelan Bolívar", + symbol_native: "Bs.F.", + decimal_digits: 2, + }, + VND: { + code: "VND", + name: "Vietnamese Dong", + symbol_native: "₫", + decimal_digits: 0, + }, + XAF: { + code: "XAF", + name: "CFA Franc BEAC", + symbol_native: "FCFA", + decimal_digits: 0, + }, + XOF: { + code: "XOF", + name: "CFA Franc BCEAO", + symbol_native: "CFA", + decimal_digits: 0, + }, + YER: { + code: "YER", + name: "Yemeni Rial", + symbol_native: "ر.ي.‏", + decimal_digits: 0, + }, + ZAR: { + code: "ZAR", + name: "South African Rand", + symbol_native: "R", + decimal_digits: 2, + }, + ZMK: { + code: "ZMK", + name: "Zambian Kwacha", + symbol_native: "ZK", + decimal_digits: 0, + }, + ZWL: { + code: "ZWL", + name: "Zimbabwean Dollar", + symbol_native: "ZWL$", + decimal_digits: 0, + }, +} diff --git a/packages/admin-next/dashboard/src/lib/debounce.ts b/packages/admin-next/dashboard/src/lib/debounce.ts new file mode 100644 index 0000000000..3ac6187085 --- /dev/null +++ b/packages/admin-next/dashboard/src/lib/debounce.ts @@ -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) + } +} diff --git a/packages/admin-next/dashboard/src/lib/is-axios-error.ts b/packages/admin-next/dashboard/src/lib/is-axios-error.ts new file mode 100644 index 0000000000..1d8cba11f8 --- /dev/null +++ b/packages/admin-next/dashboard/src/lib/is-axios-error.ts @@ -0,0 +1,5 @@ +import type { AxiosError } from "axios" + +export const isAxiosError = (error: any): error is AxiosError => { + return error.isAxiosError +} diff --git a/packages/admin-next/dashboard/src/lib/medusa.ts b/packages/admin-next/dashboard/src/lib/medusa.ts index 5b3a4f4abe..1edaff40b1 100644 --- a/packages/admin-next/dashboard/src/lib/medusa.ts +++ b/packages/admin-next/dashboard/src/lib/medusa.ts @@ -1,6 +1,9 @@ import Medusa from "@medusajs/medusa-js" 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({ defaultOptions: { queries: { @@ -12,6 +15,6 @@ export const queryClient = new QueryClient({ }) export const medusa = new Medusa({ - baseUrl: "http://localhost:9000", - maxRetries: 3, + baseUrl: MEDUSA_BACKEND_URL, + maxRetries: 1, }) diff --git a/packages/admin-next/dashboard/src/lib/money-amount-helpers.ts b/packages/admin-next/dashboard/src/lib/money-amount-helpers.ts new file mode 100644 index 0000000000..d043af4f37 --- /dev/null +++ b/packages/admin-next/dashboard/src/lib/money-amount-helpers.ts @@ -0,0 +1,21 @@ +import { currencies } from "./currencies" + +export const getPresentationalAmount = (amount: number, currency: string) => { + const decimalDigits = currencies[currency.toUpperCase()].decimal_digits + + if (decimalDigits === 0) { + throw new Error("Currency has no decimal digits") + } + + return amount / 10 ** decimalDigits +} + +export const getDbAmount = (amount: number, currency: string) => { + const decimalDigits = currencies[currency.toUpperCase()].decimal_digits + + if (decimalDigits === 0) { + throw new Error("Currency has no decimal digits") + } + + return amount * 10 ** decimalDigits +} diff --git a/packages/admin-next/dashboard/src/providers/auth-provider/auth-context.tsx b/packages/admin-next/dashboard/src/providers/auth-provider/auth-context.tsx deleted file mode 100644 index 4068ea01d8..0000000000 --- a/packages/admin-next/dashboard/src/providers/auth-provider/auth-context.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { AdminAuthRes, User } from "@medusajs/medusa" -import { createContext } from "react" - -type AuthContextValue = { - login: (email: string, password: string) => Promise - user: Omit | null - isLoading: boolean -} - -export const AuthContext = createContext(null) diff --git a/packages/admin-next/dashboard/src/providers/auth-provider/auth-provider.tsx b/packages/admin-next/dashboard/src/providers/auth-provider/auth-provider.tsx deleted file mode 100644 index ddb2589cd4..0000000000 --- a/packages/admin-next/dashboard/src/providers/auth-provider/auth-provider.tsx +++ /dev/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 ( - - {children} - - ) -} diff --git a/packages/admin-next/dashboard/src/providers/auth-provider/index.ts b/packages/admin-next/dashboard/src/providers/auth-provider/index.ts deleted file mode 100644 index c2b66aa1c0..0000000000 --- a/packages/admin-next/dashboard/src/providers/auth-provider/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./auth-provider"; -export * from "./use-auth"; diff --git a/packages/admin-next/dashboard/src/providers/auth-provider/use-auth.tsx b/packages/admin-next/dashboard/src/providers/auth-provider/use-auth.tsx deleted file mode 100644 index 8bef3683ca..0000000000 --- a/packages/admin-next/dashboard/src/providers/auth-provider/use-auth.tsx +++ /dev/null @@ -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; -}; diff --git a/packages/admin-next/dashboard/src/providers/router-provider/router-provider.tsx b/packages/admin-next/dashboard/src/providers/router-provider/router-provider.tsx index ae5b05bfb8..3249db7847 100644 --- a/packages/admin-next/dashboard/src/providers/router-provider/router-provider.tsx +++ b/packages/admin-next/dashboard/src/providers/router-provider/router-provider.tsx @@ -1,18 +1,24 @@ +import type { + AdminCustomerGroupsRes, + AdminCustomersRes, + AdminProductsRes, + AdminRegionsRes, +} from "@medusajs/medusa" import { + Outlet, RouterProvider as Provider, RouteObject, createBrowserRouter, } from "react-router-dom" -import { RequireAuth } from "../../components/authentication/require-auth" -import { AppLayout } from "../../components/layout/app-layout" +import { ProtectedRoute } from "../../components/authentication/require-auth" +import { ErrorBoundary } from "../../components/error/error-boundary" +import { MainLayout } from "../../components/layout/main-layout" import { PublicLayout } from "../../components/layout/public-layout" +import { SettingsLayout } from "../../components/layout/settings-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 { SearchProvider } from "../search-provider" const routeExtensions: RouteObject[] = routes.pages.map((ext) => { return { @@ -45,161 +51,239 @@ const router = createBrowserRouter([ ], }, { - element: ( - - - - - - ), + element: , errorElement: , children: [ { path: "/", - lazy: () => import("../../routes/home"), - }, - { - path: "/orders", + element: , children: [ { index: true, - lazy: () => import("../../routes/orders/list"), + lazy: () => import("../../routes/home"), }, { - path: ":id", - 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"), + path: "/orders", handle: { - crumb: (data: AdminProductsRes) => data.product.title, + crumb: () => "Orders", }, - }, - ], - }, - { - path: "/categories", - children: [ - { - index: true, - lazy: () => import("../../routes/categories/list"), + children: [ + { + index: true, + lazy: () => import("../../routes/orders/list"), + }, + { + path: ":id", + lazy: () => import("../../routes/orders/details"), + }, + ], }, { - path: ":id", - lazy: () => import("../../routes/categories/details"), - }, - ], - }, - { - path: "/collections", - children: [ - { - index: true, - lazy: () => import("../../routes/collections/list"), + path: "/draft-orders", + handle: { + crumb: () => "Draft Orders", + }, + children: [ + { + index: true, + lazy: () => import("../../routes/draft-orders/list"), + }, + { + path: ":id", + lazy: () => import("../../routes/draft-orders/details"), + }, + ], }, { - path: ":id", - lazy: () => import("../../routes/collections/details"), - }, - ], - }, - { - path: "/customers", - children: [ - { - index: true, - lazy: () => import("../../routes/customers/list"), + path: "/products", + handle: { + crumb: () => "Products", + }, + children: [ + { + index: true, + lazy: () => import("../../routes/products/product-list"), + }, + { + path: ":id", + lazy: () => import("../../routes/products/product-detail"), + handle: { + crumb: (data: AdminProductsRes) => data.product.title, + }, + }, + ], }, { - path: ":id", - lazy: () => import("../../routes/customers/details"), - }, - ], - }, - { - path: "/customer-groups", - children: [ - { - index: true, - lazy: () => import("../../routes/customer-groups/list"), + path: "/categories", + handle: { + crumb: () => "Categories", + }, + children: [ + { + index: true, + lazy: () => import("../../routes/categories/list"), + }, + { + path: ":id", + lazy: () => import("../../routes/categories/details"), + }, + ], }, { - path: ":id", - lazy: () => import("../../routes/customer-groups/details"), - }, - ], - }, - { - path: "/gift-cards", - children: [ - { - index: true, - lazy: () => import("../../routes/gift-cards/list"), + path: "/collections", + handle: { + crumb: () => "Collections", + }, + children: [ + { + index: true, + lazy: () => import("../../routes/collections/list"), + }, + { + path: ":id", + lazy: () => import("../../routes/collections/details"), + }, + ], }, { - path: ":id", - lazy: () => import("../../routes/gift-cards/details"), - }, - ], - }, - { - path: "/inventory", - lazy: () => import("../../routes/inventory/list"), - }, - { - path: "/discounts", - children: [ - { - index: true, - lazy: () => import("../../routes/discounts/list"), + path: "/customers", + handle: { + crumb: () => "Customers", + }, + children: [ + { + path: "", + lazy: () => import("../../routes/customers/customer-list"), + children: [ + { + path: "create", + lazy: () => + import("../../routes/customers/customer-create"), + }, + ], + }, + { + path: ":id", + lazy: () => import("../../routes/customers/customer-detail"), + handle: { + crumb: (data: AdminCustomersRes) => data.customer.email, + }, + children: [ + { + path: "edit", + lazy: () => import("../../routes/customers/customer-edit"), + }, + ], + }, + ], }, { - path: ":id", - lazy: () => import("../../routes/discounts/details"), - }, - ], - }, - { - path: "/pricing", - children: [ - { - index: true, - lazy: () => import("../../routes/pricing/list"), + path: "/customer-groups", + handle: { + crumb: () => "Customer Groups", + }, + children: [ + { + path: "", + lazy: () => + import("../../routes/customer-groups/customer-group-list"), + children: [ + { + path: "create", + lazy: () => + import( + "../../routes/customer-groups/customer-group-create" + ), + }, + ], + }, + { + path: ":id", + lazy: () => + import("../../routes/customer-groups/customer-group-detail"), + handle: { + crumb: (data: AdminCustomerGroupsRes) => + data.customer_group.name, + }, + children: [ + { + path: "add-customers", + lazy: () => + import( + "../../routes/customer-groups/customer-group-add-customers" + ), + }, + { + path: "edit", + lazy: () => + import( + "../../routes/customer-groups/customer-group-edit" + ), + }, + ], + }, + ], }, { - path: ":id", - lazy: () => import("../../routes/pricing/details"), + path: "/gift-cards", + 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", + element: , handle: { crumb: () => "Settings", }, @@ -210,52 +294,131 @@ const router = createBrowserRouter([ }, { path: "profile", - lazy: () => import("../../routes/profile/views/profile-details"), + lazy: () => import("../../routes/profile/profile-detail"), handle: { crumb: () => "Profile", }, + children: [ + { + path: "edit", + lazy: () => import("../../routes/profile/profile-edit"), + }, + ], }, { path: "store", - lazy: () => import("../../routes/store/views/store-details"), + lazy: () => import("../../routes/store/store-detail"), handle: { crumb: () => "Store", }, + children: [ + { + path: "edit", + lazy: () => import("../../routes/store/store-edit"), + }, + { + path: "add-currencies", + lazy: () => import("../../routes/store/store-add-currencies"), + }, + ], }, { path: "locations", - lazy: () => import("../../routes/locations/list"), + element: , + 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", + element: , handle: { crumb: () => "Regions", }, children: [ { - index: true, - lazy: () => import("../../routes/regions/views/region-list"), + path: "", + lazy: () => import("../../routes/regions/region-list"), + children: [ + { + path: "create", + lazy: () => import("../../routes/regions/region-create"), + }, + ], }, { 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", - lazy: () => import("../../routes/users"), + element: , handle: { crumb: () => "Users", }, - }, - { - path: "currencies", - lazy: () => - import("../../routes/currencies/views/currencies-details"), - handle: { - crumb: () => "Currencies", - }, + children: [ + { + path: "", + lazy: () => import("../../routes/users/user-list"), + children: [ + { + 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", @@ -275,32 +438,86 @@ const router = createBrowserRouter([ }, { path: "sales-channels", + element: , handle: { crumb: () => "Sales Channels", }, children: [ { - index: true, + path: "", lazy: () => - import( - "../../routes/sales-channels/views/sales-channel-list" - ), + import("../../routes/sales-channels/sales-channel-list"), + children: [ + { + path: "create", + lazy: () => + import( + "../../routes/sales-channels/sales-channel-create" + ), + }, + ], }, { path: ":id", lazy: () => - import( - "../../routes/sales-channels/views/sales-channel-details" - ), + import("../../routes/sales-channels/sales-channel-detail"), + 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", - lazy: () => import("../../routes/api-key-management"), + element: , handle: { 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, ], diff --git a/packages/admin-next/dashboard/src/providers/sidebar-provider/index.ts b/packages/admin-next/dashboard/src/providers/sidebar-provider/index.ts new file mode 100644 index 0000000000..abb41fa819 --- /dev/null +++ b/packages/admin-next/dashboard/src/providers/sidebar-provider/index.ts @@ -0,0 +1,2 @@ +export * from "./sidebar-provider" +export * from "./use-sidebar" diff --git a/packages/admin-next/dashboard/src/providers/sidebar-provider/sidebar-context.tsx b/packages/admin-next/dashboard/src/providers/sidebar-provider/sidebar-context.tsx new file mode 100644 index 0000000000..23d0e7298f --- /dev/null +++ b/packages/admin-next/dashboard/src/providers/sidebar-provider/sidebar-context.tsx @@ -0,0 +1,9 @@ +import { createContext } from "react" + +type SidebarContextValue = { + desktop: boolean + mobile: boolean + toggle: (view: "desktop" | "mobile") => void +} + +export const SidebarContext = createContext(null) diff --git a/packages/admin-next/dashboard/src/providers/sidebar-provider/sidebar-provider.tsx b/packages/admin-next/dashboard/src/providers/sidebar-provider/sidebar-provider.tsx new file mode 100644 index 0000000000..e9013d2bdf --- /dev/null +++ b/packages/admin-next/dashboard/src/providers/sidebar-provider/sidebar-provider.tsx @@ -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 ( + + {children} + + ) +} diff --git a/packages/admin-next/dashboard/src/providers/sidebar-provider/use-sidebar.tsx b/packages/admin-next/dashboard/src/providers/sidebar-provider/use-sidebar.tsx new file mode 100644 index 0000000000..21cb708e15 --- /dev/null +++ b/packages/admin-next/dashboard/src/providers/sidebar-provider/use-sidebar.tsx @@ -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 +} diff --git a/packages/admin-next/dashboard/src/routes/api-key-management/api-key-management-create/api-key-management-create.tsx b/packages/admin-next/dashboard/src/routes/api-key-management/api-key-management-create/api-key-management-create.tsx new file mode 100644 index 0000000000..ef7c98898e --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/api-key-management/api-key-management-create/api-key-management-create.tsx @@ -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 ( + + + + + + ) +} diff --git a/packages/admin-next/dashboard/src/routes/api-key-management/api-key-management-create/components/create-publishable-api-key-form/create-publishable-api-key-form.tsx b/packages/admin-next/dashboard/src/routes/api-key-management/api-key-management-create/components/create-publishable-api-key-form/create-publishable-api-key-form.tsx new file mode 100644 index 0000000000..654550fec5 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/api-key-management/api-key-management-create/components/create-publishable-api-key-form/create-publishable-api-key-form.tsx @@ -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>({ + 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 ( +
+ + +
+ + + + +
+
+ +
+
+
+ + {t("apiKeyManagement.createPublishableApiKey")} + + + {t("apiKeyManagement.publishableApiKeyHint")} + +
+
+ { + return ( + + {t("fields.title")} + + + + + + ) + }} + /> +
+
+
+
+
+ + ) +} diff --git a/packages/admin-next/dashboard/src/routes/api-key-management/api-key-management-create/components/create-publishable-api-key-form/index.ts b/packages/admin-next/dashboard/src/routes/api-key-management/api-key-management-create/components/create-publishable-api-key-form/index.ts new file mode 100644 index 0000000000..e5aa57664b --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/api-key-management/api-key-management-create/components/create-publishable-api-key-form/index.ts @@ -0,0 +1 @@ +export * from "./create-publishable-api-key-form" diff --git a/packages/admin-next/dashboard/src/routes/api-key-management/api-key-management-create/index.ts b/packages/admin-next/dashboard/src/routes/api-key-management/api-key-management-create/index.ts new file mode 100644 index 0000000000..e85125cac7 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/api-key-management/api-key-management-create/index.ts @@ -0,0 +1 @@ +export { ApiKeyManagementCreate as Component } from "./api-key-management-create" diff --git a/packages/admin-next/dashboard/src/routes/api-key-management/api-key-management-detail/api-key-management-detail.tsx b/packages/admin-next/dashboard/src/routes/api-key-management/api-key-management-detail/api-key-management-detail.tsx new file mode 100644 index 0000000000..169888b96e --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/api-key-management/api-key-management-detail/api-key-management-detail.tsx @@ -0,0 +1,3 @@ +export const ApiKeyManagementDetail = () => { + return
+} diff --git a/packages/admin-next/dashboard/src/routes/api-key-management/api-key-management-detail/index.ts b/packages/admin-next/dashboard/src/routes/api-key-management/api-key-management-detail/index.ts new file mode 100644 index 0000000000..2c193a6bd8 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/api-key-management/api-key-management-detail/index.ts @@ -0,0 +1 @@ +export { ApiKeyManagementDetail as Component } from "./api-key-management-detail" diff --git a/packages/admin-next/dashboard/src/routes/api-key-management/api-key-management-edit/api-key-management-edit.tsx b/packages/admin-next/dashboard/src/routes/api-key-management/api-key-management-edit/api-key-management-edit.tsx new file mode 100644 index 0000000000..5c4125b5dd --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/api-key-management/api-key-management-edit/api-key-management-edit.tsx @@ -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 ( + + + + ) +} diff --git a/packages/admin-next/dashboard/src/routes/api-key-management/api-key-management-edit/index.ts b/packages/admin-next/dashboard/src/routes/api-key-management/api-key-management-edit/index.ts new file mode 100644 index 0000000000..dcda77a479 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/api-key-management/api-key-management-edit/index.ts @@ -0,0 +1 @@ +export { ApiKeyManagementEdit as Component } from "./api-key-management-edit" diff --git a/packages/admin-next/dashboard/src/routes/api-key-management/api-key-management.tsx b/packages/admin-next/dashboard/src/routes/api-key-management/api-key-management-list/api-key-management-list.tsx similarity index 62% rename from packages/admin-next/dashboard/src/routes/api-key-management/api-key-management.tsx rename to packages/admin-next/dashboard/src/routes/api-key-management/api-key-management-list/api-key-management-list.tsx index d896d2578a..9f65eeb782 100644 --- a/packages/admin-next/dashboard/src/routes/api-key-management/api-key-management.tsx +++ b/packages/admin-next/dashboard/src/routes/api-key-management/api-key-management-list/api-key-management-list.tsx @@ -1,4 +1,3 @@ -import { InformationCircle } from "@medusajs/icons" import { PublishableApiKey, SalesChannel } from "@medusajs/medusa" import { Button, @@ -21,136 +20,22 @@ import { } from "@tanstack/react-table" import { useAdminCreatePublishableApiKey, - useAdminPublishableApiKeys, useAdminSalesChannels, } from "medusa-react" -import { useMemo, useState } from "react" +import { useMemo } from "react" import { useForm } from "react-hook-form" import { useTranslation } from "react-i18next" import * as zod from "zod" -import { Form } from "../../components/common/form" - -export const ApiKeyManagement = () => { - 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
Loading
- } - - // 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 ( -
- {(err as Error & { status: number })?.status === 404 ? ( -
Not found
- ) : ( -
Something went wrong!
- )} -
- ) - } - - const hasData = publishable_api_keys.length !== 0 +import { Outlet } from "react-router-dom" +import { Form } from "../../../components/common/form" +import { ApiKeyManagementListTable } from "./components/api-key-management-list-table" +export const ApiKeyManagementList = () => { return (
- -
- {t("apiKeyManagement.domain")} -
-
- {hasData ? ( - - - {table.getHeaderGroups().map((headerGroup) => { - return ( - - {headerGroup.headers.map((header) => { - return ( - - {flexRender( - header.column.columnDef.header, - header.getContext() - )} - - ) - })} - - ) - })} - - - {table.getRowModel().rows.map((row) => ( - - {row.getVisibleCells().map((cell) => ( - - {flexRender( - cell.column.columnDef.cell, - cell.getContext() - )} - - ))} - - ))} - -
- ) : ( -
-
-
- - - {t("general.noRecordsFound")} - - - {t("apiKeyManagement.createAPublishableApiKey")} - -
- -
-
- )} -
-
-
- + +
) } @@ -274,8 +159,14 @@ const CreatePublishableApiKey = (props: CreatePublishableApiKeyProps) => {
- - + + + +
diff --git a/packages/admin-next/dashboard/src/routes/api-key-management/api-key-management-list/components/api-key-management-list-table/api-key-management-list-table.tsx b/packages/admin-next/dashboard/src/routes/api-key-management/api-key-management-list/components/api-key-management-list-table/api-key-management-list-table.tsx new file mode 100644 index 0000000000..cdbf3d8adb --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/api-key-management/api-key-management-list/components/api-key-management-list-table/api-key-management-list-table.tsx @@ -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({ + pageIndex: 0, + pageSize: PAGE_SIZE, + }) + + const pagination = useMemo( + () => ({ + pageIndex, + pageSize, + }), + [pageIndex, pageSize] + ) + + const [rowSelection, setRowSelection] = useState({}) + + 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
Loading...
+ } + + if (isError) { + throw error + } + + return ( + +
+ {t("apiKeyManagement.domain")} + + + +
+
+ {(publishable_api_keys?.length ?? 0) > 0 ? ( +
+ + + {table.getHeaderGroups().map((headerGroup) => { + return ( + + {headerGroup.headers.map((header) => { + return ( + + {flexRender( + header.column.columnDef.header, + header.getContext() + )} + + ) + })} + + ) + })} + + + {table.getRowModel().rows.map((row) => ( + + navigate( + `/settings/api-key-management/${row.original.id}` + ) + } + > + {row.getVisibleCells().map((cell) => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} + + ))} + + ))} + +
+ +
+ ) : ( + + )} +
+
+ ) +} + +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 ( + + + + + + + + +
+ + {t("apiKeyManagement.revoke")} +
+
+ +
+ + {t("general.delete")} +
+
+
+
+ ) +} + +const columnHelper = createColumnHelper() + +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 + }, + }), + ], + [t] + ) + + return columns +} diff --git a/packages/admin-next/dashboard/src/routes/api-key-management/api-key-management-list/components/api-key-management-list-table/index.ts b/packages/admin-next/dashboard/src/routes/api-key-management/api-key-management-list/components/api-key-management-list-table/index.ts new file mode 100644 index 0000000000..ff8ba6f75d --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/api-key-management/api-key-management-list/components/api-key-management-list-table/index.ts @@ -0,0 +1 @@ +export * from "./api-key-management-list-table" diff --git a/packages/admin-next/dashboard/src/routes/api-key-management/api-key-management-list/index.ts b/packages/admin-next/dashboard/src/routes/api-key-management/api-key-management-list/index.ts new file mode 100644 index 0000000000..7fd84a7245 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/api-key-management/api-key-management-list/index.ts @@ -0,0 +1 @@ +export { ApiKeyManagementList as Component } from "./api-key-management-list" diff --git a/packages/admin-next/dashboard/src/routes/api-key-management/index.ts b/packages/admin-next/dashboard/src/routes/api-key-management/index.ts deleted file mode 100644 index 9ddecac337..0000000000 --- a/packages/admin-next/dashboard/src/routes/api-key-management/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { ApiKeyManagement as Component } from "./api-key-management" diff --git a/packages/admin-next/dashboard/src/routes/currencies/components/edit-currencies-details-drawer/edit-currencies-details-drawer.tsx b/packages/admin-next/dashboard/src/routes/currencies/components/edit-currencies-details-drawer/edit-currencies-details-drawer.tsx deleted file mode 100644 index 3a2fc31169..0000000000 --- a/packages/admin-next/dashboard/src/routes/currencies/components/edit-currencies-details-drawer/edit-currencies-details-drawer.tsx +++ /dev/null @@ -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>({ - 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 ( - - - - - - - {t("currencies.editCurrencyDetails")} - - -
- - { - return ( - - {t("currencies.defaultCurrency")} -
- - - - -
- - {t("currencies.defaultCurrencyHint")} - -
- ) - }} - /> - - -
- - - - - - -
-
- ) -} diff --git a/packages/admin-next/dashboard/src/routes/currencies/components/edit-currencies-details-drawer/index.ts b/packages/admin-next/dashboard/src/routes/currencies/components/edit-currencies-details-drawer/index.ts deleted file mode 100644 index 8a247de79e..0000000000 --- a/packages/admin-next/dashboard/src/routes/currencies/components/edit-currencies-details-drawer/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./edit-currencies-details-drawer" diff --git a/packages/admin-next/dashboard/src/routes/currencies/views/currencies-details/currencies-details.tsx b/packages/admin-next/dashboard/src/routes/currencies/views/currencies-details/currencies-details.tsx deleted file mode 100644 index c2550a459a..0000000000 --- a/packages/admin-next/dashboard/src/routes/currencies/views/currencies-details/currencies-details.tsx +++ /dev/null @@ -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
Loading...
- } - - return ( -
- -
-
- {t("currencies.domain")} - - {t("currencies.manageTheCurrencies")} - -
- -
-
- - {t("currencies.defaultCurrency")} - -
- - {store.default_currency_code} - - - {store.default_currency.name} - -
-
-
- -
- ) -} - -type StoreCurrenciesSectionProps = { - store: Store -} - -const PAGE_SIZE = 20 - -const StoreCurrencySection = ({ store }: StoreCurrenciesSectionProps) => { - const [addModalOpen, setAddModalOpen] = useState(false) - const [rowSelection, setRowSelection] = useState({}) - 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 ( - -
- Store Currencies - - - - - - - - setAddModalOpen(!addModalOpen)} - > - - Add Currencies - - - - Tax Preferences - - - -
-
- - - {table.getHeaderGroups().map((headerGroup) => { - return ( - - {headerGroup.headers.map((header) => { - return ( - - {flexRender( - header.column.columnDef.header, - header.getContext() - )} - - ) - })} - - ) - })} - - - {table.getRowModel().rows.map((row) => ( - - {row.getVisibleCells().map((cell) => ( - - {flexRender(cell.column.columnDef.cell, cell.getContext())} - - ))} - - ))} - -
- - - - - {t("general.countSelected", { - count: Object.keys(rowSelection).length, - })} - - - - - -
- -
- ) -} - -const storeCurrencyColumnHelper = createColumnHelper() - -const useStoreCurrencyColumns = () => { - const { t } = useTranslation() - - return useMemo( - () => [ - storeCurrencyColumnHelper.display({ - id: "select", - header: ({ table }) => { - return ( - - table.toggleAllPageRowsSelected(!!value) - } - /> - ) - }, - cell: ({ row }) => { - return ( - 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({ - pageIndex: 0, - pageSize: CURRENCIES_PAGE_SIZE, - }) - - const pagination = useMemo( - () => ({ - pageIndex, - pageSize, - }), - [pageIndex, pageSize] - ) - - const [rowSelection, setRowSelection] = useState({}) - - 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 ( - - - -
- - -
-
- -
- - - {table.getHeaderGroups().map((headerGroup) => { - return ( - - {headerGroup.headers.map((header) => { - return ( - - {flexRender( - header.column.columnDef.header, - header.getContext() - )} - - ) - })} - - ) - })} - - - {table.getRowModel().rows.map((row) => ( - c.code) - ?.includes(row.original.code), - }, - { - "bg-ui-bg-highlight hover:bg-ui-bg-highlight-hover": - row.getIsSelected(), - } - )} - > - {row.getVisibleCells().map((cell) => ( - - {flexRender( - cell.column.columnDef.cell, - cell.getContext() - )} - - ))} - - ))} - -
-
-
- -
-
-
-
- ) -} - -const currencyColumnHelper = createColumnHelper() - -const useCurrencyColumns = () => { - const { t } = useTranslation() - - return useMemo( - () => [ - currencyColumnHelper.display({ - id: "select", - header: ({ table }) => { - return ( - - 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 = ( - row.toggleSelected(!!value)} - onClick={(e) => { - e.stopPropagation() - }} - /> - ) - - if (isAdded) { - return ( - - {Component} - - ) - } - - return Component - }, - }), - currencyColumnHelper.accessor("code", { - header: t("fields.code"), - cell: ({ getValue }) => getValue().toUpperCase(), - }), - currencyColumnHelper.accessor("name", { - header: t("fields.name"), - cell: ({ getValue }) => getValue(), - }), - ], - [t] - ) -} diff --git a/packages/admin-next/dashboard/src/routes/currencies/views/currencies-details/index.ts b/packages/admin-next/dashboard/src/routes/currencies/views/currencies-details/index.ts deleted file mode 100644 index 5069f29b0c..0000000000 --- a/packages/admin-next/dashboard/src/routes/currencies/views/currencies-details/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { CurrenciesDetails as Component } from "./currencies-details" diff --git a/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-add-customers/components/add-customers-form/add-customers-form.tsx b/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-add-customers/components/add-customers-form/add-customers-form.tsx new file mode 100644 index 0000000000..ac47e264f5 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-add-customers/components/add-customers-form/add-customers-form.tsx @@ -0,0 +1,347 @@ +import { zodResolver } from "@hookform/resolvers/zod" +import { Customer } from "@medusajs/medusa" +import { + Button, + Checkbox, + FocusModal, + Hint, + Table, + Tooltip, + clx, +} from "@medusajs/ui" +import { + PaginationState, + RowSelectionState, + createColumnHelper, + flexRender, + getCoreRowModel, + useReactTable, +} from "@tanstack/react-table" +import { + adminCustomerKeys, + useAdminAddCustomersToCustomerGroup, + useAdminCustomers, +} from "medusa-react" +import { useEffect, useMemo, useState } from "react" +import { useForm } from "react-hook-form" +import { useTranslation } from "react-i18next" +import { useNavigate } from "react-router-dom" +import * as zod from "zod" +import { + NoRecords, + NoResults, +} from "../../../../../components/common/empty-table-content" +import { Form } from "../../../../../components/common/form" +import { Query } from "../../../../../components/filtering/query" +import { LocalizedTablePagination } from "../../../../../components/localization/localized-table-pagination" +import { useQueryParams } from "../../../../../hooks/use-query-params" +import { queryClient } from "../../../../../lib/medusa" + +type AddCustomersFormProps = { + customerGroupId: string + subscribe: (state: boolean) => void +} + +const AddCustomersSchema = zod.object({ + customer_ids: zod.array(zod.string()).min(1), +}) + +const PAGE_SIZE = 10 + +export const AddCustomersForm = ({ + customerGroupId, + subscribe, +}: AddCustomersFormProps) => { + const navigate = useNavigate() + const { t } = useTranslation() + + const form = useForm>({ + defaultValues: { + customer_ids: [], + }, + resolver: zodResolver(AddCustomersSchema), + }) + + const { + formState: { isDirty }, + } = form + + useEffect(() => { + subscribe(isDirty) + }, [isDirty]) + + const [{ pageIndex, pageSize }, setPagination] = useState({ + pageIndex: 0, + pageSize: PAGE_SIZE, + }) + + const pagination = useMemo( + () => ({ + pageIndex, + pageSize, + }), + [pageIndex, pageSize] + ) + + const [rowSelection, setRowSelection] = useState({}) + + useEffect(() => { + form.setValue( + "customer_ids", + Object.keys(rowSelection).filter((k) => rowSelection[k]) + ) + }, [rowSelection]) + + const params = useQueryParams(["q"]) + const { customers, count, isLoading, isError, error } = useAdminCustomers({ + expand: "groups", + limit: PAGE_SIZE, + offset: pageIndex * PAGE_SIZE, + ...params, + }) + + const columns = useColumns() + + const table = useReactTable({ + data: customers ?? [], + columns, + pageCount: Math.ceil((count ?? 0) / PAGE_SIZE), + state: { + pagination, + rowSelection, + }, + getRowId: (row) => row.id, + onPaginationChange: setPagination, + onRowSelectionChange: setRowSelection, + getCoreRowModel: getCoreRowModel(), + manualPagination: true, + meta: { + customerGroupId, + }, + }) + + const { mutateAsync, isLoading: isMutating } = + useAdminAddCustomersToCustomerGroup(customerGroupId) + + const handleSubmit = form.handleSubmit(async (data) => { + await mutateAsync( + { + customer_ids: data.customer_ids.map((id) => ({ id })), + }, + { + onSuccess: () => { + queryClient.invalidateQueries(adminCustomerKeys.lists()) + navigate(`/customer-groups/${customerGroupId}`) + }, + } + ) + }) + + const noRecords = + !isLoading && + !customers?.length && + !Object.values(params).filter(Boolean).length + + if (isError) { + throw error + } + + return ( +
+ + +
+ {form.formState.errors.customer_ids && ( + + {form.formState.errors.customer_ids.message} + + )} + + + + +
+
+ + {noRecords ? ( +
+ +
+ ) : ( +
+
+
+
+ +
+
+
+ {(customers?.length || 0) > 0 ? ( + + + {table.getHeaderGroups().map((headerGroup) => { + return ( + + {headerGroup.headers.map((header) => { + return ( + + {flexRender( + header.column.columnDef.header, + header.getContext() + )} + + ) + })} + + ) + })} + + + {table.getRowModel().rows.map((row) => ( + cg.id) + .includes(customerGroupId), + } + )} + > + {row.getVisibleCells().map((cell) => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} + + ))} + + ))} + +
+ ) : ( +
+ +
+ )} +
+ +
+ )} +
+
+ + ) +} + +const columnHelper = createColumnHelper() + +const useColumns = () => { + const { t } = useTranslation() + + const columns = useMemo( + () => [ + columnHelper.display({ + id: "select", + header: ({ table }) => { + return ( + + table.toggleAllPageRowsSelected(!!value) + } + /> + ) + }, + cell: ({ row, table }) => { + const { customerGroupId } = table.options.meta as { + customerGroupId: string + } + + const isAdded = row.original.groups + ?.map((gc) => gc.id) + .includes(customerGroupId) + + const isSelected = row.getIsSelected() || isAdded + + const Component = ( + row.toggleSelected(!!value)} + onClick={(e) => { + e.stopPropagation() + }} + /> + ) + + if (isAdded) { + return ( + + {Component} + + ) + } + + return Component + }, + }), + columnHelper.accessor("email", { + header: t("fields.email"), + cell: ({ getValue }) => getValue(), + }), + columnHelper.display({ + id: "name", + header: t("fields.name"), + cell: ({ row }) => { + const name = [row.original.first_name, row.original.last_name] + .filter(Boolean) + .join(" ") + + return name || "-" + }, + }), + ], + [t] + ) + + return columns +} diff --git a/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-add-customers/components/add-customers-form/index.ts b/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-add-customers/components/add-customers-form/index.ts new file mode 100644 index 0000000000..512d1edf6f --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-add-customers/components/add-customers-form/index.ts @@ -0,0 +1 @@ +export * from "./add-customers-form" diff --git a/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-add-customers/customer-group-add-customers.tsx b/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-add-customers/customer-group-add-customers.tsx new file mode 100644 index 0000000000..874d277800 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-add-customers/customer-group-add-customers.tsx @@ -0,0 +1,18 @@ +import { FocusModal } from "@medusajs/ui" +import { useParams } from "react-router-dom" +import { useRouteModalState } from "../../../hooks/use-route-modal-state" +import { AddCustomersForm } from "./components/add-customers-form" + +export const CustomerGroupAddCustomers = () => { + const [open, onOpenChange, subscribe] = useRouteModalState() + + const { id } = useParams() + + return ( + + + + + + ) +} diff --git a/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-add-customers/index.ts b/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-add-customers/index.ts new file mode 100644 index 0000000000..3b39c9e782 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-add-customers/index.ts @@ -0,0 +1 @@ +export { CustomerGroupAddCustomers as Component } from "./customer-group-add-customers" diff --git a/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-create/components/create-customer-group-form/create-customer-group-form.tsx b/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-create/components/create-customer-group-form/create-customer-group-form.tsx new file mode 100644 index 0000000000..76ff4075f0 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-create/components/create-customer-group-form/create-customer-group-form.tsx @@ -0,0 +1,105 @@ +import { zodResolver } from "@hookform/resolvers/zod" +import { Button, FocusModal, Heading, Input, Text } from "@medusajs/ui" +import { useAdminCreateCustomerGroup } from "medusa-react" +import { useEffect } from "react" +import { useForm } from "react-hook-form" +import { useTranslation } from "react-i18next" +import { useNavigate } from "react-router-dom" +import * as zod from "zod" +import { Form } from "../../../../../components/common/form" + +type CreateCustomerGroupFormProps = { + subscribe: (state: boolean) => void +} + +const CreateCustomerGroupSchema = zod.object({ + name: zod.string().min(1), +}) + +export const CreateCustomerGroupForm = ({ + subscribe, +}: CreateCustomerGroupFormProps) => { + const { t } = useTranslation() + const navigate = useNavigate() + + const form = useForm>({ + defaultValues: { + name: "", + }, + resolver: zodResolver(CreateCustomerGroupSchema), + }) + + const { + formState: { isDirty }, + } = form + + useEffect(() => { + subscribe(isDirty) + }, [isDirty]) + + const { mutateAsync, isLoading } = useAdminCreateCustomerGroup() + + const handleSubmit = form.handleSubmit(async (data) => { + await mutateAsync( + { + name: data.name, + }, + { + onSuccess: ({ customer_group }) => { + navigate(`/customer-groups/${customer_group.id}`) + }, + } + ) + }) + + return ( +
+ + +
+ + + + +
+
+ +
+
+ {t("customerGroups.createCustomerGroup")} + + {t("customerGroups.createCustomerGroupHint")} + +
+
+ { + return ( + + {t("fields.name")} + + + + + + ) + }} + /> +
+
+
+
+ + ) +} diff --git a/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-create/components/create-customer-group-form/index.ts b/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-create/components/create-customer-group-form/index.ts new file mode 100644 index 0000000000..960232c9c2 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-create/components/create-customer-group-form/index.ts @@ -0,0 +1 @@ +export * from "./create-customer-group-form" diff --git a/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-create/customer-group-create.tsx b/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-create/customer-group-create.tsx new file mode 100644 index 0000000000..6fc39b5067 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-create/customer-group-create.tsx @@ -0,0 +1,15 @@ +import { FocusModal } from "@medusajs/ui" +import { useRouteModalState } from "../../../hooks/use-route-modal-state" +import { CreateCustomerGroupForm } from "./components/create-customer-group-form" + +export const CustomerGroupCreate = () => { + const [open, onOpenChange, subscribe] = useRouteModalState() + + return ( + + + + + + ) +} diff --git a/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-create/index.ts b/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-create/index.ts new file mode 100644 index 0000000000..9bab0611a8 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-create/index.ts @@ -0,0 +1 @@ +export { CustomerGroupCreate as Component } from "./customer-group-create" diff --git a/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-detail/components/customer-group-customer-section/customer-group-customer-section.tsx b/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-detail/components/customer-group-customer-section/customer-group-customer-section.tsx new file mode 100644 index 0000000000..04eb0e4bcb --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-detail/components/customer-group-customer-section/customer-group-customer-section.tsx @@ -0,0 +1,387 @@ +import { PencilSquare, Trash } from "@medusajs/icons" +import { Customer, CustomerGroup } from "@medusajs/medusa" +import { + Button, + Checkbox, + CommandBar, + Container, + Heading, + StatusBadge, + Table, + clx, + usePrompt, +} from "@medusajs/ui" +import { + PaginationState, + RowSelectionState, + createColumnHelper, + flexRender, + getCoreRowModel, + useReactTable, +} from "@tanstack/react-table" +import { + useAdminCustomerGroupCustomers, + useAdminRemoveCustomersFromCustomerGroup, +} from "medusa-react" +import { useMemo, useState } from "react" +import { useTranslation } from "react-i18next" +import { Link, useNavigate } from "react-router-dom" +import { + NoRecords, + NoResults, +} from "../../../../../components/common/empty-table-content" +import { TableRowActions } from "../../../../../components/common/table-row-actions" +import { Query } from "../../../../../components/filtering/query" +import { LocalizedTablePagination } from "../../../../../components/localization/localized-table-pagination" +import { useQueryParams } from "../../../../../hooks/use-query-params" + +type CustomerGroupCustomerSectionProps = { + group: CustomerGroup +} + +const PAGE_SIZE = 10 + +export const CustomerGroupCustomerSection = ({ + group, +}: CustomerGroupCustomerSectionProps) => { + const navigate = useNavigate() + const { t } = useTranslation() + + const [{ pageIndex, pageSize }, setPagination] = useState({ + pageIndex: 0, + pageSize: PAGE_SIZE, + }) + + const pagination = useMemo( + () => ({ + pageIndex, + pageSize, + }), + [pageIndex, pageSize] + ) + + const [rowSelection, setRowSelection] = useState({}) + + const params = useQueryParams(["q"]) + const { customers, count, isLoading, isError, error } = + useAdminCustomerGroupCustomers( + group.id, + { + limit: PAGE_SIZE, + offset: pageIndex * PAGE_SIZE, + ...params, + }, + { + keepPreviousData: true, + } + ) + + const columns = useColumns() + + const table = useReactTable({ + data: customers ?? [], + columns, + pageCount: Math.ceil((count ?? 0) / PAGE_SIZE), + state: { + pagination, + rowSelection, + }, + getRowId: (row) => row.id, + onPaginationChange: setPagination, + onRowSelectionChange: setRowSelection, + getCoreRowModel: getCoreRowModel(), + manualPagination: true, + meta: { + customerGroupId: group.id, + }, + }) + + const { mutateAsync } = useAdminRemoveCustomersFromCustomerGroup(group.id) + const prompt = usePrompt() + + const handleRemoveCustomers = async () => { + const selected = Object.keys(rowSelection).filter((k) => rowSelection[k]) + + const res = await prompt({ + title: t("general.areYouSure"), + description: t("customerGroups.removeCustomersWarning", { + count: selected.length, + }), + confirmText: t("general.continue"), + cancelText: t("general.cancel"), + }) + + if (!res) { + return + } + + await mutateAsync( + { + customer_ids: selected.map((s) => ({ id: s })), + }, + { + onSuccess: () => { + setRowSelection({}) + }, + } + ) + } + + const noRecords = + !isLoading && + !customers?.length && + !Object.values(params).filter(Boolean).length + + if (isError) { + throw error + } + + return ( + +
+ {t("customers.domain")} + + + +
+
+ {noRecords ? ( + + ) : ( +
+
+
+
+ +
+
+
+ {(customers?.length || 0) > 0 ? ( + + + {table.getHeaderGroups().map((headerGroup) => { + return ( + + {headerGroup.headers.map((header) => { + return ( + + {flexRender( + header.column.columnDef.header, + header.getContext() + )} + + ) + })} + + ) + })} + + + {table.getRowModel().rows.map((row) => ( + + navigate(`/customers/${row.original.id}`) + } + > + {row.getVisibleCells().map((cell) => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} + + ))} + + ))} + +
+ ) : ( +
+ +
+ )} + + + + + {t("general.countSelected", { + count: Object.keys(rowSelection).length, + })} + + + + + +
+
+ )} +
+
+ ) +} + +const CustomerActions = ({ + customer, + customerGroupId, +}: { + customer: Customer + customerGroupId: string +}) => { + const { t } = useTranslation() + const { mutateAsync } = + useAdminRemoveCustomersFromCustomerGroup(customerGroupId) + + const prompt = usePrompt() + + const handleRemove = async () => { + const res = await prompt({ + title: t("general.areYouSure"), + description: t("customerGroups.removeCustomersWarning", { + count: 1, + }), + confirmText: t("general.continue"), + cancelText: t("general.cancel"), + }) + + if (!res) { + return + } + + await mutateAsync({ + customer_ids: [{ id: customer.id }], + }) + } + + return ( + , + label: t("general.edit"), + to: `/customers/${customer.id}/edit`, + }, + ], + }, + { + actions: [ + { + icon: , + label: t("general.remove"), + onClick: handleRemove, + }, + ], + }, + ]} + /> + ) +} + +const columnHelper = createColumnHelper() + +const useColumns = () => { + const { t } = useTranslation() + + return useMemo( + () => [ + columnHelper.display({ + id: "select", + header: ({ table }) => { + return ( + + table.toggleAllPageRowsSelected(!!value) + } + /> + ) + }, + cell: ({ row }) => { + return ( + row.toggleSelected(!!value)} + onClick={(e) => { + e.stopPropagation() + }} + /> + ) + }, + }), + columnHelper.accessor("email", { + header: t("fields.email"), + cell: ({ getValue }) => {getValue()}, + }), + columnHelper.display({ + id: "name", + header: t("fields.name"), + cell: ({ row }) => { + const name = [row.original.first_name, row.original.last_name] + .filter(Boolean) + .join(" ") + + return name || "-" + }, + }), + columnHelper.accessor("has_account", { + header: t("fields.account"), + cell: ({ getValue }) => { + const hasAccount = getValue() + + return ( + + {hasAccount ? t("customers.registered") : t("customers.guest")} + + ) + }, + }), + columnHelper.display({ + id: "actions", + cell: ({ row, table }) => { + const { customerGroupId } = table.options.meta as { + customerGroupId: string + } + + return ( + + ) + }, + }), + ], + [t] + ) +} diff --git a/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-detail/components/customer-group-customer-section/index.ts b/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-detail/components/customer-group-customer-section/index.ts new file mode 100644 index 0000000000..3098ff7aca --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-detail/components/customer-group-customer-section/index.ts @@ -0,0 +1 @@ +export * from "./customer-group-customer-section" diff --git a/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-detail/components/customer-group-general-section/customer-group-general-section.tsx b/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-detail/components/customer-group-general-section/customer-group-general-section.tsx new file mode 100644 index 0000000000..e11dd5464e --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-detail/components/customer-group-general-section/customer-group-general-section.tsx @@ -0,0 +1,56 @@ +import { EllipsisHorizontal, PencilSquare, Trash } from "@medusajs/icons" +import type { CustomerGroup } from "@medusajs/medusa" +import { Container, DropdownMenu, Heading, IconButton } from "@medusajs/ui" +import { useAdminDeleteCustomerGroup } from "medusa-react" +import { useTranslation } from "react-i18next" +import { Link, useNavigate } from "react-router-dom" + +type CustomerGroupGeneralSectionProps = { + group: CustomerGroup +} + +export const CustomerGroupGeneralSection = ({ + group, +}: CustomerGroupGeneralSectionProps) => { + const { t } = useTranslation() + const navigate = useNavigate() + + const { mutateAsync } = useAdminDeleteCustomerGroup(group.id) + + const handleDelete = async () => { + await mutateAsync(undefined, { + onSuccess: () => { + navigate("/customer-groups", { replace: true }) + }, + }) + } + + return ( + + {group.name} + + + + + + + + + + + {t("general.edit")} + + + + + + {t("general.delete")} + + + + + ) +} diff --git a/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-detail/components/customer-group-general-section/index.ts b/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-detail/components/customer-group-general-section/index.ts new file mode 100644 index 0000000000..dfe7099024 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-detail/components/customer-group-general-section/index.ts @@ -0,0 +1 @@ +export * from "./customer-group-general-section" diff --git a/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-detail/customer-group-detail.tsx b/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-detail/customer-group-detail.tsx new file mode 100644 index 0000000000..0d93790b5a --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-detail/customer-group-detail.tsx @@ -0,0 +1,40 @@ +import { useAdminCustomerGroup } from "medusa-react" +import { Outlet, json, useLoaderData, useParams } from "react-router-dom" +import { JsonViewSection } from "../../../components/common/json-view-section" +import { CustomerGroupCustomerSection } from "./components/customer-group-customer-section" +import { CustomerGroupGeneralSection } from "./components/customer-group-general-section" +import { customerGroupLoader } from "./loader" + +export const CustomerGroupDetail = () => { + const initialData = useLoaderData() as Awaited< + ReturnType + > + + const { id } = useParams() + const { customer_group, isLoading, isError, error } = useAdminCustomerGroup( + id!, + undefined, + { initialData } + ) + + if (isLoading) { + return
Loading...
+ } + + if (isError || !customer_group) { + if (error) { + throw error + } + + throw json("An unknown error occurred", 500) + } + + return ( +
+ + + + +
+ ) +} diff --git a/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-detail/index.ts b/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-detail/index.ts new file mode 100644 index 0000000000..c1f4e53399 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-detail/index.ts @@ -0,0 +1,2 @@ +export { CustomerGroupDetail as Component } from "./customer-group-detail" +export { customerGroupLoader as loader } from "./loader" diff --git a/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-detail/loader.ts b/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-detail/loader.ts new file mode 100644 index 0000000000..1c8c2beb6b --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-detail/loader.ts @@ -0,0 +1,22 @@ +import { AdminCustomerGroupsRes } from "@medusajs/medusa" +import { Response } from "@medusajs/medusa-js" +import { adminProductKeys } from "medusa-react" +import { LoaderFunctionArgs } from "react-router-dom" + +import { medusa, queryClient } from "../../../lib/medusa" + +const customerGroupDetailQuery = (id: string) => ({ + queryKey: adminProductKeys.detail(id), + queryFn: async () => medusa.admin.customerGroups.retrieve(id), +}) + +export const customerGroupLoader = async ({ params }: LoaderFunctionArgs) => { + const id = params.id + const query = customerGroupDetailQuery(id!) + + return ( + queryClient.getQueryData>( + query.queryKey + ) ?? (await queryClient.fetchQuery(query)) + ) +} diff --git a/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-edit/components/edit-customer-group-form/edit-customer-group-form.tsx b/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-edit/components/edit-customer-group-form/edit-customer-group-form.tsx new file mode 100644 index 0000000000..0e034ba603 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-edit/components/edit-customer-group-form/edit-customer-group-form.tsx @@ -0,0 +1,91 @@ +import { zodResolver } from "@hookform/resolvers/zod" +import { CustomerGroup } from "@medusajs/medusa" +import { Button, Drawer, Input } from "@medusajs/ui" +import { useAdminUpdateCustomerGroup } from "medusa-react" +import { useEffect } from "react" +import { useForm } from "react-hook-form" +import { useTranslation } from "react-i18next" +import * as z from "zod" +import { Form } from "../../../../../components/common/form" + +type EditCustomerGroupFormProps = { + group: CustomerGroup + onSuccessfulSubmit: () => void + subscribe: (state: boolean) => void +} + +const EditCustomerGroupSchema = z.object({ + name: z.string().min(1), +}) + +export const EditCustomerGroupForm = ({ + group, + onSuccessfulSubmit, + subscribe, +}: EditCustomerGroupFormProps) => { + const { t } = useTranslation() + + const form = useForm>({ + defaultValues: { + name: group.name || "", + }, + resolver: zodResolver(EditCustomerGroupSchema), + }) + + const { + formState: { isDirty }, + } = form + + useEffect(() => { + subscribe(isDirty) + }, [isDirty]) + + const { mutateAsync, isLoading } = useAdminUpdateCustomerGroup(group.id) + + const handleSubmit = form.handleSubmit(async (data) => { + await mutateAsync(data, { + onSuccess: () => { + onSuccessfulSubmit() + }, + }) + }) + + return ( +
+ + + { + return ( + + {t("fields.name")} + + + + + + ) + }} + /> + + +
+ + + + +
+
+
+ + ) +} diff --git a/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-edit/components/edit-customer-group-form/index.ts b/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-edit/components/edit-customer-group-form/index.ts new file mode 100644 index 0000000000..3062389f75 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-edit/components/edit-customer-group-form/index.ts @@ -0,0 +1 @@ +export * from "./edit-customer-group-form" diff --git a/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-edit/customer-group-edit.tsx b/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-edit/customer-group-edit.tsx new file mode 100644 index 0000000000..0e43bdd314 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-edit/customer-group-edit.tsx @@ -0,0 +1,42 @@ +import { Drawer, Heading } from "@medusajs/ui" +import { useAdminCustomerGroup } from "medusa-react" +import { useTranslation } from "react-i18next" +import { useParams } from "react-router-dom" +import { useRouteModalState } from "../../../hooks/use-route-modal-state" +import { EditCustomerGroupForm } from "./components/edit-customer-group-form" + +export const CustomerGroupEdit = () => { + const [open, onOpenChange, subscribe] = useRouteModalState() + + const { id } = useParams() + const { customer_group, isLoading, isError, error } = useAdminCustomerGroup( + id! + ) + + const { t } = useTranslation() + + const handleSuccessfulSubmit = () => { + onOpenChange(false, true) + } + + if (isError) { + throw error + } + + return ( + + + + {t("customerGroups.editCustomerGroup")} + + {!isLoading && customer_group && ( + + )} + + + ) +} diff --git a/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-edit/index.ts b/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-edit/index.ts new file mode 100644 index 0000000000..edb805c0ad --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-edit/index.ts @@ -0,0 +1 @@ +export { CustomerGroupEdit as Component } from "./customer-group-edit" diff --git a/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-list/components/customer-group-list-table/customer-group-list-table.tsx b/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-list/components/customer-group-list-table/customer-group-list-table.tsx new file mode 100644 index 0000000000..b090df3679 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-list/components/customer-group-list-table/customer-group-list-table.tsx @@ -0,0 +1,251 @@ +import { PencilSquare, Trash } from "@medusajs/icons" +import { CustomerGroup } from "@medusajs/medusa" +import { Button, Container, Heading, Table, clx, usePrompt } from "@medusajs/ui" +import { + PaginationState, + RowSelectionState, + createColumnHelper, + flexRender, + getCoreRowModel, + useReactTable, +} from "@tanstack/react-table" +import { + useAdminCustomerGroups, + useAdminDeleteCustomerGroup, +} from "medusa-react" +import { useMemo, useState } from "react" +import { useTranslation } from "react-i18next" +import { Link, useNavigate } from "react-router-dom" +import { + NoRecords, + NoResults, +} from "../../../../../components/common/empty-table-content" +import { TableRowActions } from "../../../../../components/common/table-row-actions" +import { OrderBy } from "../../../../../components/filtering/order-by" +import { Query } from "../../../../../components/filtering/query" +import { LocalizedTablePagination } from "../../../../../components/localization/localized-table-pagination" +import { useQueryParams } from "../../../../../hooks/use-query-params" + +const PAGE_SIZE = 50 + +export const CustomerGroupListTable = () => { + const navigate = useNavigate() + const { t } = useTranslation() + + const [{ pageIndex, pageSize }, setPagination] = useState({ + pageIndex: 0, + pageSize: PAGE_SIZE, + }) + + const pagination = useMemo( + () => ({ + pageIndex, + pageSize, + }), + [pageIndex, pageSize] + ) + + const [rowSelection, setRowSelection] = useState({}) + + const params = useQueryParams(["q", "order"]) + const { customer_groups, count, isLoading, isError, error } = + useAdminCustomerGroups({ + limit: PAGE_SIZE, + offset: pageIndex * PAGE_SIZE, + ...params, + }) + + const columns = useColumns() + + const table = useReactTable({ + data: customer_groups ?? [], + columns, + pageCount: Math.ceil((count ?? 0) / PAGE_SIZE), + state: { + pagination, + rowSelection, + }, + onPaginationChange: setPagination, + onRowSelectionChange: setRowSelection, + getCoreRowModel: getCoreRowModel(), + manualPagination: true, + }) + + const noRecords = + !isLoading && + !customer_groups?.length && + !Object.values(params).filter(Boolean).length + + if (isError) { + throw error + } + + return ( + +
+ {t("customerGroups.domain")} + + + +
+
+ {noRecords ? ( + + ) : ( +
+
+
+
+ + +
+
+
+ {(customer_groups?.length || 0) > 0 ? ( + + + {table.getHeaderGroups().map((headerGroup) => { + return ( + + {headerGroup.headers.map((header) => { + return ( + + {flexRender( + header.column.columnDef.header, + header.getContext() + )} + + ) + })} + + ) + })} + + + {table.getRowModel().rows.map((row) => ( + + navigate(`/customer-groups/${row.original.id}`) + } + > + {row.getVisibleCells().map((cell) => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} + + ))} + + ))} + +
+ ) : ( +
+ +
+ )} + +
+
+ )} +
+
+ ) +} + +const CustomerGroupActions = ({ group }: { group: CustomerGroup }) => { + const { t } = useTranslation() + const prompt = usePrompt() + + const { mutateAsync } = useAdminDeleteCustomerGroup(group.id) + + const handleDelete = async () => { + const res = await prompt({ + title: t("general.areYouSure"), + description: t("customerGroups.deleteCustomerGroupWarning", { + name: group.name, + }), + confirmText: t("general.delete"), + cancelText: t("general.cancel"), + }) + + if (!res) { + return + } + + await mutateAsync(undefined) + } + + return ( + , + label: t("general.edit"), + to: `/customer-groups/${group.id}/edit`, + }, + ], + }, + { + actions: [ + { + icon: , + label: t("general.delete"), + onClick: handleDelete, + }, + ], + }, + ]} + /> + ) +} + +const columnHelper = createColumnHelper() + +const useColumns = () => { + const { t } = useTranslation() + + return useMemo( + () => [ + columnHelper.accessor("name", { + header: t("fields.name"), + cell: ({ getValue }) => getValue(), + }), + columnHelper.display({ + id: "actions", + cell: ({ row }) => , + }), + ], + [t] + ) +} diff --git a/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-list/components/customer-group-list-table/index.ts b/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-list/components/customer-group-list-table/index.ts new file mode 100644 index 0000000000..ead4d74743 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-list/components/customer-group-list-table/index.ts @@ -0,0 +1 @@ +export * from "./customer-group-list-table" diff --git a/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-list/customer-group-list.tsx b/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-list/customer-group-list.tsx new file mode 100644 index 0000000000..3e3786160a --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-list/customer-group-list.tsx @@ -0,0 +1,11 @@ +import { Outlet } from "react-router-dom" +import { CustomerGroupListTable } from "./components/customer-group-list-table" + +export const CustomerGroupsList = () => { + return ( +
+ + +
+ ) +} diff --git a/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-list/index.ts b/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-list/index.ts new file mode 100644 index 0000000000..f04a72f6dd --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-list/index.ts @@ -0,0 +1 @@ +export { CustomerGroupsList as Component } from "./customer-group-list" diff --git a/packages/admin-next/dashboard/src/routes/customer-groups/details/details.tsx b/packages/admin-next/dashboard/src/routes/customer-groups/details/details.tsx deleted file mode 100644 index cc59b9ecdc..0000000000 --- a/packages/admin-next/dashboard/src/routes/customer-groups/details/details.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { Container, Heading } from "@medusajs/ui"; - -export const CustomerGroupDetails = () => { - return ( -
- - Customers - -
- ); -}; diff --git a/packages/admin-next/dashboard/src/routes/customer-groups/details/index.ts b/packages/admin-next/dashboard/src/routes/customer-groups/details/index.ts deleted file mode 100644 index bb5e10a796..0000000000 --- a/packages/admin-next/dashboard/src/routes/customer-groups/details/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { CustomerGroupDetails as Component } from "./details"; diff --git a/packages/admin-next/dashboard/src/routes/customer-groups/list/index.ts b/packages/admin-next/dashboard/src/routes/customer-groups/list/index.ts deleted file mode 100644 index c9929eedb7..0000000000 --- a/packages/admin-next/dashboard/src/routes/customer-groups/list/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { CustomerGroupsList as Component } from "./list"; diff --git a/packages/admin-next/dashboard/src/routes/customer-groups/list/list.tsx b/packages/admin-next/dashboard/src/routes/customer-groups/list/list.tsx deleted file mode 100644 index 6d5f9e292b..0000000000 --- a/packages/admin-next/dashboard/src/routes/customer-groups/list/list.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { Container, Heading } from "@medusajs/ui"; - -export const CustomerGroupsList = () => { - return ( -
- - Customer Groups - -
- ); -}; diff --git a/packages/admin-next/dashboard/src/routes/customers/customer-create/components/create-customer-form/create-customer-form.tsx b/packages/admin-next/dashboard/src/routes/customers/customer-create/components/create-customer-form/create-customer-form.tsx new file mode 100644 index 0000000000..067da3b031 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/customers/customer-create/components/create-customer-form/create-customer-form.tsx @@ -0,0 +1,221 @@ +import { zodResolver } from "@hookform/resolvers/zod" +import { Button, FocusModal, Heading, Input, Text } from "@medusajs/ui" +import { useAdminCreateCustomer } from "medusa-react" +import { useEffect } from "react" +import { useForm } from "react-hook-form" +import { useTranslation } from "react-i18next" +import { useNavigate } from "react-router-dom" +import * as zod from "zod" +import { Form } from "../../../../../components/common/form" + +type CreateCustomerFormProps = { + subscribe: (state: boolean) => void +} + +const CreateCustomerSchema = zod + .object({ + email: zod.string().email(), + first_name: zod.string().min(1), + last_name: zod.string().min(1), + phone: zod.string().min(1).optional(), + password: zod.string().min(8), + password_confirmation: zod.string().min(8), + }) + .superRefine(({ password, password_confirmation }, ctx) => { + if (password !== password_confirmation) { + return ctx.addIssue({ + code: zod.ZodIssueCode.custom, + message: "Passwords do not match", + path: ["password_confirmation"], + }) + } + }) + +export const CreateCustomerForm = ({ subscribe }: CreateCustomerFormProps) => { + const { t } = useTranslation() + const navigate = useNavigate() + + const form = useForm>({ + defaultValues: { + email: "", + first_name: "", + last_name: "", + phone: "", + password: "", + password_confirmation: "", + }, + resolver: zodResolver(CreateCustomerSchema), + }) + const { + formState: { isDirty }, + } = form + + useEffect(() => { + subscribe(isDirty) + }, [isDirty]) + + const { mutateAsync, isLoading } = useAdminCreateCustomer() + + const handleSubmit = form.handleSubmit(async (data) => { + await mutateAsync( + { + email: data.email, + first_name: data.first_name, + last_name: data.last_name, + phone: data.phone, + password: data.password, + }, + { + onSuccess: ({ customer }) => { + navigate(`/customers/${customer.id}`, { replace: true }) + }, + } + ) + }) + + return ( +
+ + +
+ + + + +
+
+ +
+
+ {t("customers.createCustomer")} + + {t("customers.createCustomerHint")} + +
+
+ { + return ( + + {t("fields.firstName")} + + + + + + ) + }} + /> + { + return ( + + {t("fields.lastName")} + + + + + + ) + }} + /> + { + return ( + + {t("fields.email")} + + + + + + ) + }} + /> + { + return ( + + {t("fields.phone")} + + + + + + ) + }} + /> +
+
+
+ + {t("fields.password")} + + + {t("customers.passwordHint")} + +
+
+ { + return ( + + {t("fields.password")} + + + + + + ) + }} + /> + { + return ( + + {t("fields.confirmPassword")} + + + + + + ) + }} + /> +
+
+
+
+
+ + ) +} diff --git a/packages/admin-next/dashboard/src/routes/customers/customer-create/components/create-customer-form/index.ts b/packages/admin-next/dashboard/src/routes/customers/customer-create/components/create-customer-form/index.ts new file mode 100644 index 0000000000..e3a418be98 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/customers/customer-create/components/create-customer-form/index.ts @@ -0,0 +1 @@ +export * from "./create-customer-form" diff --git a/packages/admin-next/dashboard/src/routes/customers/customer-create/customer-create.tsx b/packages/admin-next/dashboard/src/routes/customers/customer-create/customer-create.tsx new file mode 100644 index 0000000000..9d28afb2b2 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/customers/customer-create/customer-create.tsx @@ -0,0 +1,15 @@ +import { FocusModal } from "@medusajs/ui" +import { useRouteModalState } from "../../../hooks/use-route-modal-state" +import { CreateCustomerForm } from "./components/create-customer-form" + +export const CustomerCreate = () => { + const [open, onOpenChange, subscribe] = useRouteModalState() + + return ( + + + + + + ) +} diff --git a/packages/admin-next/dashboard/src/routes/customers/customer-create/index.ts b/packages/admin-next/dashboard/src/routes/customers/customer-create/index.ts new file mode 100644 index 0000000000..78c115ec15 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/customers/customer-create/index.ts @@ -0,0 +1 @@ +export { CustomerCreate as Component } from "./customer-create" diff --git a/packages/admin-next/dashboard/src/routes/customers/customer-detail/components/customer-general-section/customer-general-section.tsx b/packages/admin-next/dashboard/src/routes/customers/customer-detail/components/customer-general-section/customer-general-section.tsx new file mode 100644 index 0000000000..1fc3b5af2b --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/customers/customer-detail/components/customer-general-section/customer-general-section.tsx @@ -0,0 +1,73 @@ +import { Customer } from "@medusajs/medusa" +import { Button, Container, Heading, Text } from "@medusajs/ui" +import format from "date-fns/format" +import { ReactNode } from "react" +import { useTranslation } from "react-i18next" +import { Link } from "react-router-dom" + +type CustomerGeneralSectionProps = { + customer: Customer +} + +export const CustomerGeneralSection = ({ + customer, +}: CustomerGeneralSectionProps) => { + const { t } = useTranslation() + + return ( + +
+ {customer.email} + + + +
+
+ + + + + +
+
+ ) +} + +const Bulletpoint = ({ title, value }: { title: string; value: ReactNode }) => { + return ( +
+ + {title} + +
{value ?? "-"}
+
+ ) +} diff --git a/packages/admin-next/dashboard/src/routes/customers/customer-detail/components/customer-general-section/index.ts b/packages/admin-next/dashboard/src/routes/customers/customer-detail/components/customer-general-section/index.ts new file mode 100644 index 0000000000..ef9c4e73b9 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/customers/customer-detail/components/customer-general-section/index.ts @@ -0,0 +1 @@ +export * from "./customer-general-section" diff --git a/packages/admin-next/dashboard/src/routes/customers/customer-detail/components/customer-group-section/customer-group-section.tsx b/packages/admin-next/dashboard/src/routes/customers/customer-detail/components/customer-group-section/customer-group-section.tsx new file mode 100644 index 0000000000..db845763c0 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/customers/customer-detail/components/customer-group-section/customer-group-section.tsx @@ -0,0 +1,52 @@ +import { Customer, CustomerGroup } from "@medusajs/medusa" +import { Container, Heading } from "@medusajs/ui" +import { createColumnHelper } from "@tanstack/react-table" +import { useAdminCustomerGroups } from "medusa-react" +import { useMemo } from "react" +import { useTranslation } from "react-i18next" + +// TODO: Continue working on this when there is a natural way to get customer groups related to a customer. +type CustomerGroupSectionProps = { + customer: Customer +} + +export const CustomerGroupSection = ({ + customer, +}: CustomerGroupSectionProps) => { + const { customer_groups, isLoading, isError, error } = useAdminCustomerGroups( + { + id: customer.groups.map((g) => g.id).join(","), + } + ) + + if (isError) { + throw error + } + + return ( + +
+ Groups +
+
+ ) +} + +const columnHelper = createColumnHelper() + +const useColumns = () => { + const { t } = useTranslation() + + return useMemo( + () => [ + columnHelper.display({ + id: "select", + }), + columnHelper.accessor("name", { + header: t("fields.name"), + cell: ({ getValue }) => getValue(), + }), + ], + [t] + ) +} diff --git a/packages/admin-next/dashboard/src/routes/customers/customer-detail/components/customer-group-section/index.ts b/packages/admin-next/dashboard/src/routes/customers/customer-detail/components/customer-group-section/index.ts new file mode 100644 index 0000000000..421addaae6 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/customers/customer-detail/components/customer-group-section/index.ts @@ -0,0 +1 @@ +export * from "./customer-group-section" diff --git a/packages/admin-next/dashboard/src/routes/customers/customer-detail/components/customer-order-section/customer-order-section.tsx b/packages/admin-next/dashboard/src/routes/customers/customer-detail/components/customer-order-section/customer-order-section.tsx new file mode 100644 index 0000000000..adc3b72ec9 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/customers/customer-detail/components/customer-order-section/customer-order-section.tsx @@ -0,0 +1,247 @@ +import { EllipsisHorizontal, ReceiptPercent } from "@medusajs/icons" +import { Customer, Order } from "@medusajs/medusa" +import { + Button, + Container, + DropdownMenu, + Heading, + IconButton, + Table, + clx, +} from "@medusajs/ui" +import { + PaginationState, + RowSelectionState, + createColumnHelper, + flexRender, + getCoreRowModel, + useReactTable, +} from "@tanstack/react-table" +import { useAdminOrders } 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 { + OrderDateCell, + OrderDisplayIdCell, + OrderFulfillmentStatusCell, + OrderPaymentStatusCell, + OrderTotalCell, +} from "../../../../../components/common/order-table-cells" +import { Query } from "../../../../../components/filtering/query" +import { LocalizedTablePagination } from "../../../../../components/localization/localized-table-pagination" +import { useQueryParams } from "../../../../../hooks/use-query-params" + +type CustomerGeneralSectionProps = { + customer: Customer +} + +const PAGE_SIZE = 10 + +export const CustomerOrderSection = ({ + customer, +}: CustomerGeneralSectionProps) => { + const { t } = useTranslation() + const navigate = useNavigate() + + const [{ pageIndex, pageSize }, setPagination] = useState({ + pageIndex: 0, + pageSize: PAGE_SIZE, + }) + + const pagination = useMemo( + () => ({ + pageIndex, + pageSize, + }), + [pageIndex, pageSize] + ) + + const [rowSelection, setRowSelection] = useState({}) + + const params = useQueryParams(["q"]) + const { orders, count, isLoading, isError, error } = useAdminOrders( + { + customer_id: customer.id, + limit: PAGE_SIZE, + offset: pageIndex * PAGE_SIZE, + fields: + "id,status,display_id,created_at,email,fulfillment_status,payment_status,total,currency_code", + ...params, + }, + { + keepPreviousData: true, + } + ) + + const columns = useColumns() + + const table = useReactTable({ + data: orders ?? [], + columns, + pageCount: Math.ceil((count ?? 0) / PAGE_SIZE), + state: { + pagination, + rowSelection, + }, + onPaginationChange: setPagination, + onRowSelectionChange: setRowSelection, + getCoreRowModel: getCoreRowModel(), + manualPagination: true, + }) + + const noRecords = + Object.values(params).every((v) => !v) && !isLoading && !orders?.length + + if (isError) { + throw error + } + + return ( + +
+ {t("orders.domain")} +
+ +
+
+ {!noRecords && ( +
+
+
+ +
+
+ )} + {noRecords ? ( + + ) : ( +
+ + + {table.getHeaderGroups().map((headerGroup) => { + return ( + + {headerGroup.headers.map((header) => { + return ( + + {flexRender( + header.column.columnDef.header, + header.getContext() + )} + + ) + })} + + ) + })} + + + {table.getRowModel().rows.map((row) => ( + navigate(`/orders/${row.original.id}`)} + > + {row.getVisibleCells().map((cell) => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} + + ))} + + ))} + +
+ +
+ )} +
+ ) +} + +const OrderActions = ({ order }: { order: Order }) => { + return ( + + + + + + + + + + + Go to order + + + + + ) +} + +const columnHelper = createColumnHelper() + +const useColumns = () => { + const { t } = useTranslation() + + return useMemo( + () => [ + columnHelper.accessor("display_id", { + header: "Order", + cell: ({ getValue }) => , + }), + columnHelper.accessor("created_at", { + header: "Date", + cell: ({ getValue }) => , + }), + columnHelper.accessor("fulfillment_status", { + header: "Fulfillment Status", + cell: ({ getValue }) => ( + + ), + }), + columnHelper.accessor("payment_status", { + header: "Payment Status", + cell: ({ getValue }) => , + }), + columnHelper.accessor("total", { + header: () => t("fields.total"), + cell: ({ getValue, row }) => ( + + ), + }), + columnHelper.display({ + id: "actions", + cell: ({ row }) => , + }), + ], + [t] + ) +} diff --git a/packages/admin-next/dashboard/src/routes/customers/customer-detail/components/customer-order-section/index.ts b/packages/admin-next/dashboard/src/routes/customers/customer-detail/components/customer-order-section/index.ts new file mode 100644 index 0000000000..bd72160c52 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/customers/customer-detail/components/customer-order-section/index.ts @@ -0,0 +1 @@ +export * from "./customer-order-section" diff --git a/packages/admin-next/dashboard/src/routes/customers/customer-detail/customer-detail.tsx b/packages/admin-next/dashboard/src/routes/customers/customer-detail/customer-detail.tsx new file mode 100644 index 0000000000..a2e9fd338c --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/customers/customer-detail/customer-detail.tsx @@ -0,0 +1,39 @@ +import { useAdminCustomer } from "medusa-react" +import { Outlet, json, useLoaderData, useParams } from "react-router-dom" +import { JsonViewSection } from "../../../components/common/json-view-section" +import { CustomerGeneralSection } from "./components/customer-general-section" +import { CustomerOrderSection } from "./components/customer-order-section" +import { customerLoader } from "./loader" + +export const CustomerDetail = () => { + const { id } = useParams() + + const initialData = useLoaderData() as Awaited< + ReturnType + > + const { customer, isLoading, isError, error } = useAdminCustomer(id!, { + initialData, + }) + + if (isLoading) { + return
Loading...
+ } + + if (isError || !customer) { + if (error) { + throw error + } + + throw json("An unknown error occurred", 500) + } + + return ( +
+ + + {/* */} + + +
+ ) +} diff --git a/packages/admin-next/dashboard/src/routes/customers/customer-detail/index.ts b/packages/admin-next/dashboard/src/routes/customers/customer-detail/index.ts new file mode 100644 index 0000000000..bed23b93e7 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/customers/customer-detail/index.ts @@ -0,0 +1,2 @@ +export { CustomerDetail as Component } from "./customer-detail" +export { customerLoader as loader } from "./loader" diff --git a/packages/admin-next/dashboard/src/routes/customers/customer-detail/loader.ts b/packages/admin-next/dashboard/src/routes/customers/customer-detail/loader.ts new file mode 100644 index 0000000000..25d7f0c284 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/customers/customer-detail/loader.ts @@ -0,0 +1,21 @@ +import { AdminCustomersRes } from "@medusajs/medusa" +import { Response } from "@medusajs/medusa-js" +import { adminProductKeys } from "medusa-react" +import { LoaderFunctionArgs } from "react-router-dom" + +import { medusa, queryClient } from "../../../lib/medusa" + +const customerDetailQuery = (id: string) => ({ + queryKey: adminProductKeys.detail(id), + queryFn: async () => medusa.admin.customers.retrieve(id), +}) + +export const customerLoader = async ({ params }: LoaderFunctionArgs) => { + const id = params.id + const query = customerDetailQuery(id!) + + return ( + queryClient.getQueryData>(query.queryKey) ?? + (await queryClient.fetchQuery(query)) + ) +} diff --git a/packages/admin-next/dashboard/src/routes/customers/customer-edit/components/edit-customer-form/edit-customer-form.tsx b/packages/admin-next/dashboard/src/routes/customers/customer-edit/components/edit-customer-form/edit-customer-form.tsx new file mode 100644 index 0000000000..8b1cbc21d1 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/customers/customer-edit/components/edit-customer-form/edit-customer-form.tsx @@ -0,0 +1,154 @@ +import { zodResolver } from "@hookform/resolvers/zod" +import { Customer } from "@medusajs/medusa" +import { Button, Drawer, Input } from "@medusajs/ui" +import { useAdminUpdateCustomer } from "medusa-react" +import { useEffect } from "react" +import { useForm } from "react-hook-form" +import { useTranslation } from "react-i18next" +import * as zod from "zod" +import { Form } from "../../../../../components/common/form" + +type EditCustomerFormProps = { + customer: Customer + subscribe: (state: boolean) => void + onSuccessfulSubmit: () => void +} + +const EditCustomerSchema = zod.object({ + email: zod.string().email(), + first_name: zod.string().min(1).optional(), + last_name: zod.string().min(1).optional(), + phone: zod.string().optional(), +}) + +export const EditCustomerForm = ({ + customer, + subscribe, + onSuccessfulSubmit, +}: EditCustomerFormProps) => { + const { t } = useTranslation() + + const form = useForm>({ + defaultValues: { + email: customer.email || "", + first_name: customer.first_name || "", + last_name: customer.last_name || "", + phone: customer.phone || "", + }, + resolver: zodResolver(EditCustomerSchema), + }) + + const { + formState: { isDirty }, + } = form + + useEffect(() => { + subscribe(isDirty) + }, [isDirty]) + + const { mutateAsync, isLoading } = useAdminUpdateCustomer(customer.id) + + const handleSubmit = form.handleSubmit(async (data) => { + await mutateAsync( + { + email: customer.has_account ? undefined : data.email, + first_name: data.first_name, + last_name: data.last_name, + phone: data.phone, + }, + { + onSuccess: () => { + onSuccessfulSubmit() + }, + } + ) + }) + + return ( +
+ + +
+ { + return ( + + {t("fields.email")} + + + + + + ) + }} + /> + { + return ( + + {t("fields.firstName")} + + + + + + ) + }} + /> + { + return ( + + {t("fields.lastName")} + + + + + + ) + }} + /> + { + return ( + + {t("fields.phone")} + + + + + + ) + }} + /> +
+
+ +
+ + + + +
+
+
+ + ) +} diff --git a/packages/admin-next/dashboard/src/routes/customers/customer-edit/components/edit-customer-form/index.ts b/packages/admin-next/dashboard/src/routes/customers/customer-edit/components/edit-customer-form/index.ts new file mode 100644 index 0000000000..a8ded89ad0 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/customers/customer-edit/components/edit-customer-form/index.ts @@ -0,0 +1 @@ +export * from "./edit-customer-form" diff --git a/packages/admin-next/dashboard/src/routes/customers/customer-edit/customer-edit.tsx b/packages/admin-next/dashboard/src/routes/customers/customer-edit/customer-edit.tsx new file mode 100644 index 0000000000..a453f1152e --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/customers/customer-edit/customer-edit.tsx @@ -0,0 +1,39 @@ +import { Drawer, Heading } from "@medusajs/ui" +import { useAdminCustomer } from "medusa-react" +import { useTranslation } from "react-i18next" +import { useParams } from "react-router-dom" +import { useRouteModalState } from "../../../hooks/use-route-modal-state" +import { EditCustomerForm } from "./components/edit-customer-form" + +export const CustomerEdit = () => { + const [open, onOpenChange, subscribe] = useRouteModalState() + const { t } = useTranslation() + + const { id } = useParams() + const { customer, isLoading, isError, error } = useAdminCustomer(id!) + + const handleSuccessfulSubmit = () => { + onOpenChange(false, true) + } + + if (isError) { + throw error + } + + return ( + + + + {t("customers.editCustomer")} + + {!isLoading && customer && ( + + )} + + + ) +} diff --git a/packages/admin-next/dashboard/src/routes/customers/customer-edit/index.ts b/packages/admin-next/dashboard/src/routes/customers/customer-edit/index.ts new file mode 100644 index 0000000000..d70310f5ae --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/customers/customer-edit/index.ts @@ -0,0 +1 @@ +export { CustomerEdit as Component } from "./customer-edit" diff --git a/packages/admin-next/dashboard/src/routes/customers/customer-list/components/customer-list-table/customer-list-table.tsx b/packages/admin-next/dashboard/src/routes/customers/customer-list/components/customer-list-table/customer-list-table.tsx new file mode 100644 index 0000000000..f43a9c5aa4 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/customers/customer-list/components/customer-list-table/customer-list-table.tsx @@ -0,0 +1,219 @@ +import { EllipsisHorizontal, PencilSquare } from "@medusajs/icons" +import { Customer } from "@medusajs/medusa" +import { + Button, + Container, + DropdownMenu, + Heading, + IconButton, + StatusBadge, + Table, + clx, +} from "@medusajs/ui" +import { + PaginationState, + RowSelectionState, + createColumnHelper, + flexRender, + getCoreRowModel, + useReactTable, +} from "@tanstack/react-table" +import { useAdminCustomers } from "medusa-react" +import { useMemo, useState } from "react" +import { useTranslation } from "react-i18next" +import { Link, useNavigate } from "react-router-dom" +import { Query } from "../../../../../components/filtering/query" +import { LocalizedTablePagination } from "../../../../../components/localization/localized-table-pagination" +import { useQueryParams } from "../../../../../hooks/use-query-params" + +const PAGE_SIZE = 50 + +export const CustomerListTable = () => { + const { t } = useTranslation() + const navigate = useNavigate() + + const [{ pageIndex, pageSize }, setPagination] = useState({ + pageIndex: 0, + pageSize: PAGE_SIZE, + }) + + const pagination = useMemo( + () => ({ + pageIndex, + pageSize, + }), + [pageIndex, pageSize] + ) + + const [rowSelection, setRowSelection] = useState({}) + + const { q } = useQueryParams(["q"]) + const { customers, count, isLoading, isError, error } = useAdminCustomers({ + q, + limit: PAGE_SIZE, + offset: pageIndex * PAGE_SIZE, + }) + + const columns = useColumns() + + const table = useReactTable({ + data: customers ?? [], + columns, + pageCount: Math.ceil((count ?? 0) / PAGE_SIZE), + state: { + pagination, + rowSelection, + }, + onPaginationChange: setPagination, + onRowSelectionChange: setRowSelection, + getCoreRowModel: getCoreRowModel(), + manualPagination: true, + }) + + if (isError) { + throw error + } + + return ( + +
+ {t("customers.domain")} + + + +
+
+
+
+ +
+
+
+ + + {table.getHeaderGroups().map((headerGroup) => { + return ( + + {headerGroup.headers.map((header) => { + return ( + + {flexRender( + header.column.columnDef.header, + header.getContext() + )} + + ) + })} + + ) + })} + + + {table.getRowModel().rows.map((row) => ( + navigate(`/customers/${row.original.id}`)} + > + {row.getVisibleCells().map((cell) => ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} + + ))} + +
+ +
+
+ ) +} + +const CustomerActions = ({ customer }: { customer: Customer }) => { + const { t } = useTranslation() + + return ( + + + + + + + + + e.stopPropagation()} + > + + {t("general.edit")} + + + + + ) +} + +const columnHelper = createColumnHelper() + +const useColumns = () => { + const { t } = useTranslation() + + return useMemo( + () => [ + columnHelper.accessor("email", { + header: t("fields.email"), + cell: ({ getValue }) => {getValue()}, + }), + columnHelper.display({ + id: "name", + header: t("fields.name"), + cell: ({ row }) => { + const name = [row.original.first_name, row.original.last_name] + .filter(Boolean) + .join(" ") + + return name || "-" + }, + }), + columnHelper.accessor("has_account", { + header: t("fields.account"), + cell: ({ getValue }) => { + const hasAccount = getValue() + + return ( + + {hasAccount ? t("customers.registered") : t("customers.guest")} + + ) + }, + }), + columnHelper.display({ + id: "actions", + cell: ({ row }) => , + }), + ], + [t] + ) +} diff --git a/packages/admin-next/dashboard/src/routes/customers/customer-list/components/customer-list-table/index.ts b/packages/admin-next/dashboard/src/routes/customers/customer-list/components/customer-list-table/index.ts new file mode 100644 index 0000000000..c2e0ca7b09 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/customers/customer-list/components/customer-list-table/index.ts @@ -0,0 +1 @@ +export * from "./customer-list-table" diff --git a/packages/admin-next/dashboard/src/routes/customers/customer-list/customer-list.tsx b/packages/admin-next/dashboard/src/routes/customers/customer-list/customer-list.tsx new file mode 100644 index 0000000000..4a475d2450 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/customers/customer-list/customer-list.tsx @@ -0,0 +1,11 @@ +import { Outlet } from "react-router-dom" +import { CustomerListTable } from "./components/customer-list-table" + +export const CustomersList = () => { + return ( +
+ + +
+ ) +} diff --git a/packages/admin-next/dashboard/src/routes/customers/customer-list/index.ts b/packages/admin-next/dashboard/src/routes/customers/customer-list/index.ts new file mode 100644 index 0000000000..f945dbca5d --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/customers/customer-list/index.ts @@ -0,0 +1 @@ +export { CustomersList as Component } from "./customer-list" diff --git a/packages/admin-next/dashboard/src/routes/customers/details/details.tsx b/packages/admin-next/dashboard/src/routes/customers/details/details.tsx deleted file mode 100644 index 542e709a0b..0000000000 --- a/packages/admin-next/dashboard/src/routes/customers/details/details.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { Container, Heading } from "@medusajs/ui"; - -export const CustomerDetails = () => { - return ( -
- - Customers - -
- ); -}; diff --git a/packages/admin-next/dashboard/src/routes/customers/details/index.ts b/packages/admin-next/dashboard/src/routes/customers/details/index.ts deleted file mode 100644 index 884ed5286c..0000000000 --- a/packages/admin-next/dashboard/src/routes/customers/details/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { CustomerDetails as Component } from "./details"; diff --git a/packages/admin-next/dashboard/src/routes/customers/list/index.ts b/packages/admin-next/dashboard/src/routes/customers/list/index.ts deleted file mode 100644 index a4826e8aba..0000000000 --- a/packages/admin-next/dashboard/src/routes/customers/list/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { CustomersList as Component } from "./list"; diff --git a/packages/admin-next/dashboard/src/routes/customers/list/list.tsx b/packages/admin-next/dashboard/src/routes/customers/list/list.tsx deleted file mode 100644 index 7fdc230dd9..0000000000 --- a/packages/admin-next/dashboard/src/routes/customers/list/list.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { Container, Heading } from "@medusajs/ui"; - -export const CustomersList = () => { - return ( -
- - Customers - -
- ); -}; diff --git a/packages/admin-next/dashboard/src/routes/locations/list/index.ts b/packages/admin-next/dashboard/src/routes/locations/list/index.ts deleted file mode 100644 index 4ede307bd7..0000000000 --- a/packages/admin-next/dashboard/src/routes/locations/list/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { LocationsList as Component } from "./list"; diff --git a/packages/admin-next/dashboard/src/routes/locations/list/list.tsx b/packages/admin-next/dashboard/src/routes/locations/list/list.tsx deleted file mode 100644 index 25a6d42694..0000000000 --- a/packages/admin-next/dashboard/src/routes/locations/list/list.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { Container, Heading } from "@medusajs/ui"; - -export const LocationsList = () => { - return ( -
- - Locations - -
- ); -}; diff --git a/packages/admin-next/dashboard/src/routes/locations/location-add-sales-channels/index.ts b/packages/admin-next/dashboard/src/routes/locations/location-add-sales-channels/index.ts new file mode 100644 index 0000000000..eb7ea74f11 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/locations/location-add-sales-channels/index.ts @@ -0,0 +1 @@ +export { LocationAddSalesChannels as Component } from "./location-add-sales-channels" diff --git a/packages/admin-next/dashboard/src/routes/locations/location-add-sales-channels/location-add-sales-channels.tsx b/packages/admin-next/dashboard/src/routes/locations/location-add-sales-channels/location-add-sales-channels.tsx new file mode 100644 index 0000000000..6f7d42bfeb --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/locations/location-add-sales-channels/location-add-sales-channels.tsx @@ -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 ( + + + + ) +} diff --git a/packages/admin-next/dashboard/src/routes/locations/location-create/components/create-location-form/create-location-form.tsx b/packages/admin-next/dashboard/src/routes/locations/location-create/components/create-location-form/create-location-form.tsx new file mode 100644 index 0000000000..5c8c6fff37 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/locations/location-create/components/create-location-form/create-location-form.tsx @@ -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>({ + 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 ( +
+ + +
+ + + + +
+
+ +
+
+
+ + {t("locations.createLocation")} + + + {t("locations.detailsHint")} + +
+
+ { + return ( + + {t("fields.name")} + + + + + + ) + }} + /> +
+
+ { + return ( + + {t("fields.address")} + + + + + + ) + }} + /> + { + return ( + + {t("fields.address2")} + + + + + + ) + }} + /> + { + return ( + + + {t("fields.postalCode")} + + + + + + + ) + }} + /> + { + return ( + + {t("fields.city")} + + + + + + ) + }} + /> + { + return ( + + {t("fields.country")} + + + + + + ) + }} + /> + { + return ( + + {t("fields.state")} + + + + + + ) + }} + /> + { + return ( + + {t("fields.company")} + + + + + + ) + }} + /> + { + return ( + + {t("fields.phone")} + + + + + + ) + }} + /> +
+
+
+
+
+ + ) +} diff --git a/packages/admin-next/dashboard/src/routes/locations/location-create/components/create-location-form/index.ts b/packages/admin-next/dashboard/src/routes/locations/location-create/components/create-location-form/index.ts new file mode 100644 index 0000000000..0803aab3f5 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/locations/location-create/components/create-location-form/index.ts @@ -0,0 +1 @@ +export * from "./create-location-form" diff --git a/packages/admin-next/dashboard/src/routes/locations/location-create/index.ts b/packages/admin-next/dashboard/src/routes/locations/location-create/index.ts new file mode 100644 index 0000000000..9e7d96209e --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/locations/location-create/index.ts @@ -0,0 +1 @@ +export { LocationCreate as Component } from "./location-create" diff --git a/packages/admin-next/dashboard/src/routes/locations/location-create/location-create.tsx b/packages/admin-next/dashboard/src/routes/locations/location-create/location-create.tsx new file mode 100644 index 0000000000..0369b258d6 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/locations/location-create/location-create.tsx @@ -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 ( + + + + + + ) +} diff --git a/packages/admin-next/dashboard/src/routes/locations/location-detail/components/location-general-section/index.ts b/packages/admin-next/dashboard/src/routes/locations/location-detail/components/location-general-section/index.ts new file mode 100644 index 0000000000..301849accd --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/locations/location-detail/components/location-general-section/index.ts @@ -0,0 +1 @@ +export * from "./location-general-section" diff --git a/packages/admin-next/dashboard/src/routes/locations/location-detail/components/location-general-section/location-general-section.tsx b/packages/admin-next/dashboard/src/routes/locations/location-detail/components/location-general-section/location-general-section.tsx new file mode 100644 index 0000000000..b12aea39ab --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/locations/location-detail/components/location-general-section/location-general-section.tsx @@ -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 ( + +
+ {location.name} + + + +
+
+ + {t("fields.address")} + + +
+
+ + {t("fields.company")} + + + {location.address?.company || "-"} + +
+
+ + {t("fields.phone")} + + + {location.address?.phone || "-"} + +
+
+ ) +} + +const AddressDisplay = ({ + address, +}: { + address: StockLocationAddressDTO | undefined +}) => { + if (!address) { + return ( + + - + + ) + } + + 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 {addressString} +} diff --git a/packages/admin-next/dashboard/src/routes/locations/location-detail/components/location-sales-channel-section/index.ts b/packages/admin-next/dashboard/src/routes/locations/location-detail/components/location-sales-channel-section/index.ts new file mode 100644 index 0000000000..6c16c11a32 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/locations/location-detail/components/location-sales-channel-section/index.ts @@ -0,0 +1 @@ +export * from "./location-sales-channel-section" diff --git a/packages/admin-next/dashboard/src/routes/sales-channels/views/sales-channel-list/sales-channel-list.tsx b/packages/admin-next/dashboard/src/routes/locations/location-detail/components/location-sales-channel-section/location-sales-channel-section.tsx similarity index 80% rename from packages/admin-next/dashboard/src/routes/sales-channels/views/sales-channel-list/sales-channel-list.tsx rename to packages/admin-next/dashboard/src/routes/locations/location-detail/components/location-sales-channel-section/location-sales-channel-section.tsx index 24c577883e..83c4f5f18a 100644 --- a/packages/admin-next/dashboard/src/routes/sales-channels/views/sales-channel-list/sales-channel-list.tsx +++ b/packages/admin-next/dashboard/src/routes/locations/location-detail/components/location-sales-channel-section/location-sales-channel-section.tsx @@ -1,6 +1,8 @@ import { EllipsisHorizontal, PencilSquare, Trash } from "@medusajs/icons" import { SalesChannel } from "@medusajs/medusa" +import { StockLocationExpandedDTO } from "@medusajs/types" import { + Button, Container, DropdownMenu, Heading, @@ -17,16 +19,22 @@ import { getCoreRowModel, useReactTable, } from "@tanstack/react-table" -import { useAdminDeleteSalesChannel, useAdminSalesChannels } from "medusa-react" import { useMemo, useState } from "react" 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 navigate = useNavigate() +const PAGE_SIZE = 20 + +export const LocationSalesChannelSection = ({ + location, +}: LocationSalesChannelSectionProps) => { const { t } = useTranslation() + const navigate = useNavigate() const [{ pageIndex, pageSize }, setPagination] = useState({ pageIndex: 0, @@ -43,15 +51,12 @@ export const SalesChannelList = () => { const [rowSelection, setRowSelection] = useState({}) - const { sales_channels, count, isLoading } = useAdminSalesChannels({ - limit: PAGE_SIZE, - offset: pageIndex * PAGE_SIZE, - }) - + const salesChannels = location.sales_channels + const count = location.sales_channels?.length || 0 const columns = useColumns() const table = useReactTable({ - data: sales_channels ?? [], + data: salesChannels ?? [], columns, pageCount: Math.ceil((count ?? 0) / PAGE_SIZE), state: { @@ -65,12 +70,17 @@ export const SalesChannelList = () => { }) return ( -
- -
- {t("salesChannels.domain")} -
-
+ +
+ Sales Channels + + + +
+
+ {count ? ( {table.getHeaderGroups().map((headerGroup) => { @@ -120,24 +130,20 @@ export const SalesChannelList = () => { ))}
- -
-
-
+ )} +
+ ) } const SalesChannelActions = ({ id }: { id: string }) => { - const { mutateAsync } = useAdminDeleteSalesChannel(id) const { t } = useTranslation() return ( @@ -178,11 +184,11 @@ const useColumns = () => { cell: ({ getValue }) => getValue(), }), columnHelper.accessor("is_disabled", { - header: () =>
{t("fields.status")}
, + header: t("fields.status"), cell: ({ getValue }) => { const value = getValue() return ( -
+
{value ? t("general.disabled") : t("general.enabled")} diff --git a/packages/admin-next/dashboard/src/routes/locations/location-detail/index.ts b/packages/admin-next/dashboard/src/routes/locations/location-detail/index.ts new file mode 100644 index 0000000000..086ccf707a --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/locations/location-detail/index.ts @@ -0,0 +1 @@ +export { LocationDetail as Component } from "./location-detail" diff --git a/packages/admin-next/dashboard/src/routes/locations/location-detail/location-detail.tsx b/packages/admin-next/dashboard/src/routes/locations/location-detail/location-detail.tsx new file mode 100644 index 0000000000..45985636c6 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/locations/location-detail/location-detail.tsx @@ -0,0 +1,38 @@ +import { useAdminStockLocations } from "medusa-react" +import { Outlet, json, useParams } from "react-router-dom" +import { JsonViewSection } from "../../../components/common/json-view-section" +import { LocationGeneralSection } from "./components/location-general-section" +import { LocationSalesChannelSection } from "./components/location-sales-channel-section" + +export const LocationDetail = () => { + const { id } = useParams() + const { stock_locations, isLoading, isError, error } = useAdminStockLocations( + { + id, + expand: "address,sales_channels", + } + ) + + if (isLoading) { + return
Loading...
+ } + + if (isError) { + throw error + } + + const stock_location = stock_locations?.[0] + + if (!stock_location) { + throw json({ message: "Not found" }, 404) + } + + return ( +
+ + + + +
+ ) +} diff --git a/packages/admin-next/dashboard/src/routes/locations/location-edit/components/edit-location-form/edit-location-form.tsx b/packages/admin-next/dashboard/src/routes/locations/location-edit/components/edit-location-form/edit-location-form.tsx new file mode 100644 index 0000000000..3591059a73 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/locations/location-edit/components/edit-location-form/edit-location-form.tsx @@ -0,0 +1,218 @@ +import { zodResolver } from "@hookform/resolvers/zod" +import { StockLocationExpandedDTO } from "@medusajs/types" +import { Button, Drawer, Input } from "@medusajs/ui" +import { useAdminUpdateStockLocation } 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" + +type EditLocationFormProps = { + location: StockLocationExpandedDTO +} + +const EditLocationSchema = 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 EditLocationForm = ({ location }: EditLocationFormProps) => { + const form = useForm>({ + defaultValues: { + name: location.name, + address: { + address_1: location.address?.address_1 || "", + address_2: location.address?.address_2 || "", + city: location.address?.city || "", + company: location.address?.company || "", + country_code: location.address?.country_code || "", + phone: location.address?.phone || "", + postal_code: location.address?.postal_code || "", + province: location.address?.province || "", + }, + }, + resolver: zodResolver(EditLocationSchema), + }) + + const { mutateAsync, isLoading } = useAdminUpdateStockLocation(location.id) + + const { t } = useTranslation() + + const handleSubmit = form.handleSubmit(async (values) => { + mutateAsync({ + name: values.name, + address: values.address, + }) + }) + + return ( +
+ + +
+ { + return ( + + {t("fields.name")} + + + + + + ) + }} + /> +
+
+ { + return ( + + {t("fields.address")} + + + + + + ) + }} + /> + { + return ( + + {t("fields.address2")} + + + + + + ) + }} + /> + { + return ( + + {t("fields.postalCode")} + + + + + + ) + }} + /> + { + return ( + + {t("fields.city")} + + + + + + ) + }} + /> + { + return ( + + {t("fields.country")} + + + + + + ) + }} + /> + { + return ( + + {t("fields.state")} + + + + + + ) + }} + /> + { + return ( + + {t("fields.company")} + + + + + + ) + }} + /> + { + return ( + + {t("fields.phone")} + + + + + + ) + }} + /> +
+
+ +
+ + + + +
+
+
+ + ) +} diff --git a/packages/admin-next/dashboard/src/routes/locations/location-edit/index.ts b/packages/admin-next/dashboard/src/routes/locations/location-edit/index.ts new file mode 100644 index 0000000000..516dd003a1 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/locations/location-edit/index.ts @@ -0,0 +1 @@ +export { LocationEdit as Component } from "./location-edit" diff --git a/packages/admin-next/dashboard/src/routes/locations/location-edit/location-edit.tsx b/packages/admin-next/dashboard/src/routes/locations/location-edit/location-edit.tsx new file mode 100644 index 0000000000..b30df632ce --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/locations/location-edit/location-edit.tsx @@ -0,0 +1,47 @@ +import { Drawer, Heading } from "@medusajs/ui" +import { useAdminStockLocations } from "medusa-react" +import { useTranslation } from "react-i18next" +import { json, useParams } from "react-router-dom" +import { useRouteModalState } from "../../../hooks/use-route-modal-state" +import { EditLocationForm } from "./components/edit-location-form/edit-location-form" + +export const LocationEdit = () => { + const [open, onOpenChange] = useRouteModalState() + const { id } = useParams() + + const { stock_locations, isLoading, isError, error } = useAdminStockLocations( + { + id, + expand: "address", + } + ) + + const { t } = useTranslation() + + if (isError) { + throw error + } + + const stock_location = stock_locations?.[0] + + if (!isLoading && !stock_location) { + throw json({ message: "Not found" }, 404) + } + + return ( + + + + + {t("locations.editLocation")} + + + {isLoading || !stock_location ? ( +
Loading...
+ ) : ( + + )} +
+
+ ) +} diff --git a/packages/admin-next/dashboard/src/routes/locations/location-list/components/locations-list-table/index.ts b/packages/admin-next/dashboard/src/routes/locations/location-list/components/locations-list-table/index.ts new file mode 100644 index 0000000000..f1308b3734 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/locations/location-list/components/locations-list-table/index.ts @@ -0,0 +1 @@ +export * from "./locations-list-table" diff --git a/packages/admin-next/dashboard/src/routes/locations/location-list/components/locations-list-table/locations-list-table.tsx b/packages/admin-next/dashboard/src/routes/locations/location-list/components/locations-list-table/locations-list-table.tsx new file mode 100644 index 0000000000..795c81ecfd --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/locations/location-list/components/locations-list-table/locations-list-table.tsx @@ -0,0 +1,278 @@ +import { EllipsisHorizontal, PencilSquare, Trash } from "@medusajs/icons" +import { StockLocationExpandedDTO } from "@medusajs/types" +import { + Button, + Container, + DropdownMenu, + Heading, + IconButton, + Table, + clx, + usePrompt, +} from "@medusajs/ui" +import { + PaginationState, + RowSelectionState, + createColumnHelper, + flexRender, + getCoreRowModel, + useReactTable, +} from "@tanstack/react-table" +import { + useAdminDeleteStockLocation, + useAdminStockLocations, +} from "medusa-react" +import { MouseEvent, useMemo, useState } from "react" +import { useTranslation } from "react-i18next" +import { Link, useNavigate, useSearchParams } from "react-router-dom" + +import { + NoRecords, + NoResults, +} from "../../../../../components/common/empty-table-content/empty-table-content" +import { LocalizedTablePagination } from "../../../../../components/localization/localized-table-pagination" + +const PAGE_SIZE = 50 + +export const LocationsListTable = () => { + const navigate = useNavigate() + const { t } = useTranslation() + + const [{ pageIndex, pageSize }, setPagination] = useState({ + pageIndex: 0, + pageSize: PAGE_SIZE, + }) + + const pagination = useMemo( + () => ({ + pageIndex, + pageSize, + }), + [pageIndex, pageSize] + ) + + const [rowSelection, setRowSelection] = useState({}) + + const { stock_locations, count, isLoading, isError, error } = + useAdminStockLocations({ + limit: PAGE_SIZE, + offset: pageIndex * PAGE_SIZE, + expand: "address", + }) + + const columns = useColumns() + + const table = useReactTable({ + data: stock_locations ?? [], + columns, + pageCount: Math.ceil((count ?? 0) / PAGE_SIZE), + state: { + pagination, + rowSelection, + }, + manualPagination: true, + getCoreRowModel: getCoreRowModel(), + onPaginationChange: setPagination, + onRowSelectionChange: setRowSelection, + }) + + if (isLoading) { + return
Loading...
+ } + + if (isError) { + if (error) { + throw error + } + } + + return ( + +
+ Locations +
+ + + +
+
+ {(stock_locations?.length ?? 0) > 0 ? ( +
+ + + {table.getHeaderGroups().map((headerGroup) => { + return ( + + {headerGroup.headers.map((header) => { + return ( + + {flexRender( + header.column.columnDef.header, + header.getContext() + )} + + ) + })} + + ) + })} + + + {table.getRowModel().rows.map((row) => ( + + navigate(`/settings/locations/${row.original.id}`) + } + > + {row.getVisibleCells().map((cell) => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} + + ))} + + ))} + +
+ +
+ ) : ( + + )} +
+ ) +} + +const LocationActions = ({ + location, +}: { + location: StockLocationExpandedDTO +}) => { + const { t } = useTranslation() + const prompt = usePrompt() + const { mutateAsync } = useAdminDeleteStockLocation(location.id) + + const handleDelete = async (e: MouseEvent) => { + e.stopPropagation() + + const res = await prompt({ + title: t("general.areYouSure"), + description: t("locations.deleteLocationWarning", { + name: location.name, + }), + verificationText: location.name, + verificationInstruction: t("general.typeToConfirm"), + confirmText: t("general.delete"), + cancelText: t("general.cancel"), + }) + + if (!res) { + return + } + + await mutateAsync() + } + + return ( + + + + + + + + + e.stopPropagation()}> +
+ + {t("general.edit")} +
+
+ + + +
+ + {t("general.delete")} +
+
+
+
+ ) +} + +const columnHelper = createColumnHelper() + +const useColumns = () => { + const { t } = useTranslation() + + return useMemo( + () => [ + columnHelper.accessor("name", { + header: t("fields.name"), + cell: (cell) => cell.getValue(), + }), + columnHelper.accessor("address", { + header: t("fields.address"), + cell: (cell) => { + const value = cell.getValue() + + if (!value) { + return "-" + } + + return `${value.address_1}${value.city ? `, ${value.city}` : ""}` + }, + }), + columnHelper.display({ + id: "actions", + cell: ({ row }) => , + }), + ], + [t] + ) +} + +const NoLocations = () => { + const [params] = useSearchParams() + const { t } = useTranslation() + + const noParams = params.toString().length === 0 + + if (noParams) { + return ( + + ) + } + + return +} diff --git a/packages/admin-next/dashboard/src/routes/locations/location-list/index.ts b/packages/admin-next/dashboard/src/routes/locations/location-list/index.ts new file mode 100644 index 0000000000..883c2ba632 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/locations/location-list/index.ts @@ -0,0 +1 @@ +export { LocationList as Component } from "./location-list" diff --git a/packages/admin-next/dashboard/src/routes/locations/location-list/location-list.tsx b/packages/admin-next/dashboard/src/routes/locations/location-list/location-list.tsx new file mode 100644 index 0000000000..02363aba29 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/locations/location-list/location-list.tsx @@ -0,0 +1,11 @@ +import { Outlet } from "react-router-dom" +import { LocationsListTable } from "./components/locations-list-table" + +export const LocationList = () => { + return ( +
+ + +
+ ) +} diff --git a/packages/admin-next/dashboard/src/routes/login/login.tsx b/packages/admin-next/dashboard/src/routes/login/login.tsx index e042eccff2..dadf791ba5 100644 --- a/packages/admin-next/dashboard/src/routes/login/login.tsx +++ b/packages/admin-next/dashboard/src/routes/login/login.tsx @@ -1,13 +1,14 @@ import { zodResolver } from "@hookform/resolvers/zod" import { Button, Heading, Input, Text } from "@medusajs/ui" +import { useAdminGetSession, useAdminLogin } from "medusa-react" import { useForm } from "react-hook-form" -import { Link, useLocation, useNavigate } from "react-router-dom" +import { Link, Navigate, useLocation, useNavigate } from "react-router-dom" import * as z from "zod" import { Form } from "../../components/common/form" -import { useAuth } from "../../providers/auth-provider" +import { isAxiosError } from "../../lib/is-axios-error" -const schema = z.object({ +const LoginSchema = z.object({ email: z.string().email(), password: z.string(), }) @@ -15,40 +16,58 @@ const schema = z.object({ export const Login = () => { const navigate = useNavigate() const location = useLocation() - const { login } = useAuth() const from = location.state?.from?.pathname || "/" - const form = useForm>({ - resolver: zodResolver(schema), + const form = useForm>({ + resolver: zodResolver(LoginSchema), defaultValues: { email: "", password: "", }, }) - const onSubmit = form.handleSubmit(async ({ email, password }) => { - await login(email, password) - .then(() => { - navigate(from) - }) - .catch((e) => { - switch (e?.response?.status) { - case 401: - form.setError("password", { - type: "manual", - message: "Invalid email or password", - }) - break - default: - form.setError("password", { - type: "manual", - message: "Something went wrong", - }) - } - }) + const { user, isLoading } = useAdminGetSession() + + const { mutateAsync } = useAdminLogin({ + retry: false, }) + const onSubmit = form.handleSubmit(async ({ email, password }) => { + await mutateAsync( + { + email, + password, + }, + { + onSuccess: () => { + navigate(from, { replace: true }) + }, + onError: (e) => { + if (isAxiosError(e)) { + if (e.response?.status === 401) { + form.setError("password", { + type: "manual", + message: "Invalid email or password", + }) + + return + } + } + + form.setError("password", { + type: "manual", + message: "Something went wrong", + }) + }, + } + ) + }) + + if (user && !isLoading) { + return + } + return (
@@ -79,7 +98,7 @@ export const Login = () => { Password Forgot password? diff --git a/packages/admin-next/dashboard/src/routes/products/components/product-attribute-section/index.ts b/packages/admin-next/dashboard/src/routes/products/product-detail/components/product-attribute-section/index.ts similarity index 100% rename from packages/admin-next/dashboard/src/routes/products/components/product-attribute-section/index.ts rename to packages/admin-next/dashboard/src/routes/products/product-detail/components/product-attribute-section/index.ts diff --git a/packages/admin-next/dashboard/src/routes/products/components/product-attribute-section/product-attribute-section.tsx b/packages/admin-next/dashboard/src/routes/products/product-detail/components/product-attribute-section/product-attribute-section.tsx similarity index 100% rename from packages/admin-next/dashboard/src/routes/products/components/product-attribute-section/product-attribute-section.tsx rename to packages/admin-next/dashboard/src/routes/products/product-detail/components/product-attribute-section/product-attribute-section.tsx diff --git a/packages/admin-next/dashboard/src/routes/products/components/product-general-section/index.ts b/packages/admin-next/dashboard/src/routes/products/product-detail/components/product-general-section/index.ts similarity index 100% rename from packages/admin-next/dashboard/src/routes/products/components/product-general-section/index.ts rename to packages/admin-next/dashboard/src/routes/products/product-detail/components/product-general-section/index.ts diff --git a/packages/admin-next/dashboard/src/routes/products/components/product-general-section/product-general-section.tsx b/packages/admin-next/dashboard/src/routes/products/product-detail/components/product-general-section/product-general-section.tsx similarity index 100% rename from packages/admin-next/dashboard/src/routes/products/components/product-general-section/product-general-section.tsx rename to packages/admin-next/dashboard/src/routes/products/product-detail/components/product-general-section/product-general-section.tsx diff --git a/packages/admin-next/dashboard/src/routes/products/components/product-media-section/index.ts b/packages/admin-next/dashboard/src/routes/products/product-detail/components/product-media-section/index.ts similarity index 100% rename from packages/admin-next/dashboard/src/routes/products/components/product-media-section/index.ts rename to packages/admin-next/dashboard/src/routes/products/product-detail/components/product-media-section/index.ts diff --git a/packages/admin-next/dashboard/src/routes/products/components/product-media-section/product-media-section.tsx b/packages/admin-next/dashboard/src/routes/products/product-detail/components/product-media-section/product-media-section.tsx similarity index 100% rename from packages/admin-next/dashboard/src/routes/products/components/product-media-section/product-media-section.tsx rename to packages/admin-next/dashboard/src/routes/products/product-detail/components/product-media-section/product-media-section.tsx diff --git a/packages/admin-next/dashboard/src/routes/products/components/product-option-section/index.ts b/packages/admin-next/dashboard/src/routes/products/product-detail/components/product-option-section/index.ts similarity index 100% rename from packages/admin-next/dashboard/src/routes/products/components/product-option-section/index.ts rename to packages/admin-next/dashboard/src/routes/products/product-detail/components/product-option-section/index.ts diff --git a/packages/admin-next/dashboard/src/routes/products/components/product-option-section/product-option-section.tsx b/packages/admin-next/dashboard/src/routes/products/product-detail/components/product-option-section/product-option-section.tsx similarity index 100% rename from packages/admin-next/dashboard/src/routes/products/components/product-option-section/product-option-section.tsx rename to packages/admin-next/dashboard/src/routes/products/product-detail/components/product-option-section/product-option-section.tsx diff --git a/packages/admin-next/dashboard/src/routes/products/components/product-sales-channel-section/index.ts b/packages/admin-next/dashboard/src/routes/products/product-detail/components/product-sales-channel-section/index.ts similarity index 100% rename from packages/admin-next/dashboard/src/routes/products/components/product-sales-channel-section/index.ts rename to packages/admin-next/dashboard/src/routes/products/product-detail/components/product-sales-channel-section/index.ts diff --git a/packages/admin-next/dashboard/src/routes/products/components/product-sales-channel-section/product-sales-channel-section.tsx b/packages/admin-next/dashboard/src/routes/products/product-detail/components/product-sales-channel-section/product-sales-channel-section.tsx similarity index 100% rename from packages/admin-next/dashboard/src/routes/products/components/product-sales-channel-section/product-sales-channel-section.tsx rename to packages/admin-next/dashboard/src/routes/products/product-detail/components/product-sales-channel-section/product-sales-channel-section.tsx diff --git a/packages/admin-next/dashboard/src/routes/products/components/product-thumbnail-section/index.ts b/packages/admin-next/dashboard/src/routes/products/product-detail/components/product-thumbnail-section/index.ts similarity index 100% rename from packages/admin-next/dashboard/src/routes/products/components/product-thumbnail-section/index.ts rename to packages/admin-next/dashboard/src/routes/products/product-detail/components/product-thumbnail-section/index.ts diff --git a/packages/admin-next/dashboard/src/routes/products/components/product-thumbnail-section/product-thumbnail-section.tsx b/packages/admin-next/dashboard/src/routes/products/product-detail/components/product-thumbnail-section/product-thumbnail-section.tsx similarity index 100% rename from packages/admin-next/dashboard/src/routes/products/components/product-thumbnail-section/product-thumbnail-section.tsx rename to packages/admin-next/dashboard/src/routes/products/product-detail/components/product-thumbnail-section/product-thumbnail-section.tsx diff --git a/packages/admin-next/dashboard/src/routes/products/components/product-variant-section/index.ts b/packages/admin-next/dashboard/src/routes/products/product-detail/components/product-variant-section/index.ts similarity index 100% rename from packages/admin-next/dashboard/src/routes/products/components/product-variant-section/index.ts rename to packages/admin-next/dashboard/src/routes/products/product-detail/components/product-variant-section/index.ts diff --git a/packages/admin-next/dashboard/src/routes/products/components/product-variant-section/product-variant-section.tsx b/packages/admin-next/dashboard/src/routes/products/product-detail/components/product-variant-section/product-variant-section.tsx similarity index 100% rename from packages/admin-next/dashboard/src/routes/products/components/product-variant-section/product-variant-section.tsx rename to packages/admin-next/dashboard/src/routes/products/product-detail/components/product-variant-section/product-variant-section.tsx diff --git a/packages/admin-next/dashboard/src/routes/products/product-detail/index.ts b/packages/admin-next/dashboard/src/routes/products/product-detail/index.ts new file mode 100644 index 0000000000..afc54745c9 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/products/product-detail/index.ts @@ -0,0 +1,2 @@ +export { productLoader as loader } from "./loader" +export { ProductDetail as Component } from "./product-detail" diff --git a/packages/admin-next/dashboard/src/routes/products/views/product-details/loader.ts b/packages/admin-next/dashboard/src/routes/products/product-detail/loader.ts similarity index 91% rename from packages/admin-next/dashboard/src/routes/products/views/product-details/loader.ts rename to packages/admin-next/dashboard/src/routes/products/product-detail/loader.ts index 0f27a0d25f..7f5fad255e 100644 --- a/packages/admin-next/dashboard/src/routes/products/views/product-details/loader.ts +++ b/packages/admin-next/dashboard/src/routes/products/product-detail/loader.ts @@ -3,7 +3,7 @@ import { Response } from "@medusajs/medusa-js" import { adminProductKeys } from "medusa-react" import { LoaderFunctionArgs } from "react-router-dom" -import { medusa, queryClient } from "../../../../lib/medusa" +import { medusa, queryClient } from "../../../lib/medusa" const productDetailQuery = (id: string) => ({ queryKey: adminProductKeys.detail(id), diff --git a/packages/admin-next/dashboard/src/routes/products/views/product-details/details.tsx b/packages/admin-next/dashboard/src/routes/products/product-detail/product-detail.tsx similarity index 66% rename from packages/admin-next/dashboard/src/routes/products/views/product-details/details.tsx rename to packages/admin-next/dashboard/src/routes/products/product-detail/product-detail.tsx index 5139d932ae..7f0f42009b 100644 --- a/packages/admin-next/dashboard/src/routes/products/views/product-details/details.tsx +++ b/packages/admin-next/dashboard/src/routes/products/product-detail/product-detail.tsx @@ -1,43 +1,43 @@ -import { useAdminProduct } from "medusa-react"; -import { useLoaderData, useParams } from "react-router-dom"; +import { useAdminProduct } from "medusa-react" +import { useLoaderData, useParams } from "react-router-dom" -import { JsonView } from "../../../../components/common/json-view"; -import { ProductAttributeSection } from "../../components/product-attribute-section"; -import { ProductGeneralSection } from "../../components/product-general-section"; -import { ProductMediaSection } from "../../components/product-media-section"; -import { ProductOptionSection } from "../../components/product-option-section"; -import { ProductSalesChannelSection } from "../../components/product-sales-channel-section"; -import { ProductThumbnailSection } from "../../components/product-thumbnail-section"; -import { ProductVariantSection } from "../../components/product-variant-section"; -import { productLoader } from "./loader"; +import { JsonViewSection } from "../../../components/common/json-view-section" +import { ProductAttributeSection } from "./components/product-attribute-section" +import { ProductGeneralSection } from "./components/product-general-section" +import { ProductMediaSection } from "./components/product-media-section" +import { ProductOptionSection } from "./components/product-option-section" +import { ProductSalesChannelSection } from "./components/product-sales-channel-section" +import { ProductThumbnailSection } from "./components/product-thumbnail-section" +import { ProductVariantSection } from "./components/product-variant-section" +import { productLoader } from "./loader" -import after from "medusa-admin:widgets/product/details/after"; -import before from "medusa-admin:widgets/product/details/before"; -import sideAfter from "medusa-admin:widgets/product/details/side/after"; -import sideBefore from "medusa-admin:widgets/product/details/side/before"; +import after from "medusa-admin:widgets/product/details/after" +import before from "medusa-admin:widgets/product/details/before" +import sideAfter from "medusa-admin:widgets/product/details/side/after" +import sideBefore from "medusa-admin:widgets/product/details/side/before" -export const ProductDetails = () => { +export const ProductDetail = () => { const initialData = useLoaderData() as Awaited< ReturnType - >; + > - const { id } = useParams(); + const { id } = useParams() const { product, isLoading, isError, error } = useAdminProduct( id!, undefined, { initialData: initialData, } - ); + ) // TODO: Move to loading.tsx and set as Suspense fallback for the route if (isLoading) { - return
Loading
; + return
Loading
} // TODO: Move to error.tsx and set as ErrorBoundary for the route if (isError || !product) { - const err = error ? JSON.parse(JSON.stringify(error)) : null; + const err = error ? JSON.parse(JSON.stringify(error)) : null return (
{(err as Error & { status: number })?.status === 404 ? ( @@ -46,7 +46,7 @@ export const ProductDetails = () => {
Something went wrong!
)}
- ); + ) } return ( @@ -56,9 +56,9 @@ export const ProductDetails = () => {
- ); + ) })} -
+
@@ -71,7 +71,7 @@ export const ProductDetails = () => {
- ); + ) })} @@ -80,7 +80,7 @@ export const ProductDetails = () => {
- ); + ) })}
{after.widgets.map((w, i) => { @@ -88,17 +88,17 @@ export const ProductDetails = () => {
- ); + ) })} - +
-
+
{sideBefore.widgets.map((w, i) => { return (
- ); + ) })} @@ -107,10 +107,10 @@ export const ProductDetails = () => {
- ); + ) })}
- ); -}; + ) +} diff --git a/packages/admin-next/dashboard/src/routes/products/product-list/components/product-list-table/index.ts b/packages/admin-next/dashboard/src/routes/products/product-list/components/product-list-table/index.ts new file mode 100644 index 0000000000..ee665245d3 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/products/product-list/components/product-list-table/index.ts @@ -0,0 +1 @@ +export * from "./product-list-table" diff --git a/packages/admin-next/dashboard/src/routes/products/product-list/components/product-list-table/product-list-table.tsx b/packages/admin-next/dashboard/src/routes/products/product-list/components/product-list-table/product-list-table.tsx new file mode 100644 index 0000000000..0114f4ffe9 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/products/product-list/components/product-list-table/product-list-table.tsx @@ -0,0 +1,284 @@ +import { EllipsisHorizontal, Trash } from "@medusajs/icons" +import type { Product } from "@medusajs/medusa" +import { + Button, + Checkbox, + CommandBar, + Container, + DropdownMenu, + Heading, + IconButton, + Table, + clx, +} from "@medusajs/ui" +import { + PaginationState, + RowSelectionState, + createColumnHelper, + flexRender, + getCoreRowModel, + useReactTable, +} from "@tanstack/react-table" +import { useAdminDeleteProduct, useAdminProducts } from "medusa-react" +import { useMemo, useState } from "react" +import { useTranslation } from "react-i18next" +import { useLoaderData, useNavigate } from "react-router-dom" + +import { + ProductAvailabilityCell, + ProductCollectionCell, + ProductStatusCell, + ProductTitleCell, + ProductVariantCell, +} from "../../../../../components/common/product-table-cells" + +import { LocalizedTablePagination } from "../../../../../components/localization/localized-table-pagination" +import { productsLoader } from "../../loader" + +const PAGE_SIZE = 50 + +export const ProductListTable = () => { + const navigate = useNavigate() + const { t } = useTranslation() + + const [{ pageIndex, pageSize }, setPagination] = useState({ + pageIndex: 0, + pageSize: PAGE_SIZE, + }) + + const [rowSelection, setRowSelection] = useState({}) + + const initialData = useLoaderData() as Awaited< + ReturnType> + > + + const { products, count } = useAdminProducts( + { + limit: PAGE_SIZE, + offset: pageIndex * PAGE_SIZE, + }, + { + initialData, + } + ) + + const columns = useColumns() + + const pagination = useMemo( + () => ({ + pageIndex, + pageSize, + }), + [pageIndex, pageSize] + ) + + const table = useReactTable({ + data: (products ?? []) as Product[], + columns, + pageCount: Math.ceil((count ?? 0) / PAGE_SIZE), + state: { + pagination, + rowSelection, + }, + onPaginationChange: setPagination, + onRowSelectionChange: setRowSelection, + getCoreRowModel: getCoreRowModel(), + manualPagination: true, + }) + + return ( + +
+ {t("products.domain")} + +
+
+ + + {table.getHeaderGroups().map((headerGroup) => { + return ( + + {headerGroup.headers.map((header) => { + return ( + + {flexRender( + header.column.columnDef.header, + header.getContext() + )} + + ) + })} + + ) + })} + + + {table.getRowModel().rows.map((row) => ( + navigate(`/products/${row.original.id}`)} + > + {row.getVisibleCells().map((cell) => ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} + + ))} + +
+ +
+ + + + {t("general.countSelected", { + count: Object.keys(rowSelection).length, + })} + + + { + console.log("Delete") + }} + shortcut="d" + label={t("general.delete")} + /> + + +
+ ) +} + +const ProductActions = ({ id }: { id: string }) => { + const { mutateAsync } = useAdminDeleteProduct(id) + + const handleDelete = async () => { + await mutateAsync() + } + + return ( + + + + + + + + +
+ + Delete +
+
+
+
+ ) +} + +const columnHelper = createColumnHelper() + +const useColumns = () => { + const { t } = useTranslation() + + const columns = useMemo( + () => [ + columnHelper.display({ + id: "select", + header: ({ table }) => { + return ( + + table.toggleAllPageRowsSelected(!!value) + } + /> + ) + }, + cell: ({ row }) => { + return ( + row.toggleSelected(!!value)} + onClick={(e) => { + e.stopPropagation() + }} + /> + ) + }, + }), + columnHelper.accessor("title", { + header: t("fields.title"), + cell: ({ row }) => { + return + }, + }), + columnHelper.accessor("collection", { + header: t("fields.collection"), + cell: (cell) => { + const collection = cell.getValue() + + return + }, + }), + columnHelper.accessor("sales_channels", { + header: t("fields.availability"), + cell: (cell) => { + const salesChannels = cell.getValue() + + return + }, + }), + columnHelper.accessor("variants", { + header: t("fields.inventory"), + cell: (cell) => { + const variants = cell.getValue() + + return + }, + }), + columnHelper.accessor("status", { + header: t("fields.status"), + cell: (cell) => { + const value = cell.getValue() + + return + }, + }), + columnHelper.display({ + id: "actions", + cell: ({ row }) => { + return + }, + }), + ], + [t] + ) + + return columns +} diff --git a/packages/admin-next/dashboard/src/routes/products/product-list/index.ts b/packages/admin-next/dashboard/src/routes/products/product-list/index.ts new file mode 100644 index 0000000000..085cd656a4 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/products/product-list/index.ts @@ -0,0 +1,2 @@ +export { productsLoader as productLoader } from "./loader" +export { ProductList as Component } from "./product-list" diff --git a/packages/admin-next/dashboard/src/routes/products/views/product-list/loader.ts b/packages/admin-next/dashboard/src/routes/products/product-list/loader.ts similarity index 55% rename from packages/admin-next/dashboard/src/routes/products/views/product-list/loader.ts rename to packages/admin-next/dashboard/src/routes/products/product-list/loader.ts index 2828984d46..9ad18e2dc5 100644 --- a/packages/admin-next/dashboard/src/routes/products/views/product-list/loader.ts +++ b/packages/admin-next/dashboard/src/routes/products/product-list/loader.ts @@ -1,23 +1,23 @@ -import { AdminProductsListRes } from "@medusajs/medusa"; -import { Response } from "@medusajs/medusa-js"; -import { QueryClient } from "@tanstack/react-query"; -import { adminProductKeys } from "medusa-react"; +import { AdminProductsListRes } from "@medusajs/medusa" +import { Response } from "@medusajs/medusa-js" +import { QueryClient } from "@tanstack/react-query" +import { adminProductKeys } from "medusa-react" -import { medusa, queryClient } from "../../../../lib/medusa"; +import { medusa, queryClient } from "../../../lib/medusa" const productsListQuery = () => ({ queryKey: adminProductKeys.list({ limit: 20, offset: 0 }), queryFn: async () => medusa.admin.products.list({ limit: 20, offset: 0 }), -}); +}) export const productsLoader = (client: QueryClient) => { return async () => { - const query = productsListQuery(); + const query = productsListQuery() return ( queryClient.getQueryData>( query.queryKey ) ?? (await client.fetchQuery(query)) - ); - }; -}; + ) + } +} diff --git a/packages/admin-next/dashboard/src/routes/products/product-list/product-list.tsx b/packages/admin-next/dashboard/src/routes/products/product-list/product-list.tsx new file mode 100644 index 0000000000..11513802c8 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/products/product-list/product-list.tsx @@ -0,0 +1,18 @@ +import after from "medusa-admin:widgets/product/list/after" +import before from "medusa-admin:widgets/product/list/before" + +import { ProductListTable } from "./components/product-list-table" + +export const ProductList = () => { + return ( +
+ {before.widgets.map((w, i) => ( + + ))} + + {after.widgets.map((w, i) => ( + + ))} +
+ ) +} diff --git a/packages/admin-next/dashboard/src/routes/products/views/product-details/index.ts b/packages/admin-next/dashboard/src/routes/products/views/product-details/index.ts deleted file mode 100644 index a1671c01de..0000000000 --- a/packages/admin-next/dashboard/src/routes/products/views/product-details/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { ProductDetails as Component } from "./details"; -export { productLoader as loader } from "./loader"; diff --git a/packages/admin-next/dashboard/src/routes/products/views/product-list/index.ts b/packages/admin-next/dashboard/src/routes/products/views/product-list/index.ts deleted file mode 100644 index bad072e574..0000000000 --- a/packages/admin-next/dashboard/src/routes/products/views/product-list/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { ProductList as Component } from "./list"; -export { productsLoader as productLoader } from "./loader"; diff --git a/packages/admin-next/dashboard/src/routes/products/views/product-list/list.tsx b/packages/admin-next/dashboard/src/routes/products/views/product-list/list.tsx deleted file mode 100644 index bf546c833b..0000000000 --- a/packages/admin-next/dashboard/src/routes/products/views/product-list/list.tsx +++ /dev/null @@ -1,277 +0,0 @@ -import type { Product } from "@medusajs/medusa" -import { - Checkbox, - CommandBar, - Container, - DropdownMenu, - Heading, - IconButton, - Table, - Text, - clx, -} from "@medusajs/ui" -import { - PaginationState, - RowSelectionState, - createColumnHelper, - flexRender, - getCoreRowModel, - useReactTable, -} from "@tanstack/react-table" -import { useAdminDeleteProduct, useAdminProducts } from "medusa-react" -import { useMemo, useState } from "react" -import { useLoaderData, useNavigate } from "react-router-dom" - -import { Thumbnail } from "../../../../components/common/thumbnail" -import { productsLoader } from "./loader" - -import { EllipsisVertical, Trash } from "@medusajs/icons" -import after from "medusa-admin:widgets/product/list/after" -import before from "medusa-admin:widgets/product/list/before" -import { useTranslation } from "react-i18next" - -const PAGE_SIZE = 50 - -export const ProductList = () => { - const navigate = useNavigate() - const { t } = useTranslation() - - const [{ pageIndex, pageSize }, setPagination] = useState({ - pageIndex: 0, - pageSize: PAGE_SIZE, - }) - - const [rowSelection, setRowSelection] = useState({}) - - const initialData = useLoaderData() as Awaited< - ReturnType> - > - - const { products, count } = useAdminProducts( - { - limit: PAGE_SIZE, - offset: pageIndex * PAGE_SIZE, - }, - { - initialData, - } - ) - - const columns = useColumns() - - const pagination = useMemo( - () => ({ - pageIndex, - pageSize, - }), - [pageIndex, pageSize] - ) - - const table = useReactTable({ - data: (products ?? []) as Product[], - columns, - pageCount: Math.ceil((count ?? 0) / PAGE_SIZE), - state: { - pagination, - rowSelection, - }, - onPaginationChange: setPagination, - onRowSelectionChange: setRowSelection, - getCoreRowModel: getCoreRowModel(), - manualPagination: true, - }) - - return ( -
- {before.widgets.map((w, i) => ( - - ))} - -
- {t("products.domain")} -
-
- - - {table.getHeaderGroups().map((headerGroup) => { - return ( - - {headerGroup.headers.map((header) => { - return ( - - {flexRender( - header.column.columnDef.header, - header.getContext() - )} - - ) - })} - - ) - })} - - - {table.getRowModel().rows.map((row) => ( - navigate(`/products/${row.original.id}`)} - > - {row.getVisibleCells().map((cell) => ( - - {flexRender( - cell.column.columnDef.cell, - cell.getContext() - )} - - ))} - - ))} - -
- -
- - - - {t("general.countSelected", { - count: Object.keys(rowSelection).length, - })} - - - { - console.log("Delete") - }} - shortcut="d" - label={t("general.delete")} - /> - - -
- {after.widgets.map((w, i) => ( - - ))} -
- ) -} - -const ProductActions = ({ id }: { id: string }) => { - const { mutateAsync } = useAdminDeleteProduct(id) - - const handleDelete = async () => { - await mutateAsync() - } - - return ( - - - - - - - - -
- - Delete -
-
-
-
- ) -} - -const columnHelper = createColumnHelper() - -const useColumns = () => { - const { t } = useTranslation() - - const columns = useMemo( - () => [ - columnHelper.display({ - id: "select", - header: ({ table }) => { - return ( - - table.toggleAllPageRowsSelected(!!value) - } - /> - ) - }, - cell: ({ row }) => { - return ( - row.toggleSelected(!!value)} - onClick={(e) => { - e.stopPropagation() - }} - /> - ) - }, - }), - columnHelper.accessor("title", { - header: t("fields.title"), - cell: (cell) => { - const title = cell.getValue() - const thumbnail = cell.row.original.thumbnail - - return ( -
- - - {title} - -
- ) - }, - }), - columnHelper.accessor("variants", { - header: t("products.variants"), - cell: (cell) => { - const variants = cell.getValue() - - return ( - - {variants.length} - - ) - }, - }), - columnHelper.display({ - id: "actions", - cell: ({ row }) => { - return - }, - }), - ], - [t] - ) - - return columns -} diff --git a/packages/admin-next/dashboard/src/routes/profile/components/edit-profile-details-drawer/edit-profile-details-drawer.tsx b/packages/admin-next/dashboard/src/routes/profile/components/edit-profile-details-drawer/edit-profile-details-drawer.tsx deleted file mode 100644 index 27b4d5684c..0000000000 --- a/packages/admin-next/dashboard/src/routes/profile/components/edit-profile-details-drawer/edit-profile-details-drawer.tsx +++ /dev/null @@ -1,236 +0,0 @@ -import { zodResolver } from "@hookform/resolvers/zod" -import { Button, Drawer, Heading, Input, Select, Switch } from "@medusajs/ui" -import { adminAuthKeys, useAdminUpdateUser } from "medusa-react" -import { useState } from "react" -import { useForm } from "react-hook-form" -import { Trans, useTranslation } from "react-i18next" -import * as zod from "zod" -import { Form } from "../../../../components/common/form" -import { languages } from "../../../../i18n/config" -import { queryClient } from "../../../../lib/medusa" - -const EditProfileDetailsSchema = zod.object({ - first_name: zod.string().optional(), - last_name: zod.string().optional(), - language: zod.string(), - user_insights: zod.boolean(), -}) - -type EditProfileDetailsDrawerProps = { - id: string - firstName?: string - lastName?: string - userInsights?: boolean -} - -export const EditProfileDetailsDrawer = ({ - id, - firstName = "", - lastName = "", - userInsights = false, -}: EditProfileDetailsDrawerProps) => { - const [open, setOpen] = useState(false) - const [selectOpen, setSelectOpen] = useState(false) - const { mutateAsync, isLoading } = useAdminUpdateUser(id) - - const { i18n } = useTranslation() - - const changeLanguage = (code: string) => { - i18n.changeLanguage(code) - } - - const sortedLanguages = languages.sort((a, b) => - a.display_name.localeCompare(b.display_name) - ) - - const form = useForm>({ - defaultValues: { - first_name: firstName, - last_name: lastName, - language: i18n.language, - user_insights: userInsights, - }, - resolver: zodResolver(EditProfileDetailsSchema), - }) - - const { t } = useTranslation() - - const onOpenChange = (open: boolean) => { - if (!open) { - form.reset() - - /** - * If the select is open while closing the drawer, we need to close it as well. - * Otherwise it will cause "pointer-events: none" to stay applied to the body, - * making the page unresponsive. - */ - setSelectOpen(false) - } - - setOpen(open) - } - - const onSubmit = form.handleSubmit(async (values) => { - await mutateAsync( - { - first_name: values.first_name, - last_name: values.last_name, - }, - { - onSuccess: ({ user }) => { - form.reset({ - first_name: user.first_name, - last_name: user.last_name, - }) - - // Invalidate the current user session. - queryClient.invalidateQueries(adminAuthKeys.details()) - }, - onError: (error) => { - console.log(error) - return - }, - } - ) - - changeLanguage(values.language) - - onOpenChange(false) - }) - - return ( - - - - - - - {t("profile.editProfileDetails")} - - -
-
-
- ( - - {t("fields.firstName")} - - - - - - )} - /> - ( - - {t("fields.lastName")} - - - - - - )} - /> -
- ( - -
- Language - {t("profile.languageHint")} -
-
- - - - -
-
- )} - /> - ( - -
- User Insights - - - -
- - - , - ]} - /> - - - -
- )} - /> -
-
-
- -
- - - - -
-
-
-
- ) -} diff --git a/packages/admin-next/dashboard/src/routes/profile/profile-detail/components/profile-general-section/index.ts b/packages/admin-next/dashboard/src/routes/profile/profile-detail/components/profile-general-section/index.ts new file mode 100644 index 0000000000..948c77df96 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/profile/profile-detail/components/profile-general-section/index.ts @@ -0,0 +1 @@ +export * from "./profile-general-section" diff --git a/packages/admin-next/dashboard/src/routes/profile/profile-detail/components/profile-general-section/profile-general-section.tsx b/packages/admin-next/dashboard/src/routes/profile/profile-detail/components/profile-general-section/profile-general-section.tsx new file mode 100644 index 0000000000..49ef382d96 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/profile/profile-detail/components/profile-general-section/profile-general-section.tsx @@ -0,0 +1,63 @@ +import { User } from "@medusajs/medusa" +import { Button, Container, Heading, StatusBadge, Text } from "@medusajs/ui" +import { useTranslation } from "react-i18next" +import { Link } from "react-router-dom" +import { languages } from "../../../../../i18n/config" + +type ProfileGeneralSectionProps = { + user: Omit +} + +export const ProfileGeneralSection = ({ user }: ProfileGeneralSectionProps) => { + const { i18n, t } = useTranslation() + return ( + +
+
+ {t("profile.domain")} + + {t("profile.manageYourProfileDetails")} + +
+ + + +
+
+ + {t("fields.name")} + + + {user.first_name} {user.last_name} + +
+
+ + {t("fields.email")} + + + {user.email} + +
+
+ + {t("profile.language")} + + + {languages.find((lang) => lang.code === i18n.language) + ?.display_name || "-"} + +
+
+ + {t("profile.usageInsights")} + + + {t("general.disabled")} + +
+
+ ) +} diff --git a/packages/admin-next/dashboard/src/routes/profile/profile-detail/index.ts b/packages/admin-next/dashboard/src/routes/profile/profile-detail/index.ts new file mode 100644 index 0000000000..9d981a433d --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/profile/profile-detail/index.ts @@ -0,0 +1 @@ +export { ProfileDetail as Component } from "./profile-detail" diff --git a/packages/admin-next/dashboard/src/routes/profile/profile-detail/profile-detail.tsx b/packages/admin-next/dashboard/src/routes/profile/profile-detail/profile-detail.tsx new file mode 100644 index 0000000000..c69975be28 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/profile/profile-detail/profile-detail.tsx @@ -0,0 +1,26 @@ +import { useAdminGetSession } from "medusa-react" +import { Outlet, json } from "react-router-dom" +import { ProfileGeneralSection } from "./components/profile-general-section" + +export const ProfileDetail = () => { + const { user, isLoading, isError, error } = useAdminGetSession() + + if (isLoading) { + return
Loading...
+ } + + if (isError || !user) { + if (error) { + throw error + } + + throw json("An unknown error has occured", 500) + } + + return ( +
+ + +
+ ) +} diff --git a/packages/admin-next/dashboard/src/routes/profile/profile-edit/components/edit-profile-form/edit-profile-form.tsx b/packages/admin-next/dashboard/src/routes/profile/profile-edit/components/edit-profile-form/edit-profile-form.tsx new file mode 100644 index 0000000000..7c616718aa --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/profile/profile-edit/components/edit-profile-form/edit-profile-form.tsx @@ -0,0 +1,208 @@ +import { User } from "@medusajs/medusa" +import * as zod from "zod" + +import { zodResolver } from "@hookform/resolvers/zod" +import { Button, Drawer, Input, Select, Switch } from "@medusajs/ui" +import { adminAuthKeys, useAdminUpdateUser } from "medusa-react" +import { useForm } from "react-hook-form" +import { Trans, useTranslation } from "react-i18next" +import { Form } from "../../../../../components/common/form" +import { languages } from "../../../../../i18n/config" +import { queryClient } from "../../../../../lib/medusa" + +type EditProfileProps = { + user: Omit + usageInsights: boolean + onSuccess: () => void +} + +const EditProfileSchema = zod.object({ + first_name: zod.string().optional(), + last_name: zod.string().optional(), + language: zod.string(), + usage_insights: zod.boolean(), +}) + +export const EditProfileForm = ({ + user, + usageInsights, + onSuccess, +}: EditProfileProps) => { + const { t, i18n } = useTranslation() + const { mutateAsync, isLoading } = useAdminUpdateUser(user.id) + + const form = useForm>({ + defaultValues: { + first_name: user.first_name ?? "", + last_name: user.last_name ?? "", + language: i18n.language, + usage_insights: usageInsights, + }, + resolver: zodResolver(EditProfileSchema), + }) + + const changeLanguage = (code: string) => { + i18n.changeLanguage(code) + } + + const sortedLanguages = languages.sort((a, b) => + a.display_name.localeCompare(b.display_name) + ) + + const handleSubmit = form.handleSubmit(async (values) => { + await mutateAsync( + { + first_name: values.first_name, + last_name: values.last_name, + }, + { + onSuccess: ({ user }) => { + form.reset({ + first_name: user.first_name, + last_name: user.last_name, + }) + + // Invalidate the current user session. + queryClient.invalidateQueries(adminAuthKeys.details()) + }, + onError: () => { + return + }, + } + ) + + changeLanguage(values.language) + + onSuccess() + }) + + return ( +
+ + +
+
+ ( + + {t("fields.firstName")} + + + + + + )} + /> + ( + + {t("fields.lastName")} + + + + + + )} + /> +
+ ( + +
+ {t("profile.language")} + {t("profile.languageHint")} +
+
+ + + + +
+
+ )} + /> + ( + +
+ {t("profile.usageInsights")} + + + +
+ + + , + ]} + /> + + + +
+ )} + /> +
+
+ +
+ + + + +
+
+
+ + ) +} diff --git a/packages/admin-next/dashboard/src/routes/profile/profile-edit/index.ts b/packages/admin-next/dashboard/src/routes/profile/profile-edit/index.ts new file mode 100644 index 0000000000..99be2393cb --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/profile/profile-edit/index.ts @@ -0,0 +1 @@ +export { ProfileEdit as Component } from "./profile-edit" diff --git a/packages/admin-next/dashboard/src/routes/profile/profile-edit/profile-edit.tsx b/packages/admin-next/dashboard/src/routes/profile/profile-edit/profile-edit.tsx new file mode 100644 index 0000000000..4154cb7f49 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/profile/profile-edit/profile-edit.tsx @@ -0,0 +1,52 @@ +import { Drawer, Heading } from "@medusajs/ui" +import { useAdminGetSession } from "medusa-react" +import { useEffect, useState } from "react" +import { useTranslation } from "react-i18next" +import { useNavigate } from "react-router-dom" +import { EditProfileForm } from "./components/edit-profile-form/edit-profile-form" + +export const ProfileEdit = () => { + const [open, setOpen] = useState(false) + const navigate = useNavigate() + + const { user, isLoading, isError, error } = useAdminGetSession() + + const { t } = useTranslation() + + useEffect(() => { + setOpen(true) + }, []) + + const onOpenChange = (open: boolean) => { + if (!open) { + setTimeout(() => { + navigate(`/settings/profile`, { replace: true }) + }, 200) + } + + setOpen(open) + } + + if (isError) { + throw error + } + + return ( + + + + {t("profile.editProfile")} + + {isLoading || !user ? ( +
Loading...
+ ) : ( + onOpenChange(false)} + /> + )} +
+
+ ) +} diff --git a/packages/admin-next/dashboard/src/routes/profile/views/profile-details/index.ts b/packages/admin-next/dashboard/src/routes/profile/views/profile-details/index.ts deleted file mode 100644 index 0402621d02..0000000000 --- a/packages/admin-next/dashboard/src/routes/profile/views/profile-details/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { Profile as Component } from "./profile"; diff --git a/packages/admin-next/dashboard/src/routes/profile/views/profile-details/profile.tsx b/packages/admin-next/dashboard/src/routes/profile/views/profile-details/profile.tsx deleted file mode 100644 index 6e607d3608..0000000000 --- a/packages/admin-next/dashboard/src/routes/profile/views/profile-details/profile.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import { Container, Heading, Text } from "@medusajs/ui" - -import { Spinner } from "@medusajs/icons" -import { useAdminGetSession } from "medusa-react" -import { useTranslation } from "react-i18next" -import { languages } from "../../../../i18n/config" -import { EditProfileDetailsDrawer } from "../../components/edit-profile-details-drawer/edit-profile-details-drawer" - -export const Profile = () => { - const { user, isLoading } = useAdminGetSession() - const { i18n, t } = useTranslation() - - if (isLoading || !user) { - return ( -
- -
- ) - } - - return ( -
- -
- {t("profile.domain")} - - {t("profile.manageYourProfileDetails")} - -
-
- - Name - - - {user.first_name} {user.last_name} - -
-
- - Email - - - {user.email} - -
-
- - Language - - - {languages.find((lang) => lang.code === i18n.language) - ?.display_name || "-"} - -
-
- - Usage insights - -
-
- -
-
-
- ) -} diff --git a/packages/admin-next/dashboard/src/routes/regions/components/edit-region-details-drawer/edit-region-details-drawer.tsx b/packages/admin-next/dashboard/src/routes/regions/components/edit-region-details-drawer/edit-region-details-drawer.tsx deleted file mode 100644 index 8752f83a90..0000000000 --- a/packages/admin-next/dashboard/src/routes/regions/components/edit-region-details-drawer/edit-region-details-drawer.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { Drawer } from "@medusajs/ui" -import { useState } from "react" - -export const EditRegionDetailsDrawer = () => { - const [open, setOpen] = useState(false) - - return ( - - - - ) -} diff --git a/packages/admin-next/dashboard/src/routes/regions/region-create/index.ts b/packages/admin-next/dashboard/src/routes/regions/region-create/index.ts new file mode 100644 index 0000000000..cbd7dbdfe1 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/regions/region-create/index.ts @@ -0,0 +1 @@ +export { RegionCreate as Component } from "./region-create" diff --git a/packages/admin-next/dashboard/src/routes/regions/region-create/region-create.tsx b/packages/admin-next/dashboard/src/routes/regions/region-create/region-create.tsx new file mode 100644 index 0000000000..191e96b1d4 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/regions/region-create/region-create.tsx @@ -0,0 +1,201 @@ +import { zodResolver } from "@hookform/resolvers/zod" +import { Button, FocusModal, Heading, Input, Switch, Text } from "@medusajs/ui" +import { useAdminCreateRegion } from "medusa-react" +import { useForm } from "react-hook-form" +import { useTranslation } from "react-i18next" +import * as zod from "zod" + +import { useNavigate } from "react-router-dom" +import { Form } from "../../../components/common/form" +import { useRouteModalState } from "../../../hooks/use-route-modal-state" + +const CreateRegionFormSchema = zod.object({ + name: zod.string().min(1), + currency_code: zod.string(), + includes_tax: zod.boolean(), + countries: zod.array(zod.string()), + fulfillment_providers: zod.array(zod.string()).min(1), + payment_providers: zod.array(zod.string()).min(1), + tax_rate: zod.number().min(0).max(100), + tax_code: zod.string().optional(), +}) + +export const RegionCreate = () => { + const [open, onOpenChange] = useRouteModalState() + const navigate = useNavigate() + + const { t } = useTranslation() + + const form = useForm>({ + defaultValues: { + name: "", + currency_code: "", + includes_tax: false, + countries: [], + fulfillment_providers: [], + payment_providers: [], + tax_code: "", + }, + resolver: zodResolver(CreateRegionFormSchema), + }) + + const { mutateAsync, isLoading } = useAdminCreateRegion() + + const handleSubmit = form.handleSubmit(async (values) => { + await mutateAsync( + { + name: values.name, + countries: values.countries, + currency_code: values.currency_code, + fulfillment_providers: values.fulfillment_providers, + payment_providers: values.payment_providers, + tax_rate: values.tax_rate, + tax_code: values.tax_code, + includes_tax: values.includes_tax, + }, + { + onSuccess: ({ region }) => { + navigate(`../${region.id}`) + }, + } + ) + }) + + return ( + + +
+ + +
+ + + + +
+
+ +
+ +
+
+ { + return ( + + {t("fields.name")} + + + + + + ) + }} + /> + { + return ( + + {t("fields.currency")} + + + + ) + }} + /> +
+
+ { + return ( + + {t("fields.taxRate")} + + + + + + ) + }} + /> + { + return ( + + + {t("fields.taxCode")} + + + + + + + ) + }} + /> +
+
+ { + return ( + +
+
+ + {t("fields.taxInclusivePricing")} + + + + +
+ {t("regions.taxInclusiveHint")} + +
+
+ ) + }} + /> +
+
+ + {t("fields.providers")} + + + {t("regions.providersHint")} + +
+
+
+
+
+ + {t("fields.metadata")} + +
+
+
+
+
+ +
+
+ ) +} diff --git a/packages/admin-next/dashboard/src/routes/regions/region-detail/components/region-general-section/index.ts b/packages/admin-next/dashboard/src/routes/regions/region-detail/components/region-general-section/index.ts new file mode 100644 index 0000000000..73ca48c45c --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/regions/region-detail/components/region-general-section/index.ts @@ -0,0 +1 @@ +export * from "./region-general-section" diff --git a/packages/admin-next/dashboard/src/routes/regions/region-detail/components/region-general-section/region-general-section.tsx b/packages/admin-next/dashboard/src/routes/regions/region-detail/components/region-general-section/region-general-section.tsx new file mode 100644 index 0000000000..860ef29da6 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/regions/region-detail/components/region-general-section/region-general-section.tsx @@ -0,0 +1,230 @@ +import { + BuildingTax, + EllipsisHorizontal, + PencilSquare, + Trash, +} from "@medusajs/icons" +import { Country, Region } from "@medusajs/medusa" +import { + Badge, + Button, + Container, + Drawer, + DropdownMenu, + Heading, + IconButton, + StatusBadge, + Text, + Tooltip, + usePrompt, +} from "@medusajs/ui" +import { useAdminDeleteRegion, useAdminUpdateRegion } from "medusa-react" +import { useForm } from "react-hook-form" +import { useTranslation } from "react-i18next" +import { Link } from "react-router-dom" +import * as zod from "zod" + +type RegionGeneralSectionProps = { + region: Region +} + +export const RegionGeneralSection = ({ region }: RegionGeneralSectionProps) => { + const { t } = useTranslation() + + return ( + +
+
+ {region.name} + +
+ +
+
+ + {t("fields.currency")} + +
+ + {region.currency_code} + + + {region.currency?.name} + +
+
+
+ + {t("fields.taxInclusivePricing")} + + + {region.includes_tax ? t("general.enabled") : t("general.disabled")} + +
+
+ + {t("fields.paymentProviders")} + + + {region.payment_providers.length > 0 + ? region.payment_providers.map((p) => p.id).join(", ") + : "-"} + +
+
+ + {t("fields.fulfillmentProviders")} + + + {region.fulfillment_providers.length > 0 + ? region.fulfillment_providers.map((p) => p.id).join(", ") + : "-"} + +
+
+ ) +} + +const RegionActions = ({ region }: { region: Region }) => { + const { t } = useTranslation() + const { mutateAsync } = useAdminDeleteRegion(region.id) + const prompt = usePrompt() + + const handleDelete = async () => { + const res = await prompt({ + title: t("general.areYouSure"), + description: t("regions.deleteRegionWarning", { + name: region.name, + }), + verificationText: region.name, + verificationInstruction: t("general.typeToConfirm"), + confirmText: t("general.delete"), + cancelText: t("general.cancel"), + }) + + if (!res) { + return + } + + await mutateAsync(undefined) + } + + return ( + + + + + + + + + +
+ + {t("general.edit")} +
+
+ + + +
+ + Tax settings +
+
+ + + +
+ + Delete +
+
+
+
+ ) +} + +const RegionCountries = ({ countries }: { countries: Country[] }) => { + const { t } = useTranslation() + + return ( +
+ + {countries + .slice(0, 2) + .map((c) => c.display_name) + .join(", ")} + + {countries.length > 2 && ( + + {countries.slice(2).map((c) => ( +
  • {c.display_name}
  • + ))} + + } + > + + {t("general.plusCountMore", { + count: countries.length - 2, + })} + +
    + )} +
    + ) +} + +const EditRegionSchema = zod.object({ + name: zod.string().min(1), + includes_tax: zod.boolean(), + currency_code: zod.string(), + countries: zod.array(zod.string()), +}) + +const EditRegionDrawer = ({ region }: { region: Region }) => { + const { t } = useTranslation() + const { mutateAsync, isLoading } = useAdminUpdateRegion(region.id) + + const form = useForm>({ + defaultValues: { + name: region.name, + currency_code: region.currency_code, + includes_tax: region.includes_tax, + countries: region.countries.map((c) => c.iso_2), + }, + }) + + const handleSubmit = form.handleSubmit(async (values) => { + await mutateAsync({ + name: values.name, + currency_code: values.currency_code, + includes_tax: values.includes_tax, + }) + }) + + return ( + + + + {t("regions.editRegion")} + + + + + + + + + ) +} diff --git a/packages/admin-next/dashboard/src/routes/regions/region-detail/components/region-shipping-option-section/region-shipping-option-section.tsx b/packages/admin-next/dashboard/src/routes/regions/region-detail/components/region-shipping-option-section/region-shipping-option-section.tsx new file mode 100644 index 0000000000..488fe486a5 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/regions/region-detail/components/region-shipping-option-section/region-shipping-option-section.tsx @@ -0,0 +1,156 @@ +import { Region, ShippingOption } from "@medusajs/medusa" +import { Container, StatusBadge, Table, clx } from "@medusajs/ui" +import { + PaginationState, + RowSelectionState, + createColumnHelper, + flexRender, + getCoreRowModel, + useReactTable, +} from "@tanstack/react-table" +import { useAdminShippingOptions } from "medusa-react" +import { useMemo, useState } from "react" +import { useTranslation } from "react-i18next" +import { LocalizedTablePagination } from "../../../../../components/localization/localized-table-pagination" + +type RegionShippingOptionSectionProps = { + region: Region +} + +const PAGE_SIZE = 20 + +// TODO: Need to fix pagination and search for shipping options +export const RegionShippingOptionSection = ({ + region, +}: RegionShippingOptionSectionProps) => { + const [{ pageIndex, pageSize }, setPagination] = useState({ + pageIndex: 0, + pageSize: PAGE_SIZE, + }) + + const pagination = useMemo( + () => ({ + pageIndex, + pageSize, + }), + [pageIndex, pageSize] + ) + + const { shipping_options, count, isError, error, isLoading } = + useAdminShippingOptions({ + region_id: region.id, + }) + + const [rowSelection, setRowSelection] = useState({}) + + const columns = useShippingOptionColumns() + + const table = useReactTable({ + data: shipping_options ?? [], + columns, + pageCount: Math.ceil((count ?? 0) / PAGE_SIZE), + state: { + pagination, + rowSelection, + }, + manualPagination: true, + getCoreRowModel: getCoreRowModel(), + onPaginationChange: setPagination, + onRowSelectionChange: setRowSelection, + }) + + if (isLoading) { + return
    Loading...
    + } + + if (isError) { + throw error + } + + return ( + +
    {/* Filters go here */}
    + + + {table.getHeaderGroups().map((headerGroup) => { + return ( + + {headerGroup.headers.map((header) => { + return ( + + {flexRender( + header.column.columnDef.header, + header.getContext() + )} + + ) + })} + + ) + })} + + + {table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} + + ))} + +
    + +
    + ) +} + +const columnHelper = createColumnHelper() + +const useShippingOptionColumns = () => { + const { t } = useTranslation() + + return useMemo( + () => [ + columnHelper.accessor("name", { + header: t("fields.name"), + cell: (cell) => cell.getValue(), + }), + columnHelper.accessor("admin_only", { + header: t("fields.availability"), + cell: (cell) => { + const value = cell.getValue() + + return ( + + {value ? t("general.admin") : t("general.store")} + + ) + }, + }), + ], + [t] + ) +} diff --git a/packages/admin-next/dashboard/src/routes/regions/region-detail/index.ts b/packages/admin-next/dashboard/src/routes/regions/region-detail/index.ts new file mode 100644 index 0000000000..17c680a88e --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/regions/region-detail/index.ts @@ -0,0 +1,2 @@ +export { regionLoader as loader } from "./loader" +export { RegionDetail as Component } from "./region-detail" diff --git a/packages/admin-next/dashboard/src/routes/regions/region-detail/loader.ts b/packages/admin-next/dashboard/src/routes/regions/region-detail/loader.ts new file mode 100644 index 0000000000..e8c97d2216 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/regions/region-detail/loader.ts @@ -0,0 +1,23 @@ +import { AdminProductsRes } from "@medusajs/medusa" +import { Response } from "@medusajs/medusa-js" +import { adminRegionKeys } from "medusa-react" +import { LoaderFunctionArgs } from "react-router-dom" + +import { medusa, queryClient } from "../../../lib/medusa" + +const regionQuery = (id: string) => ({ + queryKey: adminRegionKeys.detail(id), + queryFn: async () => medusa.admin.regions.retrieve(id), +}) + +export const regionLoader = async ({ params }: LoaderFunctionArgs) => { + const id = params.id + const query = regionQuery(id!) + + console.log("regionLoader", query) + + return ( + queryClient.getQueryData>(query.queryKey) ?? + (await queryClient.fetchQuery(query)) + ) +} diff --git a/packages/admin-next/dashboard/src/routes/regions/region-detail/region-detail.tsx b/packages/admin-next/dashboard/src/routes/regions/region-detail/region-detail.tsx new file mode 100644 index 0000000000..eeca1c0a65 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/regions/region-detail/region-detail.tsx @@ -0,0 +1,42 @@ +import { useAdminRegion } from "medusa-react" +import { Outlet, useNavigate, useParams } from "react-router-dom" + +import { JsonViewSection } from "../../../components/common/json-view-section" +import { RegionGeneralSection } from "./components/region-general-section" +import { RegionShippingOptionSection } from "./components/region-shipping-option-section/region-shipping-option-section" + +export const RegionDetail = () => { + const { id } = useParams() + const { region, isLoading, isError, error } = useAdminRegion(id!) + const navigate = useNavigate() + + // TODO: Move to loading.tsx and set as Suspense fallback for the route + if (isLoading) { + return
    Loading
    + } + + // TODO: Move to error.tsx and set as ErrorBoundary for the route + if (isError || !region) { + const err = error ? JSON.parse(JSON.stringify(error)) : null + return ( +
    + {(err as Error & { status: number })?.status === 404 ? ( +
    Not found
    + ) : ( +
    Something went wrong!
    + )} +
    + ) + } + + console.log("RegionDetail") + + return ( +
    + + + + +
    + ) +} diff --git a/packages/admin-next/dashboard/src/routes/regions/region-edit/components/edit-region-form/edit-region-form.tsx b/packages/admin-next/dashboard/src/routes/regions/region-edit/components/edit-region-form/edit-region-form.tsx new file mode 100644 index 0000000000..00c008540a --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/regions/region-edit/components/edit-region-form/edit-region-form.tsx @@ -0,0 +1,105 @@ +import { Region } from "@medusajs/medusa" +import { Button, Drawer, Input, Switch } from "@medusajs/ui" +import { useAdminUpdateRegion } from "medusa-react" +import { useForm } from "react-hook-form" +import { useTranslation } from "react-i18next" +import * as zod from "zod" +import { Form } from "../../../../../components/common/form" + +type EditRegionFormProps = { + region: Region +} + +const EditRegionSchema = zod.object({ + name: zod.string().min(1), + includes_tax: zod.boolean(), + currency_code: zod.string(), + countries: zod.array(zod.string()), +}) + +export const EditRegionForm = ({ region }: EditRegionFormProps) => { + const { mutateAsync, isLoading } = useAdminUpdateRegion(region.id) + + const form = useForm>({ + defaultValues: { + name: region.name, + currency_code: region.currency_code, + includes_tax: region.includes_tax, + countries: region.countries.map((c) => c.iso_2), + }, + }) + + const { t } = useTranslation() + + const handleSubmit = form.handleSubmit(async (values) => { + await mutateAsync({ + name: values.name, + currency_code: values.currency_code, + includes_tax: values.includes_tax, + }) + }) + + return ( +
    + + +
    + { + return ( + + {t("fields.name")} + + + + + + ) + }} + /> + { + return ( + +
    +
    + + {t("fields.taxInclusivePricing")} + + {t("regions.taxInclusiveHint")} +
    + + + +
    + +
    + ) + }} + /> +
    +
    + +
    + + + + +
    +
    +
    + + ) +} diff --git a/packages/admin-next/dashboard/src/routes/regions/region-edit/components/edit-region-form/index.ts b/packages/admin-next/dashboard/src/routes/regions/region-edit/components/edit-region-form/index.ts new file mode 100644 index 0000000000..b3dbbd11b0 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/regions/region-edit/components/edit-region-form/index.ts @@ -0,0 +1 @@ +export * from "./edit-region-form" diff --git a/packages/admin-next/dashboard/src/routes/regions/region-edit/index.ts b/packages/admin-next/dashboard/src/routes/regions/region-edit/index.ts new file mode 100644 index 0000000000..931fd4c574 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/regions/region-edit/index.ts @@ -0,0 +1 @@ +export { RegionEdit as Component } from "./region-edit" diff --git a/packages/admin-next/dashboard/src/routes/regions/region-edit/region-edit.tsx b/packages/admin-next/dashboard/src/routes/regions/region-edit/region-edit.tsx new file mode 100644 index 0000000000..9cc8af616d --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/regions/region-edit/region-edit.tsx @@ -0,0 +1,49 @@ +import { Drawer, Heading } from "@medusajs/ui" +import { useAdminRegion } from "medusa-react" +import { useEffect, useState } from "react" +import { useTranslation } from "react-i18next" +import { json, useNavigate, useParams } from "react-router-dom" + +import { EditRegionForm } from "./components/edit-region-form" + +export const RegionEdit = () => { + const [open, setOpen] = useState(false) + const { id } = useParams() + const { region, isLoading, isError, error } = useAdminRegion(id!) + const navigate = useNavigate() + + useEffect(() => { + setOpen(true) + }, []) + + const onOpenChange = (open: boolean) => { + if (!open) { + setTimeout(() => { + navigate(`/settings/regions/${id}`, { replace: true }) + }, 200) + } + + setOpen(open) + } + + const { t } = useTranslation() + + if (isError) { + throw error + } + + if (!region && !isLoading) { + throw json("An unknown error has occured", 500) + } + + return ( + + + + {t("regions.editRegion")} + + {region && } + + + ) +} diff --git a/packages/admin-next/dashboard/src/routes/regions/region-list/components/region-list-table/index.ts b/packages/admin-next/dashboard/src/routes/regions/region-list/components/region-list-table/index.ts new file mode 100644 index 0000000000..e91f99376a --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/regions/region-list/components/region-list-table/index.ts @@ -0,0 +1 @@ +export * from "./region-list-table" diff --git a/packages/admin-next/dashboard/src/routes/regions/region-list/components/region-list-table/region-list-table.tsx b/packages/admin-next/dashboard/src/routes/regions/region-list/components/region-list-table/region-list-table.tsx new file mode 100644 index 0000000000..e90b7b7987 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/regions/region-list/components/region-list-table/region-list-table.tsx @@ -0,0 +1,302 @@ +import { EllipsisHorizontal, PencilSquare, Trash } from "@medusajs/icons" +import { Region } from "@medusajs/medusa" +import { + Button, + Container, + DropdownMenu, + Heading, + IconButton, + Table, + Tooltip, + clx, +} from "@medusajs/ui" +import { + PaginationState, + RowSelectionState, + createColumnHelper, + flexRender, + getCoreRowModel, + useReactTable, +} from "@tanstack/react-table" +import { useAdminRegions } from "medusa-react" +import { useMemo, useState } from "react" +import { useTranslation } from "react-i18next" +import { Link, useNavigate } from "react-router-dom" +import { LocalizedTablePagination } from "../../../../../components/localization/localized-table-pagination" + +const PAGE_SIZE = 50 + +export const RegionListTable = () => { + const navigate = useNavigate() + const { t } = useTranslation() + + const [{ pageIndex, pageSize }, setPagination] = useState({ + pageIndex: 0, + pageSize: PAGE_SIZE, + }) + + const pagination = useMemo( + () => ({ + pageIndex, + pageSize, + }), + [pageIndex, pageSize] + ) + + const [rowSelection, setRowSelection] = useState({}) + + const { regions, count, isLoading, isError, error } = useAdminRegions({ + limit: PAGE_SIZE, + offset: pageIndex * PAGE_SIZE, + }) + + const columns = useColumns() + + const table = useReactTable({ + data: regions ?? [], + columns, + pageCount: Math.ceil((count ?? 0) / PAGE_SIZE), + state: { + pagination, + rowSelection, + }, + onPaginationChange: setPagination, + onRowSelectionChange: setRowSelection, + getCoreRowModel: getCoreRowModel(), + manualPagination: true, + }) + + if (isError) { + throw error + } + + return ( + +
    + {t("regions.domain")} + + + +
    + + + {table.getHeaderGroups().map((headerGroup) => { + return ( + + {headerGroup.headers.map((header) => { + return ( + + {flexRender( + header.column.columnDef.header, + header.getContext() + )} + + ) + })} + + ) + })} + + + {table.getRowModel().rows.map((row) => ( + navigate(`/settings/regions/${row.original.id}`)} + > + {row.getVisibleCells().map((cell) => ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} + + ))} + +
    + +
    + ) +} + +const RegionActions = ({ region }: { region: Region }) => { + const { t } = useTranslation() + + return ( + + e.stopPropagation()}> + + + + + + + e.stopPropagation()}> +
    + + {t("general.edit")} +
    +
    + + + e.stopPropagation()}> +
    + + {t("general.delete")} +
    +
    +
    +
    + ) +} + +const columnHelper = createColumnHelper() + +const useColumns = () => { + const { t } = useTranslation() + + return useMemo( + () => [ + columnHelper.accessor("name", { + header: t("fields.name"), + cell: (cell) => cell.getValue(), + }), + columnHelper.accessor("countries", { + header: t("fields.countries"), + cell: (cell) => { + const countries = cell.getValue() + + const displayValue = countries + .slice(0, 2) + .map((c) => c.display_name) + .join(", ") + + const additionalCountries = countries + .slice(2) + .map((c) => c.display_name) + + return ( +
    + {displayValue} + {additionalCountries.length > 0 && ( + + {additionalCountries.map((c) => ( +
  • {c}
  • + ))} + + } + > + + {t("general.plusCountMore", { + count: additionalCountries.length, + })} + +
    + )} +
    + ) + }, + }), + columnHelper.accessor("payment_providers", { + header: t("fields.paymentProviders"), + cell: (cell) => { + const providers = cell.getValue() + + const displayValue = providers + .slice(0, 2) + .map((p) => p.id) + .join(", ") + + const additionalProviders = providers.slice(2).map((c) => c.id) + + return ( +
    + {displayValue} + {additionalProviders.length > 0 && ( + + {additionalProviders.map((c) => ( +
  • {c}
  • + ))} + + } + > + + {t("general.plusCountMore", { + count: additionalProviders.length, + })} + +
    + )} +
    + ) + }, + }), + columnHelper.accessor("fulfillment_providers", { + header: t("fields.fulfillmentProviders"), + cell: (cell) => { + const providers = cell.getValue() + + const displayValue = providers + .slice(0, 2) + .map((p) => p.id) + .join(", ") + + const additionalProviders = providers.slice(2).map((c) => c.id) + + return ( +
    + {displayValue} + {additionalProviders.length > 0 && ( + + {additionalProviders.map((c) => ( +
  • {c}
  • + ))} + + } + > + + {t("general.plusCountMore", { + count: additionalProviders.length, + })} + +
    + )} +
    + ) + }, + }), + columnHelper.display({ + id: "actions", + cell: ({ row }) => { + return + }, + }), + ], + [t] + ) +} diff --git a/packages/admin-next/dashboard/src/routes/regions/views/region-list/index.ts b/packages/admin-next/dashboard/src/routes/regions/region-list/index.ts similarity index 100% rename from packages/admin-next/dashboard/src/routes/regions/views/region-list/index.ts rename to packages/admin-next/dashboard/src/routes/regions/region-list/index.ts diff --git a/packages/admin-next/dashboard/src/routes/regions/region-list/region-list.tsx b/packages/admin-next/dashboard/src/routes/regions/region-list/region-list.tsx new file mode 100644 index 0000000000..fb22082bb6 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/regions/region-list/region-list.tsx @@ -0,0 +1,13 @@ +import { Outlet } from "react-router-dom" +import { RegionListTable } from "./components/region-list-table" + +export const RegionList = () => { + console.log("RegionList") + + return ( +
    + + +
    + ) +} diff --git a/packages/admin-next/dashboard/src/routes/regions/views/region-details/index.ts b/packages/admin-next/dashboard/src/routes/regions/views/region-details/index.ts deleted file mode 100644 index cd1dedc129..0000000000 --- a/packages/admin-next/dashboard/src/routes/regions/views/region-details/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { RegionDetails as Component } from "./region-details" diff --git a/packages/admin-next/dashboard/src/routes/regions/views/region-details/region-details.tsx b/packages/admin-next/dashboard/src/routes/regions/views/region-details/region-details.tsx deleted file mode 100644 index 2f00216e38..0000000000 --- a/packages/admin-next/dashboard/src/routes/regions/views/region-details/region-details.tsx +++ /dev/null @@ -1,142 +0,0 @@ -import { EllipsisHorizontal } from "@medusajs/icons" -import { - Badge, - Container, - Heading, - IconButton, - Text, - Tooltip, -} from "@medusajs/ui" -import { useAdminRegion } from "medusa-react" -import { useTranslation } from "react-i18next" -import { useParams } from "react-router-dom" - -export const RegionDetails = () => { - const { id } = useParams() - const { region, isLoading, isError, error } = useAdminRegion(id!) - - const { t } = useTranslation() - - // TODO: Move to loading.tsx and set as Suspense fallback for the route - if (isLoading) { - return
    Loading
    - } - - // TODO: Move to error.tsx and set as ErrorBoundary for the route - if (isError || !region) { - const err = error ? JSON.parse(JSON.stringify(error)) : null - return ( -
    - {(err as Error & { status: number })?.status === 404 ? ( -
    Not found
    - ) : ( -
    Something went wrong!
    - )} -
    - ) - } - - return ( -
    - -
    -
    - {region.name} -
    - - {region.countries - .slice(0, 2) - .map((c) => c.display_name) - .join(", ")} - - {region.countries.length > 2 && ( - - {region.countries.slice(2).map((c) => ( -
  • {c.display_name}
  • - ))} - - } - > - - {t("general.plusCountMore", { - count: region.countries.length - 2, - })} - -
    - )} -
    -
    - - - -
    -
    - - Currency - -
    - - {region.currency_code} - - - {region.currency.name} - -
    -
    -
    - - Default Tax Rate - - - {region.tax_rate} - -
    -
    - - Default Tax Code - - - {region.tax_code ?? "-"} - -
    -
    - - Tax Inclusive Pricing - - - {region.includes_tax ? t("general.enabled") : t("general.disabled")} - -
    -
    - - Payment Providers - - - {region.payment_providers.length > 0 - ? region.payment_providers.map((p) => p.id).join(", ") - : "-"} - -
    -
    - - Fulfillment Providers - - - {region.fulfillment_providers.length > 0 - ? region.fulfillment_providers.map((p) => p.id).join(", ") - : "-"} - -
    -
    - - -
    - ) -} diff --git a/packages/admin-next/dashboard/src/routes/regions/views/region-list/region-list.tsx b/packages/admin-next/dashboard/src/routes/regions/views/region-list/region-list.tsx deleted file mode 100644 index 84c4592430..0000000000 --- a/packages/admin-next/dashboard/src/routes/regions/views/region-list/region-list.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { useAdminRegions } from "medusa-react" -import { Link } from "react-router-dom" - -export const RegionList = () => { - const { regions, isLoading, isError, error } = useAdminRegions() - - if (isLoading) { - return
    Loading...
    - } - - if (isError || !regions) { - return
    Error
    - } - - return ( -
    - {regions.map((region) => { - return ( -
    - {region.name} -
    - ) - })} -
    - ) -} diff --git a/packages/admin-next/dashboard/src/routes/sales-channels/components/sales-channel-details-section/index.ts b/packages/admin-next/dashboard/src/routes/sales-channels/components/sales-channel-details-section/index.ts deleted file mode 100644 index a2677176a7..0000000000 --- a/packages/admin-next/dashboard/src/routes/sales-channels/components/sales-channel-details-section/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./sales-channel-details-section" diff --git a/packages/admin-next/dashboard/src/routes/sales-channels/components/sales-channel-details-section/sales-channel-details-section.tsx b/packages/admin-next/dashboard/src/routes/sales-channels/components/sales-channel-details-section/sales-channel-details-section.tsx deleted file mode 100644 index 559c432748..0000000000 --- a/packages/admin-next/dashboard/src/routes/sales-channels/components/sales-channel-details-section/sales-channel-details-section.tsx +++ /dev/null @@ -1,200 +0,0 @@ -import { zodResolver } from "@hookform/resolvers/zod" -import { EllipsisHorizontal, PencilSquare, Trash } from "@medusajs/icons" -import { SalesChannel } from "@medusajs/medusa" -import { - Button, - Container, - Drawer, - DropdownMenu, - Heading, - IconButton, - Input, - StatusBadge, - Switch, - Text, - Textarea, -} from "@medusajs/ui" -import { useAdminUpdateSalesChannel } 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" - -type Props = { - salesChannel: SalesChannel -} - -export const SalesChannelDetailsSection = ({ salesChannel }: Props) => { - const [drawerOpen, setDrawerOpen] = useState(false) - const { t } = useTranslation() - - return ( -
    - -
    -
    - {salesChannel.name} - - {salesChannel.description} - -
    -
    - - {t( - `general.${salesChannel.is_disabled ? "disabled" : "enabled"}` - )} - - - - - - - - - setDrawerOpen(!drawerOpen)} - > - - {t("general.edit")} - - - - - {t("general.delete")} - - - -
    -
    -
    - -
    - ) -} - -const EditSalesChannelDetailsSchema = zod.object({ - name: zod.string().min(1), - description: zod.string().optional(), - is_active: zod.boolean(), -}) - -type DrawerProps = { - salesChannel: SalesChannel - open: boolean - setOpen: (open: boolean) => void -} - -const EditSalesChannelDetailsDrawer = ({ - salesChannel, - open, - setOpen, -}: DrawerProps) => { - const form = useForm>({ - defaultValues: { - name: salesChannel.name, - description: salesChannel.description ?? "", - is_active: !salesChannel.is_disabled, - }, - resolver: zodResolver(EditSalesChannelDetailsSchema), - }) - const { mutateAsync, isLoading } = useAdminUpdateSalesChannel(salesChannel.id) - const { t } = useTranslation() - - const onSubmit = form.handleSubmit(async (values) => { - await mutateAsync( - { - name: values.name, - description: values.description ?? undefined, - is_disabled: !values.is_active, - }, - { - onSuccess: () => { - setOpen(false) - }, - } - ) - }) - - return ( - -
    - - - {t("salesChannels.editSalesChannel")} - - - { - return ( - - {t("fields.name")} - - - - - - ) - }} - /> - { - return ( - - {t("fields.description")} - -